better-ship 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/better-auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CloudflareTurnstileOptions, LastLoginMethodOptions, anonymous, bearer, captcha, deviceAuthorization, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
1
|
+
import { CloudflareTurnstileOptions, EmailOTPOptions, LastLoginMethodOptions, anonymous, bearer, captcha, deviceAuthorization, emailOTP, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
2
2
|
import { BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitStorage, DBAdapterInstance } from "better-auth";
|
|
3
3
|
//#region src/infrastructure/better-auth/options.d.ts
|
|
4
4
|
/**
|
|
@@ -35,6 +35,7 @@ export declare const authDefaults: {
|
|
|
35
35
|
/** Configure built-in plugins with native options. Runtime auth requires captcha; lastLoginMethod is enabled unless explicitly disabled. */
|
|
36
36
|
export type AuthPluginOptions = {
|
|
37
37
|
readonly magicLink?: Parameters<typeof magicLink>[0];
|
|
38
|
+
readonly emailOTP?: EmailOTPOptions;
|
|
38
39
|
readonly anonymous?: NonNullable<Parameters<typeof anonymous>[0]>;
|
|
39
40
|
readonly captcha?: Omit<CloudflareTurnstileOptions, 'provider'>;
|
|
40
41
|
readonly lastLoginMethod?: false | NonNullable<Parameters<typeof lastLoginMethod>[0]>;
|
|
@@ -61,6 +62,7 @@ export type AuthConfig = Omit<BetterAuthOptions, 'database' | 'plugins' | 'rateL
|
|
|
61
62
|
});
|
|
62
63
|
type PluginTypes = {
|
|
63
64
|
magicLink: ReturnType<typeof magicLink>;
|
|
65
|
+
emailOTP: ReturnType<typeof emailOTP>;
|
|
64
66
|
anonymous: ReturnType<typeof anonymous>;
|
|
65
67
|
captcha: ReturnType<typeof captcha>;
|
|
66
68
|
lastLoginMethod: ReturnType<typeof lastLoginMethod<{}>>;
|
|
@@ -72,7 +74,7 @@ type LastLoginPlugin<Options extends AuthPluginOptions> = Options extends {
|
|
|
72
74
|
readonly lastLoginMethod: infer Settings extends LastLoginMethodOptions;
|
|
73
75
|
} ? ReturnType<typeof lastLoginMethod<Settings>> : PluginTypes['lastLoginMethod'];
|
|
74
76
|
type SelectedPlugin<Options extends AuthPluginOptions, Key extends keyof PluginTypes> = Options extends Record<Key, false | undefined> ? [] : Options extends Record<Key, NonNullable<AuthPluginOptions[Key]>> ? [Key extends 'lastLoginMethod' ? LastLoginPlugin<Options> : PluginTypes[Key]] : Key extends 'lastLoginMethod' ? [PluginTypes[Key]] : [];
|
|
75
|
-
type SelectedPlugins<Options extends AuthPluginOptions> = [...SelectedPlugin<Options, 'captcha'>, ...SelectedPlugin<Options, 'lastLoginMethod'>, ...SelectedPlugin<Options, 'openAPI'>, ...SelectedPlugin<Options, 'magicLink'>, ...SelectedPlugin<Options, 'anonymous'>, ...SelectedPlugin<Options, 'deviceAuthorization'>, ...SelectedPlugin<Options, 'bearer'>];
|
|
77
|
+
type SelectedPlugins<Options extends AuthPluginOptions> = [...SelectedPlugin<Options, 'captcha'>, ...SelectedPlugin<Options, 'lastLoginMethod'>, ...SelectedPlugin<Options, 'openAPI'>, ...SelectedPlugin<Options, 'magicLink'>, ...SelectedPlugin<Options, 'emailOTP'>, ...SelectedPlugin<Options, 'anonymous'>, ...SelectedPlugin<Options, 'deviceAuthorization'>, ...SelectedPlugin<Options, 'bearer'>];
|
|
76
78
|
type ConfiguredPlugins<Config extends AuthConfig> = SelectedPlugins<Config['integrations']>;
|
|
77
79
|
type MergeOptions<Defaults, Overrides> = Omit<Defaults, keyof Overrides> & Overrides;
|
|
78
80
|
type SessionOptions<Config extends AuthConfig> = MergeOptions<typeof authDefaults.session, NonNullable<Config['session']>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"better-auth.d.ts","names":[],"sources":["../src/infrastructure/better-auth/options.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"better-auth.d.ts","names":[],"sources":["../src/infrastructure/better-auth/options.ts"],"mappings":";;;;;;qBAuCa;;;;;;IAQE;IAAY;;;;;;;;;;;;;;;;;MASV;;;;;YAKL;WACD,YAAY,kBAAkB;WAC9B,WAAW;WACX,YAAY,YAAY,kBAAkB;WAC1C,UAAU,KAAK;WACf,0BAA0B,YAAY,kBAAkB;WACxD,UAAU,YAAY,kBAAkB;WACxC,SAAS,YAAY,kBAAkB;WACvC,sBAAsB,YAAY,kBAAkB;;;;;;YAOnD,aAAa,KAAK;WACnB,YAAY,KACnB,YAAY;;WAKC;WACA,kBAAkB;WAClB,cAAc;aACZ,SAAS,KAAK;;;WAIhB;WACA;WACA,cAAc;;KAI1B;EACH,WAAW,kBAAkB;EAC7B,UAAU,kBAAkB;EAC5B,WAAW,kBAAkB;EAC7B,SAAS,kBAAkB;EAC3B,iBAAiB,kBAAkB;EACnC,SAAS,kBAAkB;EAC3B,QAAQ,kBAAkB;EAC1B,qBAAqB,kBAAkB;;KAGpC,gBAAgB,gBAAgB,qBAAqB;WAC/C,uBAAuB,iBAAiB;IAE/C,kBAAkB,gBAAgB,aAClC;KAEC,eAAe,gBAAgB,mBAAmB,kBAAkB,eACvE,gBAAgB,OAAO,+BAEnB,gBAAgB,OAAO,KAAK,YAAY,kBAAkB,UACvD,gCAAgC,gBAAgB,WAAW,YAAY,QACxE,iCACG,YAAY;KAGlB,gBAAgB,gBAAgB,yBAChC,eAAe,wBACf,eAAe,gCACf,eAAe,wBACf,eAAe,0BACf,eAAe,yBACf,eAAe,0BACf,eAAe,oCACf,eAAe;KAGf,kBAAkB,eAAe,cAAc,gBAAgB;KAE/D,aAAa,UAAU,aAAa,KAAK,gBAAgB,aAAa;KAEtE,eAAe,eAAe,cAAc,oBACxC,aAAa,SACpB,YAAY;KAGT,kBAAkB,eAAe,YAAY,gBAAgB,sBAAsB,KACtF;EAGA,UAAU;EACV,YAAY,oBAAoB,aAAa,YAAY,YAAY;EACrE,WAAW,YAAY;EACvB,SAAS,KAAK,eAAe;IAC3B,aAAa,oBACJ,aAAa,QAAQ,aAC5B,YAAY,YAAY;;EAG5B,UAAU,oBAAoB,aAAa,UAAU,YAAY;IAC/D,UAAU,oBACD,aAAa,SAAS,UAC7B,YAAY,YAAY;IAE1B,WAAW,oBACF,aAAa,SAAS,WAC7B,YAAY,YAAY;;EAG5B,aAAa,kBAAkB,YAAY;;;YAIjC,kCACJ,eAAe,kBACf,gBAAgB,yBAEtB,UAAU,mBACV,QAAQ,QACR,oBAAoB,YACjB,kBAAkB,QAAQ;;;;;qBA0ClB,yBAAyB"}
|
package/dist/better-auth.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { a as createLogger } from "./core-Cx5SeyKV.js";
|
|
2
2
|
import { v7 } from "uuid";
|
|
3
3
|
import { isAPIError } from "better-auth/api";
|
|
4
|
-
import { anonymous, bearer, captcha, deviceAuthorization, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
4
|
+
import { anonymous, bearer, captcha, deviceAuthorization, emailOTP, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
5
5
|
|
|
6
6
|
//#region src/infrastructure/better-auth/options.ts
|
|
7
7
|
const logger = createLogger("auth");
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_CAPTCHA_ENDPOINTS = [
|
|
9
9
|
"/sign-up/email",
|
|
10
10
|
"/sign-in/email",
|
|
11
|
-
"/request-password-reset"
|
|
12
|
-
|
|
11
|
+
"/request-password-reset"
|
|
12
|
+
];
|
|
13
|
+
const MAGIC_LINK_CAPTCHA_ENDPOINT = "/sign-in/magic-link";
|
|
14
|
+
const EMAIL_OTP_CAPTCHA_ENDPOINTS = [
|
|
15
|
+
"/email-otp/send-verification-otp",
|
|
16
|
+
"/email-otp/request-password-reset",
|
|
17
|
+
"/forget-password/email-otp"
|
|
13
18
|
];
|
|
14
19
|
/**
|
|
15
20
|
* Shared policy from Porte and Typist. The factory merges application overrides into these defaults.
|
|
@@ -47,7 +52,12 @@ function buildPlugins(options) {
|
|
|
47
52
|
const plugins = [];
|
|
48
53
|
if (options.captcha !== void 0) {
|
|
49
54
|
const captchaOptions = { ...options.captcha };
|
|
50
|
-
if (
|
|
55
|
+
if (captchaOptions.endpoints === void 0) {
|
|
56
|
+
const endpoints = [...DEFAULT_CAPTCHA_ENDPOINTS];
|
|
57
|
+
if (options.magicLink !== void 0) endpoints.push(MAGIC_LINK_CAPTCHA_ENDPOINT);
|
|
58
|
+
if (options.emailOTP !== void 0) endpoints.push(...EMAIL_OTP_CAPTCHA_ENDPOINTS);
|
|
59
|
+
if (endpoints.length > DEFAULT_CAPTCHA_ENDPOINTS.length) captchaOptions.endpoints = endpoints;
|
|
60
|
+
}
|
|
51
61
|
plugins.push(captcha({
|
|
52
62
|
...captchaOptions,
|
|
53
63
|
provider: "cloudflare-turnstile"
|
|
@@ -59,6 +69,12 @@ function buildPlugins(options) {
|
|
|
59
69
|
expiresIn: 300,
|
|
60
70
|
...options.magicLink
|
|
61
71
|
}));
|
|
72
|
+
if (options.emailOTP !== void 0) plugins.push(emailOTP({
|
|
73
|
+
expiresIn: 300,
|
|
74
|
+
storeOTP: "encrypted",
|
|
75
|
+
resendStrategy: "reuse",
|
|
76
|
+
...options.emailOTP
|
|
77
|
+
}));
|
|
62
78
|
if (options.anonymous !== void 0) plugins.push(anonymous(options.anonymous));
|
|
63
79
|
if (options.deviceAuthorization !== void 0) plugins.push(deviceAuthorization(options.deviceAuthorization));
|
|
64
80
|
if (options.bearer !== void 0) plugins.push(bearer(options.bearer));
|
package/dist/better-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"better-auth.js","names":["uuidv7"],"sources":["../src/infrastructure/better-auth/options.ts"],"sourcesContent":["import type {\n BetterAuthOptions,\n BetterAuthPlugin,\n BetterAuthRateLimitStorage,\n DBAdapterInstance,\n} from 'better-auth'\nimport { isAPIError } from 'better-auth/api'\nimport {\n anonymous,\n bearer,\n captcha,\n deviceAuthorization,\n lastLoginMethod,\n magicLink,\n openAPI,\n type CloudflareTurnstileOptions,\n type LastLoginMethodOptions,\n} from 'better-auth/plugins'\nimport { v7 as uuidv7 } from 'uuid'\n\nimport { createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n\n// Better Auth does not export its default captcha paths; keep them when adding magic-link requests.\nconst MAGIC_LINK_CAPTCHA_ENDPOINTS = [\n '/sign-up/email',\n '/sign-in/email',\n '/request-password-reset',\n '/sign-in/magic-link',\n]\n\n/**\n * Shared policy from Porte and Typist. The factory merges application overrides into these defaults.\n */\nexport const authDefaults = {\n onAPIError: {\n throw: false,\n onError(error) {\n if (isAPIError(error) && error.statusCode < 500) return\n logger.error('auth_operation_failed', { error })\n },\n },\n rateLimit: { window: 60, max: 100 },\n session: {\n expiresIn: 60 * 60 * 24 * 7,\n updateAge: 60 * 60 * 24,\n freshAge: 60 * 60 * 24,\n cookieCache: { enabled: true, maxAge: 300 },\n },\n advanced: {\n database: { generateId: () => uuidv7(), joins: true },\n ipAddress: { ipAddressHeaders: ['cf-connecting-ip', 'x-forwarded-for'] },\n },\n} satisfies BetterAuthOptions\n\n/** Configure built-in plugins with native options. Runtime auth requires captcha; lastLoginMethod is enabled unless explicitly disabled. */\nexport type AuthPluginOptions = {\n readonly magicLink?: Parameters<typeof magicLink>[0]\n readonly anonymous?: NonNullable<Parameters<typeof anonymous>[0]>\n readonly captcha?: Omit<CloudflareTurnstileOptions, 'provider'>\n readonly lastLoginMethod?: false | NonNullable<Parameters<typeof lastLoginMethod>[0]>\n readonly openAPI?: NonNullable<Parameters<typeof openAPI>[0]>\n readonly bearer?: NonNullable<Parameters<typeof bearer>[0]>\n readonly deviceAuthorization?: NonNullable<Parameters<typeof deviceAuthorization>[0]>\n}\n\n/**\n * Runtime auth requires rate limiting and Turnstile credentials. Schema generation omits both.\n * Both modes select plugins through `integrations`, so schema generation must repeat the runtime selection.\n */\nexport type AuthConfig = Omit<BetterAuthOptions, 'database' | 'plugins' | 'rateLimit'> & {\n readonly rateLimit?: Omit<\n NonNullable<BetterAuthOptions['rateLimit']>,\n 'enabled' | 'storage' | 'customStorage'\n >\n} & (\n | {\n readonly mode?: 'runtime'\n readonly rateLimitStorage: BetterAuthRateLimitStorage\n readonly integrations: AuthPluginOptions & {\n readonly captcha: Omit<CloudflareTurnstileOptions, 'provider'>\n }\n }\n | {\n readonly mode: 'schema'\n readonly rateLimitStorage?: never\n readonly integrations: AuthPluginOptions\n }\n )\n\ntype PluginTypes = {\n magicLink: ReturnType<typeof magicLink>\n anonymous: ReturnType<typeof anonymous>\n captcha: ReturnType<typeof captcha>\n lastLoginMethod: ReturnType<typeof lastLoginMethod<{}>>\n openAPI: ReturnType<typeof openAPI>\n bearer: ReturnType<typeof bearer>\n deviceAuthorization: ReturnType<typeof deviceAuthorization>\n}\n\ntype LastLoginPlugin<Options extends AuthPluginOptions> = Options extends {\n readonly lastLoginMethod: infer Settings extends LastLoginMethodOptions\n}\n ? ReturnType<typeof lastLoginMethod<Settings>>\n : PluginTypes['lastLoginMethod']\n\ntype SelectedPlugin<Options extends AuthPluginOptions, Key extends keyof PluginTypes> =\n Options extends Record<Key, false | undefined>\n ? []\n : Options extends Record<Key, NonNullable<AuthPluginOptions[Key]>>\n ? [Key extends 'lastLoginMethod' ? LastLoginPlugin<Options> : PluginTypes[Key]]\n : Key extends 'lastLoginMethod'\n ? [PluginTypes[Key]]\n : []\n\ntype SelectedPlugins<Options extends AuthPluginOptions> = [\n ...SelectedPlugin<Options, 'captcha'>,\n ...SelectedPlugin<Options, 'lastLoginMethod'>,\n ...SelectedPlugin<Options, 'openAPI'>,\n ...SelectedPlugin<Options, 'magicLink'>,\n ...SelectedPlugin<Options, 'anonymous'>,\n ...SelectedPlugin<Options, 'deviceAuthorization'>,\n ...SelectedPlugin<Options, 'bearer'>,\n]\n\ntype ConfiguredPlugins<Config extends AuthConfig> = SelectedPlugins<Config['integrations']>\n\ntype MergeOptions<Defaults, Overrides> = Omit<Defaults, keyof Overrides> & Overrides\n\ntype SessionOptions<Config extends AuthConfig> = MergeOptions<\n typeof authDefaults.session,\n NonNullable<Config['session']>\n>\n\ntype ConfiguredOptions<Config extends AuthConfig, Plugins extends BetterAuthPlugin[]> = Omit<\n Config,\n 'mode' | 'integrations' | 'rateLimitStorage' | 'rateLimit' | 'session' | 'advanced' | 'onAPIError'\n> & {\n database: DBAdapterInstance\n onAPIError: MergeOptions<typeof authDefaults.onAPIError, NonNullable<Config['onAPIError']>>\n rateLimit: NonNullable<BetterAuthOptions['rateLimit']>\n session: Omit<SessionOptions<Config>, 'cookieCache'> & {\n cookieCache: MergeOptions<\n typeof authDefaults.session.cookieCache,\n NonNullable<NonNullable<Config['session']>['cookieCache']>\n >\n }\n advanced: MergeOptions<typeof authDefaults.advanced, NonNullable<Config['advanced']>> & {\n database: MergeOptions<\n typeof authDefaults.advanced.database,\n NonNullable<NonNullable<Config['advanced']>['database']>\n >\n ipAddress: MergeOptions<\n typeof authDefaults.advanced.ipAddress,\n NonNullable<NonNullable<Config['advanced']>['ipAddress']>\n >\n }\n plugins: [...ConfiguredPlugins<Config>, ...Plugins]\n}\n\n/** A required database adapter and application config produce options with inferred plugin and session types. */\nexport type BetterAuthOptionsFactory = <\n const Config extends AuthConfig,\n const Plugins extends BetterAuthPlugin[] = [],\n>(\n database: DBAdapterInstance,\n config: Config,\n additionalPlugins?: Plugins,\n) => ConfiguredOptions<Config, Plugins>\n\nfunction buildPlugins<const Options extends AuthPluginOptions>(\n options: Options,\n): SelectedPlugins<Options> {\n const plugins: BetterAuthPlugin[] = []\n if (options.captcha !== undefined) {\n const captchaOptions = { ...options.captcha }\n if (options.magicLink !== undefined && captchaOptions.endpoints === undefined)\n captchaOptions.endpoints = [...MAGIC_LINK_CAPTCHA_ENDPOINTS]\n plugins.push(captcha({ ...captchaOptions, provider: 'cloudflare-turnstile' }))\n }\n if (options.lastLoginMethod !== false) plugins.push(lastLoginMethod(options.lastLoginMethod))\n if (options.openAPI !== undefined) plugins.push(openAPI(options.openAPI))\n if (options.magicLink !== undefined)\n plugins.push(magicLink({ expiresIn: 300, ...options.magicLink }))\n if (options.anonymous !== undefined) plugins.push(anonymous(options.anonymous))\n if (options.deviceAuthorization !== undefined)\n plugins.push(deviceAuthorization(options.deviceAuthorization))\n if (options.bearer !== undefined) plugins.push(bearer(options.bearer))\n // SAFETY: Each branch adds exactly the plugin selected by the corresponding configuration key.\n return plugins as SelectedPlugins<Options>\n}\n\n/**\n * Build native Better Auth options with shared defaults and configured plugins; both arguments are required.\n * Pass Stripe and other plugins in the third argument. Put tanstackStartCookies() last.\n */\nexport const createBetterAuthOptions: BetterAuthOptionsFactory = (\n database,\n config,\n additionalPlugins,\n) => {\n const { mode, integrations, rateLimitStorage, ...options } = config\n const result: BetterAuthOptions = {\n ...options,\n database,\n onAPIError: { ...authDefaults.onAPIError, ...options.onAPIError },\n rateLimit:\n mode === 'schema'\n ? { enabled: false }\n : {\n ...authDefaults.rateLimit,\n ...options.rateLimit,\n enabled: true,\n customStorage: rateLimitStorage,\n },\n session: {\n ...authDefaults.session,\n ...options.session,\n cookieCache: { ...authDefaults.session.cookieCache, ...options.session?.cookieCache },\n },\n advanced: {\n ...authDefaults.advanced,\n ...options.advanced,\n database: { ...authDefaults.advanced.database, ...options.advanced?.database },\n ipAddress: { ...authDefaults.advanced.ipAddress, ...options.advanced?.ipAddress },\n },\n plugins: [...buildPlugins(integrations), ...(additionalPlugins ?? [])],\n }\n // SAFETY: The factory preserves configured field schemas and plugins while merging only the documented default fields.\n return result as ConfiguredOptions<typeof config, Exclude<typeof additionalPlugins, undefined>>\n}\n"],"mappings":";;;;;;AAsBA,MAAM,SAAS,aAAa,MAAM;AAGlC,MAAM,+BAA+B;CACnC;CACA;CACA;CACA;AACF;;;;AAKA,MAAa,eAAe;CAC1B,YAAY;EACV,OAAO;EACP,QAAQ,OAAO;GACb,IAAI,WAAW,KAAK,KAAK,MAAM,aAAa,KAAK;GACjD,OAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC;EACjD;CACF;CACA,WAAW;EAAE,QAAQ;EAAI,KAAK;CAAI;CAClC,SAAS;EACP,WAAW;EACX,WAAW;EACX,UAAU;EACV,aAAa;GAAE,SAAS;GAAM,QAAQ;EAAI;CAC5C;CACA,UAAU;EACR,UAAU;GAAE,kBAAkBA,GAAO;GAAG,OAAO;EAAK;EACpD,WAAW,EAAE,kBAAkB,CAAC,oBAAoB,iBAAiB,EAAE;CACzE;AACF;AAqHA,SAAS,aACP,SAC0B;CAC1B,MAAM,UAA8B,CAAC;CACrC,IAAI,QAAQ,YAAY,QAAW;EACjC,MAAM,iBAAiB,EAAE,GAAG,QAAQ,QAAQ;EAC5C,IAAI,QAAQ,cAAc,UAAa,eAAe,cAAc,QAClE,eAAe,YAAY,CAAC,GAAG,4BAA4B;EAC7D,QAAQ,KAAK,QAAQ;GAAE,GAAG;GAAgB,UAAU;EAAuB,CAAC,CAAC;CAC/E;CACA,IAAI,QAAQ,oBAAoB,OAAO,QAAQ,KAAK,gBAAgB,QAAQ,eAAe,CAAC;CAC5F,IAAI,QAAQ,YAAY,QAAW,QAAQ,KAAK,QAAQ,QAAQ,OAAO,CAAC;CACxE,IAAI,QAAQ,cAAc,QACxB,QAAQ,KAAK,UAAU;EAAE,WAAW;EAAK,GAAG,QAAQ;CAAU,CAAC,CAAC;CAClE,IAAI,QAAQ,cAAc,QAAW,QAAQ,KAAK,UAAU,QAAQ,SAAS,CAAC;CAC9E,IAAI,QAAQ,wBAAwB,QAClC,QAAQ,KAAK,oBAAoB,QAAQ,mBAAmB,CAAC;CAC/D,IAAI,QAAQ,WAAW,QAAW,QAAQ,KAAK,OAAO,QAAQ,MAAM,CAAC;CAErE,OAAO;AACT;;;;;AAMA,MAAa,2BACX,UACA,QACA,sBACG;CACH,MAAM,EAAE,MAAM,cAAc,kBAAkB,GAAG,YAAY;CA4B7D,OAAO;EA1BL,GAAG;EACH;EACA,YAAY;GAAE,GAAG,aAAa;GAAY,GAAG,QAAQ;EAAW;EAChE,WACE,SAAS,WACL,EAAE,SAAS,MAAM,IACjB;GACE,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,SAAS;GACT,eAAe;EACjB;EACN,SAAS;GACP,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,aAAa;IAAE,GAAG,aAAa,QAAQ;IAAa,GAAG,QAAQ,SAAS;GAAY;EACtF;EACA,UAAU;GACR,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,UAAU;IAAE,GAAG,aAAa,SAAS;IAAU,GAAG,QAAQ,UAAU;GAAS;GAC7E,WAAW;IAAE,GAAG,aAAa,SAAS;IAAW,GAAG,QAAQ,UAAU;GAAU;EAClF;EACA,SAAS,CAAC,GAAG,aAAa,YAAY,GAAG,GAAI,qBAAqB,CAAC,CAAE;CAG3D;AACd"}
|
|
1
|
+
{"version":3,"file":"better-auth.js","names":["uuidv7"],"sources":["../src/infrastructure/better-auth/options.ts"],"sourcesContent":["import type {\n BetterAuthOptions,\n BetterAuthPlugin,\n BetterAuthRateLimitStorage,\n DBAdapterInstance,\n} from 'better-auth'\nimport { isAPIError } from 'better-auth/api'\nimport {\n anonymous,\n bearer,\n captcha,\n deviceAuthorization,\n emailOTP,\n lastLoginMethod,\n magicLink,\n openAPI,\n type CloudflareTurnstileOptions,\n type EmailOTPOptions,\n type LastLoginMethodOptions,\n} from 'better-auth/plugins'\nimport { v7 as uuidv7 } from 'uuid'\n\nimport { createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n\n// Better Auth does not export its default captcha paths; keep them when adding email delivery endpoints.\nconst DEFAULT_CAPTCHA_ENDPOINTS = ['/sign-up/email', '/sign-in/email', '/request-password-reset']\nconst MAGIC_LINK_CAPTCHA_ENDPOINT = '/sign-in/magic-link'\n// Every OTP endpoint that sends an email, including the deprecated password-reset alias.\nconst EMAIL_OTP_CAPTCHA_ENDPOINTS = [\n '/email-otp/send-verification-otp',\n '/email-otp/request-password-reset',\n '/forget-password/email-otp',\n]\n\n/**\n * Shared policy from Porte and Typist. The factory merges application overrides into these defaults.\n */\nexport const authDefaults = {\n onAPIError: {\n throw: false,\n onError(error) {\n if (isAPIError(error) && error.statusCode < 500) return\n logger.error('auth_operation_failed', { error })\n },\n },\n rateLimit: { window: 60, max: 100 },\n session: {\n expiresIn: 60 * 60 * 24 * 7,\n updateAge: 60 * 60 * 24,\n freshAge: 60 * 60 * 24,\n cookieCache: { enabled: true, maxAge: 300 },\n },\n advanced: {\n database: { generateId: () => uuidv7(), joins: true },\n ipAddress: { ipAddressHeaders: ['cf-connecting-ip', 'x-forwarded-for'] },\n },\n} satisfies BetterAuthOptions\n\n/** Configure built-in plugins with native options. Runtime auth requires captcha; lastLoginMethod is enabled unless explicitly disabled. */\nexport type AuthPluginOptions = {\n readonly magicLink?: Parameters<typeof magicLink>[0]\n readonly emailOTP?: EmailOTPOptions\n readonly anonymous?: NonNullable<Parameters<typeof anonymous>[0]>\n readonly captcha?: Omit<CloudflareTurnstileOptions, 'provider'>\n readonly lastLoginMethod?: false | NonNullable<Parameters<typeof lastLoginMethod>[0]>\n readonly openAPI?: NonNullable<Parameters<typeof openAPI>[0]>\n readonly bearer?: NonNullable<Parameters<typeof bearer>[0]>\n readonly deviceAuthorization?: NonNullable<Parameters<typeof deviceAuthorization>[0]>\n}\n\n/**\n * Runtime auth requires rate limiting and Turnstile credentials. Schema generation omits both.\n * Both modes select plugins through `integrations`, so schema generation must repeat the runtime selection.\n */\nexport type AuthConfig = Omit<BetterAuthOptions, 'database' | 'plugins' | 'rateLimit'> & {\n readonly rateLimit?: Omit<\n NonNullable<BetterAuthOptions['rateLimit']>,\n 'enabled' | 'storage' | 'customStorage'\n >\n} & (\n | {\n readonly mode?: 'runtime'\n readonly rateLimitStorage: BetterAuthRateLimitStorage\n readonly integrations: AuthPluginOptions & {\n readonly captcha: Omit<CloudflareTurnstileOptions, 'provider'>\n }\n }\n | {\n readonly mode: 'schema'\n readonly rateLimitStorage?: never\n readonly integrations: AuthPluginOptions\n }\n )\n\ntype PluginTypes = {\n magicLink: ReturnType<typeof magicLink>\n emailOTP: ReturnType<typeof emailOTP>\n anonymous: ReturnType<typeof anonymous>\n captcha: ReturnType<typeof captcha>\n lastLoginMethod: ReturnType<typeof lastLoginMethod<{}>>\n openAPI: ReturnType<typeof openAPI>\n bearer: ReturnType<typeof bearer>\n deviceAuthorization: ReturnType<typeof deviceAuthorization>\n}\n\ntype LastLoginPlugin<Options extends AuthPluginOptions> = Options extends {\n readonly lastLoginMethod: infer Settings extends LastLoginMethodOptions\n}\n ? ReturnType<typeof lastLoginMethod<Settings>>\n : PluginTypes['lastLoginMethod']\n\ntype SelectedPlugin<Options extends AuthPluginOptions, Key extends keyof PluginTypes> =\n Options extends Record<Key, false | undefined>\n ? []\n : Options extends Record<Key, NonNullable<AuthPluginOptions[Key]>>\n ? [Key extends 'lastLoginMethod' ? LastLoginPlugin<Options> : PluginTypes[Key]]\n : Key extends 'lastLoginMethod'\n ? [PluginTypes[Key]]\n : []\n\ntype SelectedPlugins<Options extends AuthPluginOptions> = [\n ...SelectedPlugin<Options, 'captcha'>,\n ...SelectedPlugin<Options, 'lastLoginMethod'>,\n ...SelectedPlugin<Options, 'openAPI'>,\n ...SelectedPlugin<Options, 'magicLink'>,\n ...SelectedPlugin<Options, 'emailOTP'>,\n ...SelectedPlugin<Options, 'anonymous'>,\n ...SelectedPlugin<Options, 'deviceAuthorization'>,\n ...SelectedPlugin<Options, 'bearer'>,\n]\n\ntype ConfiguredPlugins<Config extends AuthConfig> = SelectedPlugins<Config['integrations']>\n\ntype MergeOptions<Defaults, Overrides> = Omit<Defaults, keyof Overrides> & Overrides\n\ntype SessionOptions<Config extends AuthConfig> = MergeOptions<\n typeof authDefaults.session,\n NonNullable<Config['session']>\n>\n\ntype ConfiguredOptions<Config extends AuthConfig, Plugins extends BetterAuthPlugin[]> = Omit<\n Config,\n 'mode' | 'integrations' | 'rateLimitStorage' | 'rateLimit' | 'session' | 'advanced' | 'onAPIError'\n> & {\n database: DBAdapterInstance\n onAPIError: MergeOptions<typeof authDefaults.onAPIError, NonNullable<Config['onAPIError']>>\n rateLimit: NonNullable<BetterAuthOptions['rateLimit']>\n session: Omit<SessionOptions<Config>, 'cookieCache'> & {\n cookieCache: MergeOptions<\n typeof authDefaults.session.cookieCache,\n NonNullable<NonNullable<Config['session']>['cookieCache']>\n >\n }\n advanced: MergeOptions<typeof authDefaults.advanced, NonNullable<Config['advanced']>> & {\n database: MergeOptions<\n typeof authDefaults.advanced.database,\n NonNullable<NonNullable<Config['advanced']>['database']>\n >\n ipAddress: MergeOptions<\n typeof authDefaults.advanced.ipAddress,\n NonNullable<NonNullable<Config['advanced']>['ipAddress']>\n >\n }\n plugins: [...ConfiguredPlugins<Config>, ...Plugins]\n}\n\n/** A required database adapter and application config produce options with inferred plugin and session types. */\nexport type BetterAuthOptionsFactory = <\n const Config extends AuthConfig,\n const Plugins extends BetterAuthPlugin[] = [],\n>(\n database: DBAdapterInstance,\n config: Config,\n additionalPlugins?: Plugins,\n) => ConfiguredOptions<Config, Plugins>\n\nfunction buildPlugins<const Options extends AuthPluginOptions>(\n options: Options,\n): SelectedPlugins<Options> {\n const plugins: BetterAuthPlugin[] = []\n if (options.captcha !== undefined) {\n const captchaOptions = { ...options.captcha }\n if (captchaOptions.endpoints === undefined) {\n const endpoints = [...DEFAULT_CAPTCHA_ENDPOINTS]\n if (options.magicLink !== undefined) endpoints.push(MAGIC_LINK_CAPTCHA_ENDPOINT)\n if (options.emailOTP !== undefined) endpoints.push(...EMAIL_OTP_CAPTCHA_ENDPOINTS)\n if (endpoints.length > DEFAULT_CAPTCHA_ENDPOINTS.length) captchaOptions.endpoints = endpoints\n }\n plugins.push(captcha({ ...captchaOptions, provider: 'cloudflare-turnstile' }))\n }\n if (options.lastLoginMethod !== false) plugins.push(lastLoginMethod(options.lastLoginMethod))\n if (options.openAPI !== undefined) plugins.push(openAPI(options.openAPI))\n if (options.magicLink !== undefined)\n plugins.push(magicLink({ expiresIn: 300, ...options.magicLink }))\n // Encrypted storage keeps resend reuse possible; an unsalted hash of a six-digit code protects nothing.\n if (options.emailOTP !== undefined)\n plugins.push(\n emailOTP({\n expiresIn: 300,\n storeOTP: 'encrypted',\n resendStrategy: 'reuse',\n ...options.emailOTP,\n }),\n )\n if (options.anonymous !== undefined) plugins.push(anonymous(options.anonymous))\n if (options.deviceAuthorization !== undefined)\n plugins.push(deviceAuthorization(options.deviceAuthorization))\n if (options.bearer !== undefined) plugins.push(bearer(options.bearer))\n // SAFETY: Each branch adds exactly the plugin selected by the corresponding configuration key.\n return plugins as SelectedPlugins<Options>\n}\n\n/**\n * Build native Better Auth options with shared defaults and configured plugins; both arguments are required.\n * Pass Stripe and other plugins in the third argument. Put tanstackStartCookies() last.\n */\nexport const createBetterAuthOptions: BetterAuthOptionsFactory = (\n database,\n config,\n additionalPlugins,\n) => {\n const { mode, integrations, rateLimitStorage, ...options } = config\n const result: BetterAuthOptions = {\n ...options,\n database,\n onAPIError: { ...authDefaults.onAPIError, ...options.onAPIError },\n rateLimit:\n mode === 'schema'\n ? { enabled: false }\n : {\n ...authDefaults.rateLimit,\n ...options.rateLimit,\n enabled: true,\n customStorage: rateLimitStorage,\n },\n session: {\n ...authDefaults.session,\n ...options.session,\n cookieCache: { ...authDefaults.session.cookieCache, ...options.session?.cookieCache },\n },\n advanced: {\n ...authDefaults.advanced,\n ...options.advanced,\n database: { ...authDefaults.advanced.database, ...options.advanced?.database },\n ipAddress: { ...authDefaults.advanced.ipAddress, ...options.advanced?.ipAddress },\n },\n plugins: [...buildPlugins(integrations), ...(additionalPlugins ?? [])],\n }\n // SAFETY: The factory preserves configured field schemas and plugins while merging only the documented default fields.\n return result as ConfiguredOptions<typeof config, Exclude<typeof additionalPlugins, undefined>>\n}\n"],"mappings":";;;;;;AAwBA,MAAM,SAAS,aAAa,MAAM;AAGlC,MAAM,4BAA4B;CAAC;CAAkB;CAAkB;AAAyB;AAChG,MAAM,8BAA8B;AAEpC,MAAM,8BAA8B;CAClC;CACA;CACA;AACF;;;;AAKA,MAAa,eAAe;CAC1B,YAAY;EACV,OAAO;EACP,QAAQ,OAAO;GACb,IAAI,WAAW,KAAK,KAAK,MAAM,aAAa,KAAK;GACjD,OAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC;EACjD;CACF;CACA,WAAW;EAAE,QAAQ;EAAI,KAAK;CAAI;CAClC,SAAS;EACP,WAAW;EACX,WAAW;EACX,UAAU;EACV,aAAa;GAAE,SAAS;GAAM,QAAQ;EAAI;CAC5C;CACA,UAAU;EACR,UAAU;GAAE,kBAAkBA,GAAO;GAAG,OAAO;EAAK;EACpD,WAAW,EAAE,kBAAkB,CAAC,oBAAoB,iBAAiB,EAAE;CACzE;AACF;AAwHA,SAAS,aACP,SAC0B;CAC1B,MAAM,UAA8B,CAAC;CACrC,IAAI,QAAQ,YAAY,QAAW;EACjC,MAAM,iBAAiB,EAAE,GAAG,QAAQ,QAAQ;EAC5C,IAAI,eAAe,cAAc,QAAW;GAC1C,MAAM,YAAY,CAAC,GAAG,yBAAyB;GAC/C,IAAI,QAAQ,cAAc,QAAW,UAAU,KAAK,2BAA2B;GAC/E,IAAI,QAAQ,aAAa,QAAW,UAAU,KAAK,GAAG,2BAA2B;GACjF,IAAI,UAAU,SAAS,0BAA0B,QAAQ,eAAe,YAAY;EACtF;EACA,QAAQ,KAAK,QAAQ;GAAE,GAAG;GAAgB,UAAU;EAAuB,CAAC,CAAC;CAC/E;CACA,IAAI,QAAQ,oBAAoB,OAAO,QAAQ,KAAK,gBAAgB,QAAQ,eAAe,CAAC;CAC5F,IAAI,QAAQ,YAAY,QAAW,QAAQ,KAAK,QAAQ,QAAQ,OAAO,CAAC;CACxE,IAAI,QAAQ,cAAc,QACxB,QAAQ,KAAK,UAAU;EAAE,WAAW;EAAK,GAAG,QAAQ;CAAU,CAAC,CAAC;CAElE,IAAI,QAAQ,aAAa,QACvB,QAAQ,KACN,SAAS;EACP,WAAW;EACX,UAAU;EACV,gBAAgB;EAChB,GAAG,QAAQ;CACb,CAAC,CACH;CACF,IAAI,QAAQ,cAAc,QAAW,QAAQ,KAAK,UAAU,QAAQ,SAAS,CAAC;CAC9E,IAAI,QAAQ,wBAAwB,QAClC,QAAQ,KAAK,oBAAoB,QAAQ,mBAAmB,CAAC;CAC/D,IAAI,QAAQ,WAAW,QAAW,QAAQ,KAAK,OAAO,QAAQ,MAAM,CAAC;CAErE,OAAO;AACT;;;;;AAMA,MAAa,2BACX,UACA,QACA,sBACG;CACH,MAAM,EAAE,MAAM,cAAc,kBAAkB,GAAG,YAAY;CA4B7D,OAAO;EA1BL,GAAG;EACH;EACA,YAAY;GAAE,GAAG,aAAa;GAAY,GAAG,QAAQ;EAAW;EAChE,WACE,SAAS,WACL,EAAE,SAAS,MAAM,IACjB;GACE,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,SAAS;GACT,eAAe;EACjB;EACN,SAAS;GACP,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,aAAa;IAAE,GAAG,aAAa,QAAQ;IAAa,GAAG,QAAQ,SAAS;GAAY;EACtF;EACA,UAAU;GACR,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,UAAU;IAAE,GAAG,aAAa,SAAS;IAAU,GAAG,QAAQ,UAAU;GAAS;GAC7E,WAAW;IAAE,GAAG,aAAa,SAAS;IAAW,GAAG,QAAQ,UAAU;GAAU;EAClF;EACA,SAAS,CAAC,GAAG,aAAa,YAAY,GAAG,GAAI,qBAAqB,CAAC,CAAE;CAG3D;AACd"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "better-ship",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
bearer,
|
|
11
11
|
captcha,
|
|
12
12
|
deviceAuthorization,
|
|
13
|
+
emailOTP,
|
|
13
14
|
lastLoginMethod,
|
|
14
15
|
magicLink,
|
|
15
16
|
openAPI,
|
|
16
17
|
type CloudflareTurnstileOptions,
|
|
18
|
+
type EmailOTPOptions,
|
|
17
19
|
type LastLoginMethodOptions,
|
|
18
20
|
} from 'better-auth/plugins'
|
|
19
21
|
import { v7 as uuidv7 } from 'uuid'
|
|
@@ -22,12 +24,14 @@ import { createLogger } from '@/core'
|
|
|
22
24
|
|
|
23
25
|
const logger = createLogger('auth')
|
|
24
26
|
|
|
25
|
-
// Better Auth does not export its default captcha paths; keep them when adding
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'/
|
|
27
|
+
// Better Auth does not export its default captcha paths; keep them when adding email delivery endpoints.
|
|
28
|
+
const DEFAULT_CAPTCHA_ENDPOINTS = ['/sign-up/email', '/sign-in/email', '/request-password-reset']
|
|
29
|
+
const MAGIC_LINK_CAPTCHA_ENDPOINT = '/sign-in/magic-link'
|
|
30
|
+
// Every OTP endpoint that sends an email, including the deprecated password-reset alias.
|
|
31
|
+
const EMAIL_OTP_CAPTCHA_ENDPOINTS = [
|
|
32
|
+
'/email-otp/send-verification-otp',
|
|
33
|
+
'/email-otp/request-password-reset',
|
|
34
|
+
'/forget-password/email-otp',
|
|
31
35
|
]
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -57,6 +61,7 @@ export const authDefaults = {
|
|
|
57
61
|
/** Configure built-in plugins with native options. Runtime auth requires captcha; lastLoginMethod is enabled unless explicitly disabled. */
|
|
58
62
|
export type AuthPluginOptions = {
|
|
59
63
|
readonly magicLink?: Parameters<typeof magicLink>[0]
|
|
64
|
+
readonly emailOTP?: EmailOTPOptions
|
|
60
65
|
readonly anonymous?: NonNullable<Parameters<typeof anonymous>[0]>
|
|
61
66
|
readonly captcha?: Omit<CloudflareTurnstileOptions, 'provider'>
|
|
62
67
|
readonly lastLoginMethod?: false | NonNullable<Parameters<typeof lastLoginMethod>[0]>
|
|
@@ -91,6 +96,7 @@ export type AuthConfig = Omit<BetterAuthOptions, 'database' | 'plugins' | 'rateL
|
|
|
91
96
|
|
|
92
97
|
type PluginTypes = {
|
|
93
98
|
magicLink: ReturnType<typeof magicLink>
|
|
99
|
+
emailOTP: ReturnType<typeof emailOTP>
|
|
94
100
|
anonymous: ReturnType<typeof anonymous>
|
|
95
101
|
captcha: ReturnType<typeof captcha>
|
|
96
102
|
lastLoginMethod: ReturnType<typeof lastLoginMethod<{}>>
|
|
@@ -119,6 +125,7 @@ type SelectedPlugins<Options extends AuthPluginOptions> = [
|
|
|
119
125
|
...SelectedPlugin<Options, 'lastLoginMethod'>,
|
|
120
126
|
...SelectedPlugin<Options, 'openAPI'>,
|
|
121
127
|
...SelectedPlugin<Options, 'magicLink'>,
|
|
128
|
+
...SelectedPlugin<Options, 'emailOTP'>,
|
|
122
129
|
...SelectedPlugin<Options, 'anonymous'>,
|
|
123
130
|
...SelectedPlugin<Options, 'deviceAuthorization'>,
|
|
124
131
|
...SelectedPlugin<Options, 'bearer'>,
|
|
@@ -175,14 +182,28 @@ function buildPlugins<const Options extends AuthPluginOptions>(
|
|
|
175
182
|
const plugins: BetterAuthPlugin[] = []
|
|
176
183
|
if (options.captcha !== undefined) {
|
|
177
184
|
const captchaOptions = { ...options.captcha }
|
|
178
|
-
if (
|
|
179
|
-
|
|
185
|
+
if (captchaOptions.endpoints === undefined) {
|
|
186
|
+
const endpoints = [...DEFAULT_CAPTCHA_ENDPOINTS]
|
|
187
|
+
if (options.magicLink !== undefined) endpoints.push(MAGIC_LINK_CAPTCHA_ENDPOINT)
|
|
188
|
+
if (options.emailOTP !== undefined) endpoints.push(...EMAIL_OTP_CAPTCHA_ENDPOINTS)
|
|
189
|
+
if (endpoints.length > DEFAULT_CAPTCHA_ENDPOINTS.length) captchaOptions.endpoints = endpoints
|
|
190
|
+
}
|
|
180
191
|
plugins.push(captcha({ ...captchaOptions, provider: 'cloudflare-turnstile' }))
|
|
181
192
|
}
|
|
182
193
|
if (options.lastLoginMethod !== false) plugins.push(lastLoginMethod(options.lastLoginMethod))
|
|
183
194
|
if (options.openAPI !== undefined) plugins.push(openAPI(options.openAPI))
|
|
184
195
|
if (options.magicLink !== undefined)
|
|
185
196
|
plugins.push(magicLink({ expiresIn: 300, ...options.magicLink }))
|
|
197
|
+
// Encrypted storage keeps resend reuse possible; an unsalted hash of a six-digit code protects nothing.
|
|
198
|
+
if (options.emailOTP !== undefined)
|
|
199
|
+
plugins.push(
|
|
200
|
+
emailOTP({
|
|
201
|
+
expiresIn: 300,
|
|
202
|
+
storeOTP: 'encrypted',
|
|
203
|
+
resendStrategy: 'reuse',
|
|
204
|
+
...options.emailOTP,
|
|
205
|
+
}),
|
|
206
|
+
)
|
|
186
207
|
if (options.anonymous !== undefined) plugins.push(anonymous(options.anonymous))
|
|
187
208
|
if (options.deviceAuthorization !== undefined)
|
|
188
209
|
plugins.push(deviceAuthorization(options.deviceAuthorization))
|