@checodo/better-auth-authentication 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @checodo/better-auth-authentication
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5f427b0: First release: sign in with Steam, Epic Games, and Xbox through better-auth's own `signIn.social`, like any built-in social provider.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clas Heinemeyer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @checodo/better-auth-authentication
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sign in with Steam, Epic Games, and Xbox in [better-auth](https://better-auth.com), through `signIn.social` like any built-in social provider.
4
+
5
+ A community plugin, not made by the better-auth team. It looks and works
6
+ like better-auth's own plugins: a server plugin, a typed client plugin,
7
+ error codes.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @checodo/better-auth-authentication
13
+ ```
14
+
15
+ Requires `better-auth` and `@better-auth/core` 1.7.20 or a later 1.x
16
+ release (peer dependencies). ESM only.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ // auth.ts
22
+ import { betterAuth } from "better-auth";
23
+ import { authentication } from "@checodo/better-auth-authentication";
24
+
25
+ export const auth = betterAuth({
26
+ plugins: [
27
+ authentication({
28
+ steam: { apiKey: process.env.STEAM_API_KEY },
29
+ }),
30
+ ],
31
+ });
32
+ ```
33
+
34
+ ```ts
35
+ // auth-client.ts
36
+ import { createAuthClient } from "better-auth/client";
37
+ import { authenticationClient } from "@checodo/better-auth-authentication/client";
38
+
39
+ export const authClient = createAuthClient({ plugins: [authenticationClient()] });
40
+ ```
41
+
42
+ ## Documentation
43
+
44
+ Every option, endpoint, error code, and table:
45
+ [docs](https://github.com/checodo/better-auth-plugins/blob/main/apps/docs/content/docs/authentication/index.mdx).
@@ -0,0 +1,25 @@
1
+ import { i as AUTHENTICATION_ERROR_CODES, r as AUTHENTICATION_CALLBACK_ERRORS, t as AuthenticationPlugin } from "./index-B-TXlJI5.js";
2
+ /**
3
+ * Client companion to `authentication()`. The providers need no client
4
+ * methods of their own: sign in with better-auth's `signIn.social` and link
5
+ * with `linkSocial`. This adds the plugin's error codes to the client.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createAuthClient } from "better-auth/client";
10
+ * import { authenticationClient } from "@checodo/better-auth-authentication/client";
11
+ *
12
+ * export const authClient = createAuthClient({ plugins: [authenticationClient()] });
13
+ *
14
+ * await authClient.signIn.social({ provider: "epic", callbackURL: "/dashboard" });
15
+ * ```
16
+ */
17
+ export declare const authenticationClient: () => {
18
+ id: "authentication";
19
+ $InferServerPlugin: AuthenticationPlugin;
20
+ $ERROR_CODES: {
21
+ EMAIL_DOMAIN_RESERVED: import("better-auth").RawError<"EMAIL_DOMAIN_RESERVED">;
22
+ };
23
+ };
24
+ export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES };
25
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;;;;;;;qBAmBa;;EAGiB,oBAAA"}
package/dist/client.js ADDED
@@ -0,0 +1,27 @@
1
+ import { n as AUTHENTICATION_ERROR_CODES, t as AUTHENTICATION_CALLBACK_ERRORS } from "./error-codes-CVA2nmg9.js";
2
+ // src/client.ts
3
+ /**
4
+ * Client companion to `authentication()`. The providers need no client
5
+ * methods of their own: sign in with better-auth's `signIn.social` and link
6
+ * with `linkSocial`. This adds the plugin's error codes to the client.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createAuthClient } from "better-auth/client";
11
+ * import { authenticationClient } from "@checodo/better-auth-authentication/client";
12
+ *
13
+ * export const authClient = createAuthClient({ plugins: [authenticationClient()] });
14
+ *
15
+ * await authClient.signIn.social({ provider: "epic", callbackURL: "/dashboard" });
16
+ * ```
17
+ */
18
+ const authenticationClient = () => {
19
+ return {
20
+ id: "authentication",
21
+ $InferServerPlugin: {},
22
+ $ERROR_CODES: AUTHENTICATION_ERROR_CODES
23
+ };
24
+ };
25
+ export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES, authenticationClient };
26
+
27
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from \"better-auth/client\";\nimport { AUTHENTICATION_ERROR_CODES } from \"./error-codes\";\nimport type { AuthenticationPlugin } from \"./index\";\n\n/**\n * Client companion to `authentication()`. The providers need no client\n * methods of their own: sign in with better-auth's `signIn.social` and link\n * with `linkSocial`. This adds the plugin's error codes to the client.\n *\n * @example\n * ```ts\n * import { createAuthClient } from \"better-auth/client\";\n * import { authenticationClient } from \"@checodo/better-auth-authentication/client\";\n *\n * export const authClient = createAuthClient({ plugins: [authenticationClient()] });\n *\n * await authClient.signIn.social({ provider: \"epic\", callbackURL: \"/dashboard\" });\n * ```\n */\nexport const authenticationClient = () => {\n return {\n id: \"authentication\",\n $InferServerPlugin: {} as AuthenticationPlugin,\n $ERROR_CODES: AUTHENTICATION_ERROR_CODES,\n } satisfies BetterAuthClientPlugin;\n};\n\nexport { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES } from \"./error-codes\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,MAAa,6BAA6B;CACxC,OAAO;EACL,IAAI;EACJ,oBAAoB,CAAC;EACrB,cAAc;CAChB;AACF"}
@@ -0,0 +1,16 @@
1
+ import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
2
+ // src/error-codes.ts
3
+ const AUTHENTICATION_ERROR_CODES = defineErrorCodes({ EMAIL_DOMAIN_RESERVED: "This email domain is reserved for accounts from a sign-in provider and can't be used" });
4
+ /**
5
+ * `error` values in the query of the error page a failed sign-in redirects
6
+ * to (better-auth's `/error`, or `errorCallbackURL`), besides better-auth's own.
7
+ */
8
+ const AUTHENTICATION_CALLBACK_ERRORS = {
9
+ /** The player cancelled on Steam's sign-in page. */
10
+ STEAM_SIGN_IN_CANCELLED: "steam_sign_in_cancelled",
11
+ /** Steam's answer didn't check out, or Steam couldn't confirm it. */
12
+ STEAM_VERIFICATION_FAILED: "steam_verification_failed"
13
+ };
14
+ export { AUTHENTICATION_ERROR_CODES as n, AUTHENTICATION_CALLBACK_ERRORS as t };
15
+
16
+ //# sourceMappingURL=error-codes-CVA2nmg9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-codes-CVA2nmg9.js","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from \"@better-auth/core/utils/error-codes\";\n\nexport const AUTHENTICATION_ERROR_CODES = defineErrorCodes({\n EMAIL_DOMAIN_RESERVED:\n \"This email domain is reserved for accounts from a sign-in provider and can't be used\",\n});\n\nexport type AuthenticationErrorCode = keyof typeof AUTHENTICATION_ERROR_CODES;\n\n/**\n * `error` values in the query of the error page a failed sign-in redirects\n * to (better-auth's `/error`, or `errorCallbackURL`), besides better-auth's own.\n */\nexport const AUTHENTICATION_CALLBACK_ERRORS = {\n /** The player cancelled on Steam's sign-in page. */\n STEAM_SIGN_IN_CANCELLED: \"steam_sign_in_cancelled\",\n /** Steam's answer didn't check out, or Steam couldn't confirm it. */\n STEAM_VERIFICATION_FAILED: \"steam_verification_failed\",\n} as const;\n"],"mappings":";;AAEA,MAAa,6BAA6B,iBAAiB,EACzD,uBACE,uFACJ,CAAC;;;;;AAQD,MAAa,iCAAiC;;CAE5C,yBAAyB;;CAEzB,2BAA2B;AAC7B"}
@@ -0,0 +1,327 @@
1
+ import { OAuthMappedUser, ProviderOptions } from "@better-auth/core/oauth2";
2
+ /** Like better-auth's `mapProfileToUser`: changes the user created from a profile. */
3
+ type ProfileMapper<Profile extends object> = (profile: Profile) => OAuthMappedUser | Promise<OAuthMappedUser>;
4
+ /**
5
+ * What every provider accepts, like better-auth's built-in providers:
6
+ * sign-up control and profile mapping.
7
+ */
8
+ interface CommonProviderOptions<Profile extends object> {
9
+ /** Only existing users can sign in; new users get an error. */
10
+ disableSignUp?: boolean | undefined;
11
+ /** New users must sign in with `requestSignUp: true`. */
12
+ disableImplicitSignUp?: boolean | undefined;
13
+ /** Update the user's name and image from the provider on every sign-in. */
14
+ overrideUserInfoOnSignIn?: boolean | undefined;
15
+ /** Change the user created from the provider's profile. */
16
+ mapProfileToUser?: ProfileMapper<Profile> | undefined;
17
+ /**
18
+ * Domain of the placeholder email for users the provider gives no email
19
+ * (`<account id>@<domain>`). Defaults to `<provider id>.invalid`, a domain
20
+ * that can never receive email. Sign-ups with this domain are refused
21
+ * everywhere else, so nobody can claim a player's placeholder.
22
+ */
23
+ emailDomain?: string | undefined;
24
+ }
25
+ /** OAuth options shared by Epic and Xbox, like `github({ ... })`. */
26
+ type OAuthClientOptions = Pick<ProviderOptions, "clientSecret" | "scope" | "disableDefaultScope" | "redirectURI" | "prompt"> & {
27
+ clientId: string;
28
+ clientSecret: string;
29
+ };
30
+ /** The Steam player, from the Steam Web API's `GetPlayerSummaries`. */
31
+ interface SteamProfile {
32
+ /** 64-bit SteamID, as a string. */
33
+ steamid: string;
34
+ personaname?: string | undefined;
35
+ profileurl?: string | undefined;
36
+ avatar?: string | undefined;
37
+ avatarmedium?: string | undefined;
38
+ avatarfull?: string | undefined;
39
+ /** Only for public profiles. */
40
+ realname?: string | undefined;
41
+ /** Only for public profiles. */
42
+ loccountrycode?: string | undefined;
43
+ [key: string]: unknown;
44
+ }
45
+ interface SteamOptions extends CommonProviderOptions<SteamProfile> {
46
+ /**
47
+ * A Steam Web API key (https://steamcommunity.com/dev/apikey), to load
48
+ * the player's name and avatar. Without it, users are named after their
49
+ * SteamID. Never logged.
50
+ */
51
+ apiKey?: string | undefined;
52
+ /**
53
+ * Web API host. Defaults to `https://api.steampowered.com`; publisher keys
54
+ * use `https://partner.steam-api.com`.
55
+ */
56
+ apiBaseURL?: string | undefined;
57
+ }
58
+ /** The Epic Games account, from the ID token. */
59
+ interface EpicProfile {
60
+ /** The Epic account id. */
61
+ sub: string;
62
+ /** The display name. */
63
+ dn?: string | undefined;
64
+ [key: string]: unknown;
65
+ }
66
+ type EpicOptions = OAuthClientOptions & CommonProviderOptions<EpicProfile> & {
67
+ /**
68
+ * Send a PKCE challenge. Epic doesn't document PKCE for web sign-in, so
69
+ * it is off by default.
70
+ */
71
+ pkce?: boolean | undefined;
72
+ };
73
+ /** The Xbox player, from the XSTS token's display claims. */
74
+ interface XboxProfile {
75
+ /** The Xbox user id (XUID). */
76
+ xid: string;
77
+ /** The gamertag. */
78
+ gtg?: string | undefined;
79
+ /** User hash. */
80
+ uhs?: string | undefined;
81
+ [key: string]: unknown;
82
+ }
83
+ type XboxOptions = Omit<OAuthClientOptions, "scope" | "disableDefaultScope"> & CommonProviderOptions<XboxProfile> & {
84
+ /**
85
+ * The XSTS relying party. Defaults to `http://xboxlive.com`, which
86
+ * returns the XUID and gamertag; Xbox partners can use their own.
87
+ */
88
+ relyingParty?: string | undefined;
89
+ /** XSTS sandbox. Defaults to `RETAIL`. */
90
+ sandboxId?: string | undefined;
91
+ /** Request `xboxlive.offline_access` for a refresh token. Defaults to `false`. */
92
+ offlineAccess?: boolean | undefined;
93
+ };
94
+ interface AuthenticationOptions {
95
+ /** Sign in with Steam (OpenID 2.0). */
96
+ steam?: SteamOptions | undefined;
97
+ /** Sign in with Epic Games (Epic Account Services, OpenID Connect). */
98
+ epic?: EpicOptions | undefined;
99
+ /** Sign in with Xbox (Microsoft account, then Xbox Live). Needs Xbox developer access. */
100
+ xbox?: XboxOptions | undefined;
101
+ }
102
+ /** The provider ids this plugin adds. */
103
+ type AuthenticationProviderId = "steam" | "epic" | "xbox";
104
+ declare const AUTHENTICATION_ERROR_CODES: {
105
+ EMAIL_DOMAIN_RESERVED: import("better-auth").RawError<"EMAIL_DOMAIN_RESERVED">;
106
+ };
107
+ type AuthenticationErrorCode = keyof typeof AUTHENTICATION_ERROR_CODES;
108
+ /**
109
+ * `error` values in the query of the error page a failed sign-in redirects
110
+ * to (better-auth's `/error`, or `errorCallbackURL`), besides better-auth's own.
111
+ */
112
+ declare const AUTHENTICATION_CALLBACK_ERRORS: {
113
+ /** The player cancelled on Steam's sign-in page. */
114
+ readonly STEAM_SIGN_IN_CANCELLED: "steam_sign_in_cancelled";
115
+ /** Steam's answer didn't check out, or Steam couldn't confirm it. */
116
+ readonly STEAM_VERIFICATION_FAILED: "steam_verification_failed";
117
+ };
118
+ /**
119
+ * Adds Steam, Epic Games, and Xbox as social providers. They are used like
120
+ * better-auth's built-in ones: `signIn.social({ provider: "steam" })`,
121
+ * `linkSocial`, callbacks at `/callback/<provider>`, account linking,
122
+ * `callbackURL` checks, and sign-up options all work the same way.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { betterAuth } from "better-auth";
127
+ * import { authentication } from "@checodo/better-auth-authentication";
128
+ *
129
+ * export const auth = betterAuth({
130
+ * plugins: [
131
+ * authentication({
132
+ * steam: { apiKey: process.env.STEAM_API_KEY },
133
+ * epic: { clientId: "...", clientSecret: "..." },
134
+ * }),
135
+ * ],
136
+ * });
137
+ *
138
+ * // client
139
+ * await authClient.signIn.social({ provider: "steam", callbackURL: "/dashboard" });
140
+ * ```
141
+ */
142
+ declare const authentication: (options: AuthenticationOptions) => {
143
+ id: "authentication";
144
+ version: string;
145
+ init(ctx: import("better-auth").AuthContext): {
146
+ context: {
147
+ socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
148
+ };
149
+ options: {
150
+ databaseHooks: {
151
+ user?: {
152
+ create?: {
153
+ before?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
154
+ data: {
155
+ id?: string | undefined;
156
+ createdAt?: Date | undefined;
157
+ updatedAt?: Date | undefined;
158
+ email?: string | undefined;
159
+ emailVerified?: boolean | undefined;
160
+ name?: string | undefined;
161
+ image?: string | null | undefined;
162
+ } & Record<string, any>;
163
+ }>;
164
+ after?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
165
+ };
166
+ update?: {
167
+ before?: (user: Partial<import("better-auth").User> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
168
+ data: {
169
+ [x: string]: any;
170
+ id?: string | undefined;
171
+ createdAt?: Date | undefined;
172
+ updatedAt?: Date | undefined;
173
+ email?: string | undefined;
174
+ emailVerified?: boolean | undefined;
175
+ name?: string | undefined;
176
+ image?: string | null | undefined;
177
+ };
178
+ }>;
179
+ after?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
180
+ };
181
+ delete?: {
182
+ before?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
183
+ after?: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
184
+ };
185
+ };
186
+ session?: {
187
+ create?: {
188
+ before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
189
+ data: {
190
+ id?: string | undefined;
191
+ createdAt?: Date | undefined;
192
+ updatedAt?: Date | undefined;
193
+ userId?: string | undefined;
194
+ expiresAt?: Date | undefined;
195
+ token?: string | undefined;
196
+ ipAddress?: string | null | undefined;
197
+ userAgent?: string | null | undefined;
198
+ } & Record<string, any>;
199
+ }>;
200
+ after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
201
+ };
202
+ update?: {
203
+ before?: (session: Partial<import("better-auth").Session> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
204
+ data: {
205
+ [x: string]: any;
206
+ id?: string | undefined;
207
+ createdAt?: Date | undefined;
208
+ updatedAt?: Date | undefined;
209
+ userId?: string | undefined;
210
+ expiresAt?: Date | undefined;
211
+ token?: string | undefined;
212
+ ipAddress?: string | null | undefined;
213
+ userAgent?: string | null | undefined;
214
+ };
215
+ }>;
216
+ after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
217
+ };
218
+ delete?: {
219
+ before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
220
+ after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
221
+ };
222
+ };
223
+ account?: {
224
+ create?: {
225
+ before?: (account: import("better-auth").Account, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
226
+ data: {
227
+ id?: string | undefined;
228
+ createdAt?: Date | undefined;
229
+ updatedAt?: Date | undefined;
230
+ providerId?: string | undefined;
231
+ accountId?: string | undefined;
232
+ userId?: string | undefined;
233
+ accessToken?: string | null | undefined;
234
+ refreshToken?: string | null | undefined;
235
+ idToken?: string | null | undefined;
236
+ accessTokenExpiresAt?: Date | null | undefined;
237
+ refreshTokenExpiresAt?: Date | null | undefined;
238
+ scope?: string | null | undefined;
239
+ password?: string | null | undefined;
240
+ } & Record<string, any>;
241
+ }>;
242
+ after?: (account: import("better-auth").Account, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
243
+ };
244
+ update?: {
245
+ before?: (account: Partial<import("better-auth").Account> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
246
+ data: {
247
+ [x: string]: any;
248
+ id?: string | undefined;
249
+ createdAt?: Date | undefined;
250
+ updatedAt?: Date | undefined;
251
+ providerId?: string | undefined;
252
+ accountId?: string | undefined;
253
+ userId?: string | undefined;
254
+ accessToken?: string | null | undefined;
255
+ refreshToken?: string | null | undefined;
256
+ idToken?: string | null | undefined;
257
+ accessTokenExpiresAt?: Date | null | undefined;
258
+ refreshTokenExpiresAt?: Date | null | undefined;
259
+ scope?: string | null | undefined;
260
+ password?: string | null | undefined;
261
+ };
262
+ }>;
263
+ after?: (account: import("better-auth").Account & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
264
+ };
265
+ delete?: {
266
+ before?: (account: import("better-auth").Account & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
267
+ after?: (account: import("better-auth").Account & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
268
+ };
269
+ };
270
+ verification?: {
271
+ create?: {
272
+ before?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
273
+ data: {
274
+ id?: string | undefined;
275
+ createdAt?: Date | undefined;
276
+ updatedAt?: Date | undefined;
277
+ value?: string | undefined;
278
+ expiresAt?: Date | undefined;
279
+ identifier?: string | undefined;
280
+ } & Record<string, any>;
281
+ }>;
282
+ after?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
283
+ };
284
+ update?: {
285
+ before?: (verification: Partial<import("better-auth").Verification> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
286
+ data: {
287
+ [x: string]: any;
288
+ id?: string | undefined;
289
+ createdAt?: Date | undefined;
290
+ updatedAt?: Date | undefined;
291
+ value?: string | undefined;
292
+ expiresAt?: Date | undefined;
293
+ identifier?: string | undefined;
294
+ };
295
+ }>;
296
+ after?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
297
+ };
298
+ delete?: {
299
+ before?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
300
+ after?: (verification: import("better-auth").Verification & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
301
+ };
302
+ };
303
+ } | undefined;
304
+ };
305
+ };
306
+ hooks: {
307
+ before: {
308
+ matcher: (context: import("better-auth").HookEndpointContext) => boolean;
309
+ handler: import("better-auth/api").AuthMiddleware;
310
+ }[];
311
+ };
312
+ $ERROR_CODES: {
313
+ EMAIL_DOMAIN_RESERVED: import("better-auth").RawError<"EMAIL_DOMAIN_RESERVED">;
314
+ };
315
+ options: AuthenticationOptions;
316
+ };
317
+ type AuthenticationPlugin = ReturnType<typeof authentication>;
318
+ /** Lets `auth.$context.getPlugin("authentication")` infer this plugin's exact type. */
319
+ declare module "@better-auth/core" {
320
+ interface BetterAuthPluginRegistry<AuthOptions, Options> {
321
+ authentication: {
322
+ creator: typeof authentication;
323
+ };
324
+ }
325
+ }
326
+ export { AuthenticationErrorCode as a, CommonProviderOptions as c, SteamOptions as d, SteamProfile as f, AUTHENTICATION_ERROR_CODES as i, EpicOptions as l, XboxProfile as m, authentication as n, AuthenticationOptions as o, XboxOptions as p, AUTHENTICATION_CALLBACK_ERRORS as r, AuthenticationProviderId as s, AuthenticationPlugin as t, EpicProfile as u };
327
+ //# sourceMappingURL=index-B-TXlJI5.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-B-TXlJI5.d.ts","names":[],"sources":["../src/types.ts","../src/error-codes.ts","../src/index.ts"],"mappings":";;KAGY,cAAc,2BACxB,SAAS,YACN,kBAAkB,QAAQ;;;;;UAMd,sBAAsB;;EAErC;;EAEA;;EAEA;;EAEA,mBAAmB,cAAc;;;;;;;EAOjC;;;KAIU,qBAAqB,KAC/B;EAGA;EACA;;;UAIe;;EAEf;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;GACC;;UAGc,qBAAqB,sBAAsB;;;;;;EAM1D;;;;;EAKA;;;UAIe;;EAEf;;EAEA;GACC;;KAGS,cAAc,qBACxB,sBAAsB;;;;;EAKpB;;;UAIa;;EAEf;;EAEA;;EAEA;GACC;;KAGS,cAAc,KAAK,uDAC7B,sBAAsB;;;;;EAKpB;;EAEA;;EAEA;;UAGa;;EAEf,QAAQ;;EAER,OAAO;;EAEP,OAAO;;;KAIG;cCtHC;;;KAKD,uCAAuC;;;;;cAMtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCmCA,iBAAkB,SAAS;;;;;;;;;;UA0Cs7uC;YAA6P,UAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;YAA8Q,SAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;UAAwH;YAA6P,UAAC,MAAA,8BAAA,QAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;YAAuR,SAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;UAAwH;YAAwK,UAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;YAA0M,SAAC,4BAAA,OAAA,yBAAA,+BAAA,kCAAA;;;;UAAmL;YAAmQ,UAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;YAA0R,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;UAAuK;YAAgQ,UAAC,SAAA,8BAAA,WAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;;YAAmS,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;UAA8H;YAA8K,UAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;YAAmN,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;;;UAAyL;YAAmQ,UAAC,+BAAA,SAAA,+BAAA,kCAAA;;;;;;;;;;;;;;;;;YAAgQ,SAAC,+BAAA,SAAA,+BAAA,kCAAA;;UAA6I;YAA+P,UAAC,SAAA,8BAAA,WAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;;;;;;;YAAmS,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;UAA8H;YAA+K,UAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;YAAoN,SAAC,+BAAA,UAAA,yBAAA,+BAAA,kCAAA;;;;UAAmM;YAA6Q,UAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;YAA8S,SAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;UAAwI;YAA6Q,UAAC,cAAA,8BAAA,gBAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;YAAuT,SAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;UAAwI;YAAwL,UAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;YAAkO,SAAC,oCAAA,eAAA,yBAAA,+BAAA,kCAAA;;;;;;;;;;;;;;;;;KAV519C,uBAAuB,kBAAkB;;;YAIzC,yBAAyB,aAAa;IAC9C;MACE,gBAAgB"}
@@ -0,0 +1,2 @@
1
+ import { a as AuthenticationErrorCode, c as CommonProviderOptions, d as SteamOptions, f as SteamProfile, i as AUTHENTICATION_ERROR_CODES, l as EpicOptions, m as XboxProfile, n as authentication, o as AuthenticationOptions, p as XboxOptions, r as AUTHENTICATION_CALLBACK_ERRORS, s as AuthenticationProviderId, t as AuthenticationPlugin, u as EpicProfile } from "./index-B-TXlJI5.js";
2
+ export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES, type AuthenticationErrorCode, type AuthenticationOptions, AuthenticationPlugin, type AuthenticationProviderId, type CommonProviderOptions, type EpicOptions, type EpicProfile, type SteamOptions, type SteamProfile, type XboxOptions, type XboxProfile, authentication };
package/dist/index.js ADDED
@@ -0,0 +1,578 @@
1
+ import { n as AUTHENTICATION_ERROR_CODES, t as AUTHENTICATION_CALLBACK_ERRORS } from "./error-codes-CVA2nmg9.js";
2
+ import { getCurrentAdapter, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
3
+ import { APIError, createAuthMiddleware } from "better-auth/api";
4
+ import { createAuthorizationURL, refreshAccessToken, validateAuthorizationCode, validateToken } from "@better-auth/core/oauth2";
5
+ import { generateRandomString } from "better-auth/crypto";
6
+ // package.json
7
+ var version = "0.1.0";
8
+ // src/utils/email.ts
9
+ /** Hostname labels: letters, digits, hyphens; at least two labels. */
10
+ const DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,63}$/;
11
+ function isValidEmailDomain(domain) {
12
+ return DOMAIN.test(domain);
13
+ }
14
+ /**
15
+ * A placeholder email for a user the provider gives no email:
16
+ * `<account id>@<domain>`. Unique per account, and on a domain that can't
17
+ * receive email (`.invalid` by default).
18
+ */
19
+ function placeholderEmail(accountId, domain) {
20
+ return `${accountId.toLowerCase()}@${domain}`;
21
+ }
22
+ function hasDomain(email, domain) {
23
+ return email.toLowerCase().endsWith(`@${domain}`);
24
+ }
25
+ // src/hooks/reserved-email.ts
26
+ /**
27
+ * Placeholder emails (`<account id>@steam.invalid`) belong to the provider
28
+ * they came from:
29
+ *
30
+ * - If anyone could sign up with one, they could take a player's
31
+ * placeholder first and block (or, with careless account linking, take
32
+ * over) that player's sign-in. So users with these domains can only be
33
+ * created or changed by that provider's own callback, or by server code
34
+ * outside a request.
35
+ * - With `overrideUserInfoOnSignIn`, better-auth writes the provider's
36
+ * email back on every sign-in. For a user with a real email (who linked
37
+ * the provider account, or changed their email later) that would replace
38
+ * it with the placeholder; the email change is dropped instead.
39
+ */
40
+ function createReservedEmailHooks(definitions) {
41
+ /** The provider whose placeholder `email` is, if the request is that provider's callback; throws otherwise. */
42
+ const ownerOf = (email, context) => {
43
+ if (typeof email !== "string" || !context) return null;
44
+ const owner = definitions.find((definition) => hasDomain(email, definition.emailDomain));
45
+ if (!owner) return null;
46
+ const params = context.params;
47
+ if (context.path !== "/callback/:id" || params?.id !== owner.provider.id) throw APIError.from("BAD_REQUEST", AUTHENTICATION_ERROR_CODES.EMAIL_DOMAIN_RESERVED);
48
+ return owner;
49
+ };
50
+ return { user: {
51
+ create: { before: async (user, context) => {
52
+ ownerOf(user.email, context);
53
+ } },
54
+ update: { before: async (data, context) => {
55
+ if (!ownerOf(data.email, context) || !context) return;
56
+ if (!await (await getCurrentAdapter(context.context.adapter)).findOne({
57
+ model: "user",
58
+ where: [{
59
+ field: "email",
60
+ value: data.email.toLowerCase()
61
+ }]
62
+ })) return { data: {
63
+ email: void 0,
64
+ emailVerified: void 0
65
+ } };
66
+ } }
67
+ } };
68
+ }
69
+ // ../plugin-utils/src/options.ts
70
+ /**
71
+ * Returns a function that throws an option error prefixed with the plugin
72
+ * id, `[referral] "codeLength" must be ...`, for validating options at
73
+ * plugin creation.
74
+ */
75
+ function createFail(pluginId) {
76
+ return (message) => {
77
+ throw new Error(`[${pluginId}] ${message}`);
78
+ };
79
+ }
80
+ // src/providers/epic.ts
81
+ /**
82
+ * From Epic's discovery document,
83
+ * https://api.epicgames.dev/epic/oauth/v1/.well-known/openid-configuration
84
+ */
85
+ const EPIC_ISSUER = "https://api.epicgames.dev/epic/oauth/v1";
86
+ const EPIC_AUTHORIZATION_ENDPOINT = "https://www.epicgames.com/id/authorize";
87
+ const EPIC_TOKEN_ENDPOINT = "https://api.epicgames.dev/epic/oauth/v1/token";
88
+ const EPIC_JWKS_ENDPOINT = "https://api.epicgames.dev/epic/oauth/v1/.well-known/jwks.json";
89
+ /** `basic_profile` is required by Epic Account Services; `openid` returns an ID token. */
90
+ const DEFAULT_SCOPES = ["basic_profile", "openid"];
91
+ /**
92
+ * Epic Games (Epic Account Services) over OpenID Connect. The player is
93
+ * identified by the ID token, verified against Epic's keys, issuer, and
94
+ * this client id. Epic shares no email address.
95
+ */
96
+ function epic(options, emailDomain) {
97
+ return {
98
+ provider: {
99
+ id: "epic",
100
+ name: "Epic Games",
101
+ issuer: EPIC_ISSUER,
102
+ accountSubject: ({ profile }) => profile.sub,
103
+ createAuthorizationURL({ state, scopes, codeVerifier, redirectURI, additionalParams }) {
104
+ const allScopes = options.disableDefaultScope ? [] : [...DEFAULT_SCOPES];
105
+ if (options.scope) allScopes.push(...options.scope);
106
+ if (scopes) allScopes.push(...scopes);
107
+ return createAuthorizationURL({
108
+ id: "epic",
109
+ options,
110
+ authorizationEndpoint: EPIC_AUTHORIZATION_ENDPOINT,
111
+ scopes: [...new Set(allScopes)],
112
+ state,
113
+ codeVerifier: options.pkce ? codeVerifier : void 0,
114
+ redirectURI,
115
+ prompt: options.prompt,
116
+ additionalParams
117
+ });
118
+ },
119
+ validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
120
+ return validateAuthorizationCode({
121
+ code,
122
+ codeVerifier: options.pkce ? codeVerifier : void 0,
123
+ redirectURI,
124
+ options,
125
+ tokenEndpoint: EPIC_TOKEN_ENDPOINT,
126
+ authentication: "basic"
127
+ });
128
+ },
129
+ refreshAccessToken: (refreshToken) => refreshAccessToken({
130
+ refreshToken,
131
+ options: {
132
+ clientId: options.clientId,
133
+ clientSecret: options.clientSecret
134
+ },
135
+ tokenEndpoint: EPIC_TOKEN_ENDPOINT,
136
+ authentication: "basic"
137
+ }),
138
+ async getUserInfo(tokens) {
139
+ if (!tokens.idToken) {
140
+ tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] Epic returned no ID token; keep the \"openid\" scope");
141
+ return null;
142
+ }
143
+ let payload;
144
+ try {
145
+ ({payload} = await validateToken(tokens.idToken, EPIC_JWKS_ENDPOINT, {
146
+ issuer: EPIC_ISSUER,
147
+ audience: options.clientId,
148
+ requiredClaims: ["sub"]
149
+ }));
150
+ } catch (error) {
151
+ tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] Epic's ID token didn't verify", error);
152
+ return null;
153
+ }
154
+ const mapped = await options.mapProfileToUser?.(payload);
155
+ return {
156
+ user: {
157
+ name: payload.dn || `Epic ${payload.sub}`,
158
+ email: placeholderEmail(payload.sub, emailDomain),
159
+ emailVerified: false,
160
+ ...mapped
161
+ },
162
+ data: payload
163
+ };
164
+ },
165
+ disableSignUp: options.disableSignUp,
166
+ disableImplicitSignUp: options.disableImplicitSignUp,
167
+ options: {
168
+ clientId: options.clientId,
169
+ clientSecret: options.clientSecret,
170
+ redirectURI: options.redirectURI,
171
+ disableSignUp: options.disableSignUp,
172
+ disableImplicitSignUp: options.disableImplicitSignUp,
173
+ overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn
174
+ }
175
+ },
176
+ emailDomain
177
+ };
178
+ }
179
+ // src/providers/steam.ts
180
+ const STEAM_OPENID_ENDPOINT = "https://steamcommunity.com/openid/login";
181
+ const OPENID_NS = "http://specs.openid.net/auth/2.0";
182
+ const IDENTIFIER_SELECT = "http://specs.openid.net/auth/2.0/identifier_select";
183
+ const CLAIMED_ID = /^https:\/\/steamcommunity\.com\/openid\/id\/(\d{17})$/;
184
+ /** OpenID 2.0 requires these to be signed in a positive assertion. */
185
+ const REQUIRED_SIGNED = [
186
+ "op_endpoint",
187
+ "return_to",
188
+ "response_nonce",
189
+ "assoc_handle",
190
+ "claimed_id",
191
+ "identity"
192
+ ];
193
+ /** How long the one-time code from the callback hook is valid. */
194
+ const CODE_TTL_MS = 6e4;
195
+ const STEAM_TIMEOUT_MS = 1e4;
196
+ const codeIdentifier = (code) => `steam-openid:${code}`;
197
+ /** Where Steam sends the player back: better-auth's own callback, with the OAuth state. */
198
+ function steamReturnTo(redirectURI, state) {
199
+ const url = new URL(redirectURI);
200
+ url.searchParams.set("state", state);
201
+ return url.toString();
202
+ }
203
+ /** Steam's sign-in page for an OpenID 2.0 `checkid_setup` request. */
204
+ function steamAuthorizationURL(returnTo) {
205
+ const url = new URL(STEAM_OPENID_ENDPOINT);
206
+ url.searchParams.set("openid.ns", OPENID_NS);
207
+ url.searchParams.set("openid.mode", "checkid_setup");
208
+ url.searchParams.set("openid.return_to", returnTo);
209
+ url.searchParams.set("openid.realm", new URL(returnTo).origin);
210
+ url.searchParams.set("openid.identity", IDENTIFIER_SELECT);
211
+ url.searchParams.set("openid.claimed_id", IDENTIFIER_SELECT);
212
+ return url;
213
+ }
214
+ /** Parses OpenID's key-value form (`key:value` lines). */
215
+ function parseKeyValue(text) {
216
+ const result = /* @__PURE__ */ new Map();
217
+ for (const line of text.split("\n")) {
218
+ const separator = line.indexOf(":");
219
+ if (separator > 0) result.set(line.slice(0, separator), line.slice(separator + 1).trim());
220
+ }
221
+ return result;
222
+ }
223
+ /**
224
+ * Checks Steam's positive assertion and returns the SteamID64, or `null`.
225
+ * Everything in the query is untrusted until Steam confirms it: the
226
+ * fields are checked against what this request expects, then sent back to
227
+ * Steam (`check_authentication`), which verifies the signature and that
228
+ * the nonce wasn't used before.
229
+ */
230
+ async function verifySteamAssertion(query, expectedReturnTo) {
231
+ const get = (key) => {
232
+ const value = query[`openid.${key}`];
233
+ return typeof value === "string" ? value : void 0;
234
+ };
235
+ if (get("ns") !== OPENID_NS || get("mode") !== "id_res") return null;
236
+ if (get("op_endpoint") !== "https://steamcommunity.com/openid/login") return null;
237
+ if (get("return_to") !== expectedReturnTo) return null;
238
+ const claimedId = get("claimed_id");
239
+ const steamId = claimedId ? CLAIMED_ID.exec(claimedId)?.[1] : void 0;
240
+ if (!steamId || get("identity") !== claimedId) return null;
241
+ const signed = new Set((get("signed") ?? "").split(","));
242
+ if (!REQUIRED_SIGNED.every((field) => signed.has(field)) || !get("sig")) return null;
243
+ const body = new URLSearchParams();
244
+ for (const [key, value] of Object.entries(query)) if (key.startsWith("openid.") && typeof value === "string") body.set(key, value);
245
+ body.set("openid.mode", "check_authentication");
246
+ const response = await fetch(STEAM_OPENID_ENDPOINT, {
247
+ method: "POST",
248
+ body,
249
+ redirect: "error",
250
+ signal: AbortSignal.timeout(STEAM_TIMEOUT_MS)
251
+ });
252
+ if (!response.ok) return null;
253
+ const answer = parseKeyValue(await response.text());
254
+ return answer.get("ns") === OPENID_NS && answer.get("is_valid") === "true" ? steamId : null;
255
+ }
256
+ /** Loads the player's public profile; `null` if it can't be loaded. */
257
+ async function fetchSteamProfile(steamId, apiKey, apiBaseURL) {
258
+ const url = new URL("/ISteamUser/GetPlayerSummaries/v2/", apiBaseURL);
259
+ url.searchParams.set("key", apiKey);
260
+ url.searchParams.set("steamids", steamId);
261
+ const response = await fetch(url, { signal: AbortSignal.timeout(STEAM_TIMEOUT_MS) });
262
+ if (!response.ok) return null;
263
+ return (await response.json()).response?.players?.find((p) => p.steamid === steamId) ?? null;
264
+ }
265
+ /**
266
+ * Steam uses OpenID 2.0, not OAuth: it sends the player back with a signed
267
+ * assertion instead of a code. Steam's `return_to` is better-auth's own
268
+ * `/callback/steam`; a before-hook there verifies the assertion and swaps
269
+ * it for a one-time `code`, which `validateAuthorizationCode` redeems. The
270
+ * rest is better-auth's callback, like for any provider: state and CSRF
271
+ * check, user and account, session, redirect.
272
+ */
273
+ function steam(options, emailDomain) {
274
+ const apiBaseURL = options.apiBaseURL ?? "https://api.steampowered.com";
275
+ return {
276
+ provider: {
277
+ id: "steam",
278
+ name: "Steam",
279
+ accountSubject: ({ profile }) => profile.steamid,
280
+ createAuthorizationURL({ state, redirectURI }) {
281
+ return steamAuthorizationURL(steamReturnTo(redirectURI, state));
282
+ },
283
+ async validateAuthorizationCode({ code }) {
284
+ const ctx = tryGetCurrentAuthEndpointContext();
285
+ if (!ctx) return null;
286
+ const verified = await ctx.context.internalAdapter.consumeVerificationValue(codeIdentifier(code));
287
+ if (!verified || new Date(verified.expiresAt).getTime() < Date.now()) return null;
288
+ return {
289
+ tokenType: "steam-openid",
290
+ raw: { steamid: verified.value }
291
+ };
292
+ },
293
+ async getUserInfo(tokens) {
294
+ const steamid = tokens.raw?.steamid;
295
+ if (typeof steamid !== "string") return null;
296
+ let profile = { steamid };
297
+ if (options.apiKey) try {
298
+ profile = await fetchSteamProfile(steamid, options.apiKey, apiBaseURL) ?? profile;
299
+ } catch {
300
+ tryGetCurrentAuthEndpointContext()?.context.logger.warn("[authentication] could not load the Steam profile");
301
+ }
302
+ const mapped = await options.mapProfileToUser?.(profile);
303
+ return {
304
+ user: {
305
+ name: profile.personaname || `Steam ${steamid}`,
306
+ email: placeholderEmail(steamid, emailDomain),
307
+ emailVerified: false,
308
+ image: profile.avatarfull,
309
+ ...mapped
310
+ },
311
+ data: profile
312
+ };
313
+ },
314
+ disableSignUp: options.disableSignUp,
315
+ disableImplicitSignUp: options.disableImplicitSignUp,
316
+ options: {
317
+ disableSignUp: options.disableSignUp,
318
+ disableImplicitSignUp: options.disableImplicitSignUp,
319
+ overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn
320
+ }
321
+ },
322
+ emailDomain,
323
+ hooks: { before: [{
324
+ matcher: (context) => context.path === "/callback/:id" && context.params?.id === "steam",
325
+ handler: createAuthMiddleware(async (ctx) => {
326
+ const query = ctx.query ?? {};
327
+ const state = typeof query.state === "string" ? query.state : void 0;
328
+ const fail = (error) => ({ context: { query: {
329
+ state,
330
+ code: "",
331
+ error
332
+ } } });
333
+ if (!state || ctx.method !== "GET") return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);
334
+ if (query["openid.mode"] === "cancel") return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_SIGN_IN_CANCELLED);
335
+ let steamId = null;
336
+ try {
337
+ steamId = await verifySteamAssertion(query, steamReturnTo(`${ctx.context.baseURL}/callback/steam`, state));
338
+ } catch (error) {
339
+ ctx.context.logger.error("[authentication] Steam verification failed", error);
340
+ }
341
+ if (!steamId) return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);
342
+ const code = generateRandomString(32);
343
+ await ctx.context.internalAdapter.createVerificationValue({
344
+ identifier: codeIdentifier(code),
345
+ value: steamId,
346
+ expiresAt: new Date(Date.now() + CODE_TTL_MS)
347
+ });
348
+ return { context: { query: {
349
+ state,
350
+ code
351
+ } } };
352
+ })
353
+ }] }
354
+ };
355
+ }
356
+ // src/providers/xbox.ts
357
+ /**
358
+ * Microsoft's documented flow for Xbox sign-in on websites ("Xbox services
359
+ * sign-in for title websites", Microsoft Game Development Kit): Microsoft
360
+ * account OAuth on the `consumers` tenant with `xboxlive.signin`, then an
361
+ * Xbox user token, then an XSTS token whose claims name the player.
362
+ */
363
+ const XBOX_AUTHORIZATION_ENDPOINT = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
364
+ const XBOX_TOKEN_ENDPOINT = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
365
+ const XBOX_USER_AUTH_ENDPOINT = "https://user.auth.xboxlive.com/user/authenticate";
366
+ const XBOX_XSTS_ENDPOINT = "https://xsts.auth.xboxlive.com/xsts/authorize";
367
+ const XBOX_TIMEOUT_MS = 1e4;
368
+ async function postXbox(url, body) {
369
+ const response = await fetch(url, {
370
+ method: "POST",
371
+ headers: {
372
+ "content-type": "application/json",
373
+ accept: "application/json",
374
+ "x-xbl-contract-version": "1"
375
+ },
376
+ body: JSON.stringify(body),
377
+ redirect: "error",
378
+ signal: AbortSignal.timeout(XBOX_TIMEOUT_MS)
379
+ });
380
+ if (!response.ok) {
381
+ const xErr = response.headers.get("x-err") ?? "none";
382
+ tryGetCurrentAuthEndpointContext()?.context.logger.error(`[authentication] Xbox token request to ${new URL(url).host} failed (${response.status}, XErr ${xErr})`);
383
+ return null;
384
+ }
385
+ try {
386
+ return await response.json();
387
+ } catch {
388
+ tryGetCurrentAuthEndpointContext()?.context.logger.error(`[authentication] Xbox token response from ${new URL(url).host} isn't JSON`);
389
+ return null;
390
+ }
391
+ }
392
+ /** Exchanges a Microsoft access token for the player's XSTS claims (XUID, gamertag). */
393
+ async function fetchXboxProfile(accessToken, { relyingParty, sandboxId }) {
394
+ const user = await postXbox(XBOX_USER_AUTH_ENDPOINT, {
395
+ RelyingParty: "http://auth.xboxlive.com",
396
+ TokenType: "JWT",
397
+ Properties: {
398
+ AuthMethod: "RPS",
399
+ SiteName: "user.auth.xboxlive.com",
400
+ RpsTicket: `d=${accessToken}`
401
+ }
402
+ });
403
+ if (!user?.Token) return null;
404
+ const xsts = await postXbox(XBOX_XSTS_ENDPOINT, {
405
+ RelyingParty: relyingParty,
406
+ TokenType: "JWT",
407
+ Properties: {
408
+ SandboxId: sandboxId,
409
+ UserTokens: [user.Token]
410
+ }
411
+ });
412
+ const claims = xsts?.DisplayClaims?.xui?.[0];
413
+ if (!claims?.xid) {
414
+ if (xsts) tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] the XSTS token has no XUID (xid claim); check the relying party");
415
+ return null;
416
+ }
417
+ return claims;
418
+ }
419
+ /**
420
+ * Xbox, for apps enrolled in the Xbox developer program (Microsoft only
421
+ * grants `xboxlive.signin` to them). The player is identified by the XUID
422
+ * from XSTS; Xbox shares no email address.
423
+ */
424
+ function xbox(options, emailDomain) {
425
+ const relyingParty = options.relyingParty ?? "http://xboxlive.com";
426
+ const sandboxId = options.sandboxId ?? "RETAIL";
427
+ const scopes = ["xboxlive.signin", ...options.offlineAccess ? ["xboxlive.offline_access"] : []];
428
+ return {
429
+ provider: {
430
+ id: "xbox",
431
+ name: "Xbox",
432
+ accountSubject: ({ profile }) => profile.xid,
433
+ createAuthorizationURL({ state, codeVerifier, redirectURI, additionalParams }) {
434
+ return createAuthorizationURL({
435
+ id: "xbox",
436
+ options,
437
+ authorizationEndpoint: XBOX_AUTHORIZATION_ENDPOINT,
438
+ scopes,
439
+ state,
440
+ codeVerifier,
441
+ redirectURI,
442
+ prompt: options.prompt,
443
+ additionalParams
444
+ });
445
+ },
446
+ validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
447
+ return validateAuthorizationCode({
448
+ code,
449
+ codeVerifier,
450
+ redirectURI,
451
+ options,
452
+ tokenEndpoint: XBOX_TOKEN_ENDPOINT,
453
+ authentication: "post"
454
+ });
455
+ },
456
+ refreshAccessToken: (refreshToken) => refreshAccessToken({
457
+ refreshToken,
458
+ options: {
459
+ clientId: options.clientId,
460
+ clientSecret: options.clientSecret
461
+ },
462
+ tokenEndpoint: XBOX_TOKEN_ENDPOINT,
463
+ authentication: "post"
464
+ }),
465
+ async getUserInfo(tokens) {
466
+ if (!tokens.accessToken) return null;
467
+ let profile = null;
468
+ try {
469
+ profile = await fetchXboxProfile(tokens.accessToken, {
470
+ relyingParty,
471
+ sandboxId
472
+ });
473
+ } catch (error) {
474
+ tryGetCurrentAuthEndpointContext()?.context.logger.error("[authentication] Xbox Live couldn't be reached", error);
475
+ }
476
+ if (!profile) return null;
477
+ const mapped = await options.mapProfileToUser?.(profile);
478
+ return {
479
+ user: {
480
+ name: profile.gtg || `Xbox ${profile.xid}`,
481
+ email: placeholderEmail(profile.xid, emailDomain),
482
+ emailVerified: false,
483
+ ...mapped
484
+ },
485
+ data: profile
486
+ };
487
+ },
488
+ disableSignUp: options.disableSignUp,
489
+ disableImplicitSignUp: options.disableImplicitSignUp,
490
+ options: {
491
+ clientId: options.clientId,
492
+ clientSecret: options.clientSecret,
493
+ redirectURI: options.redirectURI,
494
+ disableSignUp: options.disableSignUp,
495
+ disableImplicitSignUp: options.disableImplicitSignUp,
496
+ overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn
497
+ }
498
+ },
499
+ emailDomain
500
+ };
501
+ }
502
+ // src/options.ts
503
+ const fail = createFail("authentication");
504
+ function emailDomain(providerId, domain) {
505
+ const resolved = (domain ?? `${providerId}.invalid`).toLowerCase();
506
+ if (!isValidEmailDomain(resolved)) fail(`"${providerId}.emailDomain" must be a domain name, got ${JSON.stringify(domain)}.`);
507
+ return resolved;
508
+ }
509
+ function requireClient(providerId, options) {
510
+ if (typeof options.clientId !== "string" || options.clientId === "") fail(`"${providerId}.clientId" is required.`);
511
+ if (typeof options.clientSecret !== "string" || options.clientSecret === "") fail(`"${providerId}.clientSecret" is required.`);
512
+ }
513
+ /** Validates the options once, at creation, and builds the providers. */
514
+ function resolveProviders(options) {
515
+ const definitions = [];
516
+ if (options.steam) {
517
+ if (options.steam.apiKey !== void 0 && options.steam.apiKey.trim() === "") fail("\"steam.apiKey\" is empty; leave it out to sign in without loading profiles.");
518
+ definitions.push(steam(options.steam, emailDomain("steam", options.steam.emailDomain)));
519
+ }
520
+ if (options.epic) {
521
+ requireClient("epic", options.epic);
522
+ definitions.push(epic(options.epic, emailDomain("epic", options.epic.emailDomain)));
523
+ }
524
+ if (options.xbox) {
525
+ requireClient("xbox", options.xbox);
526
+ definitions.push(xbox(options.xbox, emailDomain("xbox", options.xbox.emailDomain)));
527
+ }
528
+ if (definitions.length === 0) fail("Configure at least one provider: steam, epic, or xbox.");
529
+ const domains = definitions.map((definition) => definition.emailDomain);
530
+ if (new Set(domains).size !== domains.length) fail("Each provider needs an email domain of its own.");
531
+ return definitions;
532
+ }
533
+ // src/index.ts
534
+ /**
535
+ * Adds Steam, Epic Games, and Xbox as social providers. They are used like
536
+ * better-auth's built-in ones: `signIn.social({ provider: "steam" })`,
537
+ * `linkSocial`, callbacks at `/callback/<provider>`, account linking,
538
+ * `callbackURL` checks, and sign-up options all work the same way.
539
+ *
540
+ * @example
541
+ * ```ts
542
+ * import { betterAuth } from "better-auth";
543
+ * import { authentication } from "@checodo/better-auth-authentication";
544
+ *
545
+ * export const auth = betterAuth({
546
+ * plugins: [
547
+ * authentication({
548
+ * steam: { apiKey: process.env.STEAM_API_KEY },
549
+ * epic: { clientId: "...", clientSecret: "..." },
550
+ * }),
551
+ * ],
552
+ * });
553
+ *
554
+ * // client
555
+ * await authClient.signIn.social({ provider: "steam", callbackURL: "/dashboard" });
556
+ * ```
557
+ */
558
+ const authentication = (options) => {
559
+ const definitions = resolveProviders(options);
560
+ return {
561
+ id: "authentication",
562
+ version,
563
+ init(ctx) {
564
+ const ids = new Set(definitions.map((definition) => definition.provider.id));
565
+ for (const existing of ctx.socialProviders) if (ids.has(existing.id)) ctx.logger.warn(`[authentication] the "${existing.id}" provider replaces a social provider with the same id`);
566
+ return {
567
+ context: { socialProviders: [...definitions.map((definition) => definition.provider), ...ctx.socialProviders.filter((provider) => !ids.has(provider.id))] },
568
+ options: { databaseHooks: createReservedEmailHooks(definitions) }
569
+ };
570
+ },
571
+ hooks: { before: definitions.flatMap((definition) => definition.hooks?.before ?? []) },
572
+ $ERROR_CODES: AUTHENTICATION_ERROR_CODES,
573
+ options
574
+ };
575
+ };
576
+ export { AUTHENTICATION_CALLBACK_ERRORS, AUTHENTICATION_ERROR_CODES, authentication };
577
+
578
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../package.json","../src/utils/email.ts","../src/hooks/reserved-email.ts","../../plugin-utils/src/options.ts","../src/providers/epic.ts","../src/providers/steam.ts","../src/providers/xbox.ts","../src/options.ts","../src/index.ts"],"sourcesContent":["","/** Hostname labels: letters, digits, hyphens; at least two labels. */\nconst DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9-]{2,63}$/;\n\nexport function isValidEmailDomain(domain: string): boolean {\n return DOMAIN.test(domain);\n}\n\n/**\n * A placeholder email for a user the provider gives no email:\n * `<account id>@<domain>`. Unique per account, and on a domain that can't\n * receive email (`.invalid` by default).\n */\nexport function placeholderEmail(accountId: string, domain: string): string {\n return `${accountId.toLowerCase()}@${domain}`;\n}\n\nexport function hasDomain(email: string, domain: string): boolean {\n return email.toLowerCase().endsWith(`@${domain}`);\n}\n","import type { BetterAuthOptions, GenericEndpointContext } from \"@better-auth/core\";\nimport { getCurrentAdapter } from \"@better-auth/core/context\";\nimport { APIError } from \"better-auth/api\";\nimport { AUTHENTICATION_ERROR_CODES } from \"../error-codes\";\nimport type { ProviderDefinition } from \"../providers/types\";\nimport { hasDomain } from \"../utils/email\";\n\ntype Context = GenericEndpointContext | null | undefined;\n\n/**\n * Placeholder emails (`<account id>@steam.invalid`) belong to the provider\n * they came from:\n *\n * - If anyone could sign up with one, they could take a player's\n * placeholder first and block (or, with careless account linking, take\n * over) that player's sign-in. So users with these domains can only be\n * created or changed by that provider's own callback, or by server code\n * outside a request.\n * - With `overrideUserInfoOnSignIn`, better-auth writes the provider's\n * email back on every sign-in. For a user with a real email (who linked\n * the provider account, or changed their email later) that would replace\n * it with the placeholder; the email change is dropped instead.\n */\nexport function createReservedEmailHooks(\n definitions: ProviderDefinition[],\n): BetterAuthOptions[\"databaseHooks\"] {\n /** The provider whose placeholder `email` is, if the request is that provider's callback; throws otherwise. */\n const ownerOf = (email: unknown, context: Context): ProviderDefinition | null => {\n if (typeof email !== \"string\" || !context) return null;\n const owner = definitions.find((definition) => hasDomain(email, definition.emailDomain));\n if (!owner) return null;\n const params = context.params as { id?: unknown } | undefined;\n if (context.path !== \"/callback/:id\" || params?.id !== owner.provider.id) {\n throw APIError.from(\"BAD_REQUEST\", AUTHENTICATION_ERROR_CODES.EMAIL_DOMAIN_RESERVED);\n }\n return owner;\n };\n\n return {\n user: {\n create: {\n before: async (user, context) => {\n ownerOf(user.email, context);\n },\n },\n update: {\n before: async (data, context) => {\n if (!ownerOf(data.email, context) || !context) return;\n // Emails are unique: if no user has the placeholder, this update\n // would replace another email with it.\n const adapter = await getCurrentAdapter(context.context.adapter);\n const holder = await adapter.findOne({\n model: \"user\",\n where: [{ field: \"email\", value: (data.email as string).toLowerCase() }],\n });\n if (!holder) return { data: { email: undefined, emailVerified: undefined } };\n },\n },\n },\n };\n}\n","/**\n * Returns a function that throws an option error prefixed with the plugin\n * id, `[referral] \"codeLength\" must be ...`, for validating options at\n * plugin creation.\n */\nexport function createFail(pluginId: string): (message: string) => never {\n return (message) => {\n throw new Error(`[${pluginId}] ${message}`);\n };\n}\n","import { tryGetCurrentAuthEndpointContext } from \"@better-auth/core/context\";\nimport type { OAuthProvider } from \"@better-auth/core/oauth2\";\nimport {\n createAuthorizationURL,\n refreshAccessToken,\n validateAuthorizationCode,\n validateToken,\n} from \"@better-auth/core/oauth2\";\nimport type { EpicOptions, EpicProfile } from \"../types\";\nimport { placeholderEmail } from \"../utils/email\";\nimport type { ProviderDefinition } from \"./types\";\n\n/**\n * From Epic's discovery document,\n * https://api.epicgames.dev/epic/oauth/v1/.well-known/openid-configuration\n */\nexport const EPIC_ISSUER = \"https://api.epicgames.dev/epic/oauth/v1\";\nexport const EPIC_AUTHORIZATION_ENDPOINT = \"https://www.epicgames.com/id/authorize\";\nexport const EPIC_TOKEN_ENDPOINT = \"https://api.epicgames.dev/epic/oauth/v1/token\";\nexport const EPIC_JWKS_ENDPOINT = \"https://api.epicgames.dev/epic/oauth/v1/.well-known/jwks.json\";\n/** `basic_profile` is required by Epic Account Services; `openid` returns an ID token. */\nconst DEFAULT_SCOPES = [\"basic_profile\", \"openid\"];\n\n/**\n * Epic Games (Epic Account Services) over OpenID Connect. The player is\n * identified by the ID token, verified against Epic's keys, issuer, and\n * this client id. Epic shares no email address.\n */\nexport function epic(options: EpicOptions, emailDomain: string): ProviderDefinition {\n const provider: OAuthProvider<EpicProfile> = {\n id: \"epic\",\n name: \"Epic Games\",\n issuer: EPIC_ISSUER,\n accountSubject: ({ profile }) => profile.sub,\n createAuthorizationURL({ state, scopes, codeVerifier, redirectURI, additionalParams }) {\n const allScopes = options.disableDefaultScope ? [] : [...DEFAULT_SCOPES];\n if (options.scope) allScopes.push(...options.scope);\n if (scopes) allScopes.push(...scopes);\n return createAuthorizationURL({\n id: \"epic\",\n options,\n authorizationEndpoint: EPIC_AUTHORIZATION_ENDPOINT,\n scopes: [...new Set(allScopes)],\n state,\n codeVerifier: options.pkce ? codeVerifier : undefined,\n redirectURI,\n prompt: options.prompt,\n additionalParams,\n });\n },\n validateAuthorizationCode({ code, codeVerifier, redirectURI }) {\n return validateAuthorizationCode({\n code,\n codeVerifier: options.pkce ? codeVerifier : undefined,\n redirectURI,\n options,\n tokenEndpoint: EPIC_TOKEN_ENDPOINT,\n authentication: \"basic\",\n });\n },\n refreshAccessToken: (refreshToken) =>\n refreshAccessToken({\n refreshToken,\n options: { clientId: options.clientId, clientSecret: options.clientSecret },\n tokenEndpoint: EPIC_TOKEN_ENDPOINT,\n authentication: \"basic\",\n }),\n async getUserInfo(tokens) {\n if (!tokens.idToken) {\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n '[authentication] Epic returned no ID token; keep the \"openid\" scope',\n );\n return null;\n }\n let payload: EpicProfile;\n try {\n ({ payload } = await validateToken<EpicProfile>(tokens.idToken, EPIC_JWKS_ENDPOINT, {\n issuer: EPIC_ISSUER,\n audience: options.clientId,\n requiredClaims: [\"sub\"],\n }));\n } catch (error) {\n // `null` fails the sign-in with better-auth's error redirect, not a 500.\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n \"[authentication] Epic's ID token didn't verify\",\n error,\n );\n return null;\n }\n const mapped = await options.mapProfileToUser?.(payload);\n return {\n user: {\n name: payload.dn || `Epic ${payload.sub}`,\n email: placeholderEmail(payload.sub, emailDomain),\n emailVerified: false,\n ...mapped,\n },\n data: payload,\n };\n },\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n options: {\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n redirectURI: options.redirectURI,\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn,\n },\n };\n return { provider: provider as unknown as OAuthProvider, emailDomain };\n}\n","import type { HookEndpointContext } from \"@better-auth/core\";\nimport { tryGetCurrentAuthEndpointContext } from \"@better-auth/core/context\";\nimport type { OAuth2Tokens, OAuthProvider } from \"@better-auth/core/oauth2\";\nimport { createAuthMiddleware } from \"better-auth/api\";\nimport { generateRandomString } from \"better-auth/crypto\";\nimport { AUTHENTICATION_CALLBACK_ERRORS } from \"../error-codes\";\nimport type { SteamOptions, SteamProfile } from \"../types\";\nimport { placeholderEmail } from \"../utils/email\";\nimport type { ProviderDefinition } from \"./types\";\n\nexport const STEAM_OPENID_ENDPOINT = \"https://steamcommunity.com/openid/login\";\nconst OPENID_NS = \"http://specs.openid.net/auth/2.0\";\nconst IDENTIFIER_SELECT = \"http://specs.openid.net/auth/2.0/identifier_select\";\nconst CLAIMED_ID = /^https:\\/\\/steamcommunity\\.com\\/openid\\/id\\/(\\d{17})$/;\n/** OpenID 2.0 requires these to be signed in a positive assertion. */\nconst REQUIRED_SIGNED = [\n \"op_endpoint\",\n \"return_to\",\n \"response_nonce\",\n \"assoc_handle\",\n \"claimed_id\",\n \"identity\",\n];\n/** How long the one-time code from the callback hook is valid. */\nconst CODE_TTL_MS = 60_000;\nconst STEAM_TIMEOUT_MS = 10_000;\n\nconst codeIdentifier = (code: string) => `steam-openid:${code}`;\n\n/** Where Steam sends the player back: better-auth's own callback, with the OAuth state. */\nexport function steamReturnTo(redirectURI: string, state: string): string {\n const url = new URL(redirectURI);\n url.searchParams.set(\"state\", state);\n return url.toString();\n}\n\n/** Steam's sign-in page for an OpenID 2.0 `checkid_setup` request. */\nexport function steamAuthorizationURL(returnTo: string): URL {\n const url = new URL(STEAM_OPENID_ENDPOINT);\n url.searchParams.set(\"openid.ns\", OPENID_NS);\n url.searchParams.set(\"openid.mode\", \"checkid_setup\");\n url.searchParams.set(\"openid.return_to\", returnTo);\n url.searchParams.set(\"openid.realm\", new URL(returnTo).origin);\n url.searchParams.set(\"openid.identity\", IDENTIFIER_SELECT);\n url.searchParams.set(\"openid.claimed_id\", IDENTIFIER_SELECT);\n return url;\n}\n\n/** Parses OpenID's key-value form (`key:value` lines). */\nfunction parseKeyValue(text: string): Map<string, string> {\n const result = new Map<string, string>();\n for (const line of text.split(\"\\n\")) {\n const separator = line.indexOf(\":\");\n if (separator > 0) result.set(line.slice(0, separator), line.slice(separator + 1).trim());\n }\n return result;\n}\n\n/**\n * Checks Steam's positive assertion and returns the SteamID64, or `null`.\n * Everything in the query is untrusted until Steam confirms it: the\n * fields are checked against what this request expects, then sent back to\n * Steam (`check_authentication`), which verifies the signature and that\n * the nonce wasn't used before.\n */\nexport async function verifySteamAssertion(\n query: Record<string, unknown>,\n expectedReturnTo: string,\n): Promise<string | null> {\n const get = (key: string) => {\n const value = query[`openid.${key}`];\n return typeof value === \"string\" ? value : undefined;\n };\n if (get(\"ns\") !== OPENID_NS || get(\"mode\") !== \"id_res\") return null;\n if (get(\"op_endpoint\") !== STEAM_OPENID_ENDPOINT) return null;\n if (get(\"return_to\") !== expectedReturnTo) return null;\n const claimedId = get(\"claimed_id\");\n const steamId = claimedId ? CLAIMED_ID.exec(claimedId)?.[1] : undefined;\n if (!steamId || get(\"identity\") !== claimedId) return null;\n const signed = new Set((get(\"signed\") ?? \"\").split(\",\"));\n if (!REQUIRED_SIGNED.every((field) => signed.has(field)) || !get(\"sig\")) return null;\n\n const body = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (key.startsWith(\"openid.\") && typeof value === \"string\") body.set(key, value);\n }\n body.set(\"openid.mode\", \"check_authentication\");\n const response = await fetch(STEAM_OPENID_ENDPOINT, {\n method: \"POST\",\n body,\n redirect: \"error\",\n signal: AbortSignal.timeout(STEAM_TIMEOUT_MS),\n });\n if (!response.ok) return null;\n const answer = parseKeyValue(await response.text());\n return answer.get(\"ns\") === OPENID_NS && answer.get(\"is_valid\") === \"true\" ? steamId : null;\n}\n\n/** Loads the player's public profile; `null` if it can't be loaded. */\nasync function fetchSteamProfile(\n steamId: string,\n apiKey: string,\n apiBaseURL: string,\n): Promise<SteamProfile | null> {\n const url = new URL(\"/ISteamUser/GetPlayerSummaries/v2/\", apiBaseURL);\n url.searchParams.set(\"key\", apiKey);\n url.searchParams.set(\"steamids\", steamId);\n const response = await fetch(url, { signal: AbortSignal.timeout(STEAM_TIMEOUT_MS) });\n if (!response.ok) return null;\n const data = (await response.json()) as { response?: { players?: SteamProfile[] } };\n const player = data.response?.players?.find((p) => p.steamid === steamId);\n return player ?? null;\n}\n\n/**\n * Steam uses OpenID 2.0, not OAuth: it sends the player back with a signed\n * assertion instead of a code. Steam's `return_to` is better-auth's own\n * `/callback/steam`; a before-hook there verifies the assertion and swaps\n * it for a one-time `code`, which `validateAuthorizationCode` redeems. The\n * rest is better-auth's callback, like for any provider: state and CSRF\n * check, user and account, session, redirect.\n */\nexport function steam(options: SteamOptions, emailDomain: string): ProviderDefinition {\n const apiBaseURL = options.apiBaseURL ?? \"https://api.steampowered.com\";\n\n const provider: OAuthProvider<SteamProfile> = {\n id: \"steam\",\n name: \"Steam\",\n accountSubject: ({ profile }) => profile.steamid,\n createAuthorizationURL({ state, redirectURI }) {\n return steamAuthorizationURL(steamReturnTo(redirectURI, state));\n },\n async validateAuthorizationCode({ code }): Promise<OAuth2Tokens | null> {\n const ctx = tryGetCurrentAuthEndpointContext();\n if (!ctx) return null;\n const verified = await ctx.context.internalAdapter.consumeVerificationValue(\n codeIdentifier(code),\n );\n if (!verified || new Date(verified.expiresAt).getTime() < Date.now()) return null;\n return { tokenType: \"steam-openid\", raw: { steamid: verified.value } };\n },\n async getUserInfo(tokens) {\n const steamid = tokens.raw?.steamid;\n if (typeof steamid !== \"string\") return null;\n let profile: SteamProfile = { steamid };\n if (options.apiKey) {\n try {\n profile = (await fetchSteamProfile(steamid, options.apiKey, apiBaseURL)) ?? profile;\n } catch {\n // The profile is cosmetic; the sign-in is verified without it.\n // Not logged in detail: the request URL contains the API key.\n tryGetCurrentAuthEndpointContext()?.context.logger.warn(\n \"[authentication] could not load the Steam profile\",\n );\n }\n }\n const mapped = await options.mapProfileToUser?.(profile);\n return {\n user: {\n name: profile.personaname || `Steam ${steamid}`,\n email: placeholderEmail(steamid, emailDomain),\n emailVerified: false,\n image: profile.avatarfull,\n ...mapped,\n },\n data: profile,\n };\n },\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n options: {\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn,\n },\n };\n\n const callbackHook = {\n matcher: (context: HookEndpointContext) =>\n context.path === \"/callback/:id\" && (context.params as { id?: unknown })?.id === \"steam\",\n handler: createAuthMiddleware(async (ctx) => {\n const query = (ctx.query ?? {}) as Record<string, unknown>;\n const state = typeof query.state === \"string\" ? query.state : undefined;\n // A `code` in the query never reaches the callback: on failure it is\n // blanked (and `error` makes the callback stop first), on success\n // replaced by the hook's own.\n const fail = (error: string) => ({ context: { query: { state, code: \"\", error } } });\n // Steam always returns with GET. better-auth turns a POST to the\n // callback into a GET redirect, which comes back here.\n if (!state || ctx.method !== \"GET\") {\n return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);\n }\n if (query[\"openid.mode\"] === \"cancel\") {\n return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_SIGN_IN_CANCELLED);\n }\n\n let steamId: string | null = null;\n try {\n const expected = steamReturnTo(`${ctx.context.baseURL}/callback/steam`, state);\n steamId = await verifySteamAssertion(query, expected);\n } catch (error) {\n ctx.context.logger.error(\"[authentication] Steam verification failed\", error);\n }\n if (!steamId) return fail(AUTHENTICATION_CALLBACK_ERRORS.STEAM_VERIFICATION_FAILED);\n\n const code = generateRandomString(32);\n await ctx.context.internalAdapter.createVerificationValue({\n identifier: codeIdentifier(code),\n value: steamId,\n expiresAt: new Date(Date.now() + CODE_TTL_MS),\n });\n return { context: { query: { state, code } } };\n }),\n };\n\n return {\n provider: provider as unknown as OAuthProvider,\n emailDomain,\n hooks: { before: [callbackHook] },\n };\n}\n","import { tryGetCurrentAuthEndpointContext } from \"@better-auth/core/context\";\nimport type { OAuthProvider } from \"@better-auth/core/oauth2\";\nimport {\n createAuthorizationURL,\n refreshAccessToken,\n validateAuthorizationCode,\n} from \"@better-auth/core/oauth2\";\nimport type { XboxOptions, XboxProfile } from \"../types\";\nimport { placeholderEmail } from \"../utils/email\";\nimport type { ProviderDefinition } from \"./types\";\n\n/**\n * Microsoft's documented flow for Xbox sign-in on websites (\"Xbox services\n * sign-in for title websites\", Microsoft Game Development Kit): Microsoft\n * account OAuth on the `consumers` tenant with `xboxlive.signin`, then an\n * Xbox user token, then an XSTS token whose claims name the player.\n */\nexport const XBOX_AUTHORIZATION_ENDPOINT =\n \"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize\";\nexport const XBOX_TOKEN_ENDPOINT = \"https://login.microsoftonline.com/consumers/oauth2/v2.0/token\";\nexport const XBOX_USER_AUTH_ENDPOINT = \"https://user.auth.xboxlive.com/user/authenticate\";\nexport const XBOX_XSTS_ENDPOINT = \"https://xsts.auth.xboxlive.com/xsts/authorize\";\nconst XBOX_TIMEOUT_MS = 10_000;\n\ninterface XboxTokenResponse {\n Token?: string;\n DisplayClaims?: { xui?: Record<string, string>[] };\n}\n\nasync function postXbox(url: string, body: unknown): Promise<XboxTokenResponse | null> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n \"x-xbl-contract-version\": \"1\",\n },\n body: JSON.stringify(body),\n redirect: \"error\",\n signal: AbortSignal.timeout(XBOX_TIMEOUT_MS),\n });\n if (!response.ok) {\n // XErr names the reason, e.g. 2148916233: the account has no Xbox\n // profile. Only the status and that code are logged, never the body.\n const xErr = response.headers.get(\"x-err\") ?? \"none\";\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n `[authentication] Xbox token request to ${new URL(url).host} failed (${response.status}, XErr ${xErr})`,\n );\n return null;\n }\n try {\n return (await response.json()) as XboxTokenResponse;\n } catch {\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n `[authentication] Xbox token response from ${new URL(url).host} isn't JSON`,\n );\n return null;\n }\n}\n\n/** Exchanges a Microsoft access token for the player's XSTS claims (XUID, gamertag). */\nexport async function fetchXboxProfile(\n accessToken: string,\n { relyingParty, sandboxId }: { relyingParty: string; sandboxId: string },\n): Promise<XboxProfile | null> {\n const user = await postXbox(XBOX_USER_AUTH_ENDPOINT, {\n RelyingParty: \"http://auth.xboxlive.com\",\n TokenType: \"JWT\",\n Properties: {\n AuthMethod: \"RPS\",\n SiteName: \"user.auth.xboxlive.com\",\n RpsTicket: `d=${accessToken}`,\n },\n });\n if (!user?.Token) return null;\n const xsts = await postXbox(XBOX_XSTS_ENDPOINT, {\n RelyingParty: relyingParty,\n TokenType: \"JWT\",\n Properties: { SandboxId: sandboxId, UserTokens: [user.Token] },\n });\n const claims = xsts?.DisplayClaims?.xui?.[0];\n if (!claims?.xid) {\n if (xsts) {\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n \"[authentication] the XSTS token has no XUID (xid claim); check the relying party\",\n );\n }\n return null;\n }\n return claims as XboxProfile;\n}\n\n/**\n * Xbox, for apps enrolled in the Xbox developer program (Microsoft only\n * grants `xboxlive.signin` to them). The player is identified by the XUID\n * from XSTS; Xbox shares no email address.\n */\nexport function xbox(options: XboxOptions, emailDomain: string): ProviderDefinition {\n const relyingParty = options.relyingParty ?? \"http://xboxlive.com\";\n const sandboxId = options.sandboxId ?? \"RETAIL\";\n // Microsoft allows no other scopes in this request.\n const scopes = [\"xboxlive.signin\", ...(options.offlineAccess ? [\"xboxlive.offline_access\"] : [])];\n\n const provider: OAuthProvider<XboxProfile> = {\n id: \"xbox\",\n name: \"Xbox\",\n accountSubject: ({ profile }) => profile.xid,\n createAuthorizationURL({ state, codeVerifier, redirectURI, additionalParams }) {\n return createAuthorizationURL({\n id: \"xbox\",\n options,\n authorizationEndpoint: XBOX_AUTHORIZATION_ENDPOINT,\n scopes,\n state,\n codeVerifier,\n redirectURI,\n prompt: options.prompt,\n additionalParams,\n });\n },\n validateAuthorizationCode({ code, codeVerifier, redirectURI }) {\n return validateAuthorizationCode({\n code,\n codeVerifier,\n redirectURI,\n options,\n tokenEndpoint: XBOX_TOKEN_ENDPOINT,\n authentication: \"post\",\n });\n },\n refreshAccessToken: (refreshToken) =>\n refreshAccessToken({\n refreshToken,\n options: { clientId: options.clientId, clientSecret: options.clientSecret },\n tokenEndpoint: XBOX_TOKEN_ENDPOINT,\n authentication: \"post\",\n }),\n async getUserInfo(tokens) {\n if (!tokens.accessToken) return null;\n let profile: XboxProfile | null = null;\n try {\n profile = await fetchXboxProfile(tokens.accessToken, { relyingParty, sandboxId });\n } catch (error) {\n // Network failure or timeout: fail the sign-in like any other refusal.\n tryGetCurrentAuthEndpointContext()?.context.logger.error(\n \"[authentication] Xbox Live couldn't be reached\",\n error,\n );\n }\n if (!profile) return null;\n const mapped = await options.mapProfileToUser?.(profile);\n return {\n user: {\n name: profile.gtg || `Xbox ${profile.xid}`,\n email: placeholderEmail(profile.xid, emailDomain),\n emailVerified: false,\n ...mapped,\n },\n data: profile,\n };\n },\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n options: {\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n redirectURI: options.redirectURI,\n disableSignUp: options.disableSignUp,\n disableImplicitSignUp: options.disableImplicitSignUp,\n overrideUserInfoOnSignIn: options.overrideUserInfoOnSignIn,\n },\n };\n return { provider: provider as unknown as OAuthProvider, emailDomain };\n}\n","import { createFail } from \"@repo/plugin-utils/options\";\nimport { epic } from \"./providers/epic\";\nimport { steam } from \"./providers/steam\";\nimport type { ProviderDefinition } from \"./providers/types\";\nimport { xbox } from \"./providers/xbox\";\nimport type { AuthenticationOptions } from \"./types\";\nimport { isValidEmailDomain } from \"./utils/email\";\n\nconst fail = createFail(\"authentication\");\n\nfunction emailDomain(providerId: string, domain: string | undefined): string {\n const resolved = (domain ?? `${providerId}.invalid`).toLowerCase();\n if (!isValidEmailDomain(resolved)) {\n fail(`\"${providerId}.emailDomain\" must be a domain name, got ${JSON.stringify(domain)}.`);\n }\n return resolved;\n}\n\nfunction requireClient(\n providerId: string,\n options: { clientId?: unknown; clientSecret?: unknown },\n) {\n if (typeof options.clientId !== \"string\" || options.clientId === \"\") {\n fail(`\"${providerId}.clientId\" is required.`);\n }\n if (typeof options.clientSecret !== \"string\" || options.clientSecret === \"\") {\n fail(`\"${providerId}.clientSecret\" is required.`);\n }\n}\n\n/** Validates the options once, at creation, and builds the providers. */\nexport function resolveProviders(options: AuthenticationOptions): ProviderDefinition[] {\n const definitions: ProviderDefinition[] = [];\n if (options.steam) {\n if (options.steam.apiKey !== undefined && options.steam.apiKey.trim() === \"\") {\n fail('\"steam.apiKey\" is empty; leave it out to sign in without loading profiles.');\n }\n definitions.push(steam(options.steam, emailDomain(\"steam\", options.steam.emailDomain)));\n }\n if (options.epic) {\n requireClient(\"epic\", options.epic);\n definitions.push(epic(options.epic, emailDomain(\"epic\", options.epic.emailDomain)));\n }\n if (options.xbox) {\n requireClient(\"xbox\", options.xbox);\n definitions.push(xbox(options.xbox, emailDomain(\"xbox\", options.xbox.emailDomain)));\n }\n if (definitions.length === 0) fail(\"Configure at least one provider: steam, epic, or xbox.\");\n\n const domains = definitions.map((definition) => definition.emailDomain);\n if (new Set(domains).size !== domains.length) {\n fail(\"Each provider needs an email domain of its own.\");\n }\n return definitions;\n}\n","import type { BetterAuthPlugin } from \"@better-auth/core\";\nimport { version } from \"../package.json\";\nimport { AUTHENTICATION_ERROR_CODES } from \"./error-codes\";\nimport { createReservedEmailHooks } from \"./hooks/reserved-email\";\nimport { resolveProviders } from \"./options\";\nimport type { AuthenticationOptions } from \"./types\";\n\nexport {\n AUTHENTICATION_CALLBACK_ERRORS,\n AUTHENTICATION_ERROR_CODES,\n type AuthenticationErrorCode,\n} from \"./error-codes\";\nexport type {\n AuthenticationOptions,\n AuthenticationProviderId,\n CommonProviderOptions,\n EpicOptions,\n EpicProfile,\n SteamOptions,\n SteamProfile,\n XboxOptions,\n XboxProfile,\n} from \"./types\";\n\n/**\n * Adds Steam, Epic Games, and Xbox as social providers. They are used like\n * better-auth's built-in ones: `signIn.social({ provider: \"steam\" })`,\n * `linkSocial`, callbacks at `/callback/<provider>`, account linking,\n * `callbackURL` checks, and sign-up options all work the same way.\n *\n * @example\n * ```ts\n * import { betterAuth } from \"better-auth\";\n * import { authentication } from \"@checodo/better-auth-authentication\";\n *\n * export const auth = betterAuth({\n * plugins: [\n * authentication({\n * steam: { apiKey: process.env.STEAM_API_KEY },\n * epic: { clientId: \"...\", clientSecret: \"...\" },\n * }),\n * ],\n * });\n *\n * // client\n * await authClient.signIn.social({ provider: \"steam\", callbackURL: \"/dashboard\" });\n * ```\n */\nexport const authentication = (options: AuthenticationOptions) => {\n const definitions = resolveProviders(options);\n const before = definitions.flatMap((definition) => definition.hooks?.before ?? []);\n\n return {\n id: \"authentication\",\n version,\n init(ctx) {\n const ids = new Set(definitions.map((definition) => definition.provider.id));\n for (const existing of ctx.socialProviders) {\n if (ids.has(existing.id)) {\n ctx.logger.warn(\n `[authentication] the \"${existing.id}\" provider replaces a social provider with the same id`,\n );\n }\n }\n return {\n context: {\n socialProviders: [\n ...definitions.map((definition) => definition.provider),\n ...ctx.socialProviders.filter((provider) => !ids.has(provider.id)),\n ],\n },\n options: { databaseHooks: createReservedEmailHooks(definitions) },\n };\n },\n hooks: { before },\n $ERROR_CODES: AUTHENTICATION_ERROR_CODES,\n options,\n } satisfies BetterAuthPlugin;\n};\n\nexport type AuthenticationPlugin = ReturnType<typeof authentication>;\n\n/** Lets `auth.$context.getPlugin(\"authentication\")` infer this plugin's exact type. */\ndeclare module \"@better-auth/core\" {\n interface BetterAuthPluginRegistry<AuthOptions, Options> {\n authentication: {\n creator: typeof authentication;\n };\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@checodo/better-auth-authentication",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Sign in with Steam, Epic Games, and Xbox in better-auth, like any built-in social provider.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Clas Heinemeyer",
9
+ "email": "the.checodo@gmail.com",
10
+ "url": "https://checodo.dev"
11
+ },
12
+ "homepage": "https://github.com/checodo/better-auth-plugins/tree/main/packages/authentication",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/checodo/better-auth-plugins.git",
16
+ "directory": "packages/authentication"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/checodo/better-auth-plugins/issues"
20
+ },
21
+ "keywords": [
22
+ "better-auth",
23
+ "better-auth-plugin",
24
+ "oauth",
25
+ "steam",
26
+ "epic-games",
27
+ "xbox",
28
+ "auth"
29
+ ],
30
+ "sideEffects": false,
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./client": {
37
+ "types": "./dist/client.d.ts",
38
+ "default": "./dist/client.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "main": "./dist/index.js",
43
+ "types": "./dist/index.d.ts",
44
+ "files": [
45
+ "dist",
46
+ "CHANGELOG.md"
47
+ ],
48
+ "peerDependencies": {
49
+ "@better-auth/core": "^1.7.20",
50
+ "better-auth": "^1.7.20"
51
+ },
52
+ "dependencies": {
53
+ "zod": "^4.5.4"
54
+ },
55
+ "devDependencies": {
56
+ "@better-auth/core": "^1.7.20",
57
+ "@checodo/better-auth-waitlist": "0.1.0",
58
+ "@repo/plugin-utils": "0.0.0",
59
+ "@repo/tooling": "0.0.0",
60
+ "@types/node": "^24.19.2",
61
+ "better-auth": "^1.7.20",
62
+ "tsdown": "^0.23.7",
63
+ "typescript": "^5.9.3",
64
+ "vitest": "^5.1.0"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "dev": "tsdown --watch --no-clean",
72
+ "test": "vitest run",
73
+ "test:watch": "vitest",
74
+ "typecheck": "tsc --noEmit",
75
+ "clean": "rm -rf dist .turbo",
76
+ "lint:package": "publint && attw --pack . --profile esm-only"
77
+ }
6
78
  }