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.
@@ -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":";;;;;;qBAmCa;;;;;;IAQE;IAAY;;;;;;;;;;;;;;;;;MASV;;;;;YAKL;WACD,YAAY,kBAAkB;WAC9B,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,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,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;;;;;qBA4BlB,yBAAyB"}
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"}
@@ -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 MAGIC_LINK_CAPTCHA_ENDPOINTS = [
8
+ const DEFAULT_CAPTCHA_ENDPOINTS = [
9
9
  "/sign-up/email",
10
10
  "/sign-in/email",
11
- "/request-password-reset",
12
- "/sign-in/magic-link"
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 (options.magicLink !== void 0 && captchaOptions.endpoints === void 0) captchaOptions.endpoints = [...MAGIC_LINK_CAPTCHA_ENDPOINTS];
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));
@@ -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.6.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 magic-link requests.
26
- const MAGIC_LINK_CAPTCHA_ENDPOINTS = [
27
- '/sign-up/email',
28
- '/sign-in/email',
29
- '/request-password-reset',
30
- '/sign-in/magic-link',
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 (options.magicLink !== undefined && captchaOptions.endpoints === undefined)
179
- captchaOptions.endpoints = [...MAGIC_LINK_CAPTCHA_ENDPOINTS]
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))