better-ship 0.5.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.
Files changed (69) hide show
  1. package/dist/{application-DEerezCl.js → application-DjsQtpgw.js} +2 -2
  2. package/dist/application-DjsQtpgw.js.map +1 -0
  3. package/dist/application.d.ts +3 -2
  4. package/dist/application.js +1 -1
  5. package/dist/better-auth/apple-client-secret.d.ts +1 -1
  6. package/dist/better-auth/apple-client-secret.js +1 -1
  7. package/dist/better-auth/auth-client.js +1 -1
  8. package/dist/better-auth/rate-limit.d.ts +1 -1
  9. package/dist/better-auth/rate-limit.js +1 -1
  10. package/dist/better-auth.d.ts +4 -2
  11. package/dist/better-auth.d.ts.map +1 -1
  12. package/dist/better-auth.js +23 -7
  13. package/dist/better-auth.js.map +1 -1
  14. package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
  15. package/dist/cloudflare/auth-secondary-storage.js +1 -1
  16. package/dist/cloudflare.d.ts +7 -7
  17. package/dist/cloudflare.d.ts.map +1 -1
  18. package/dist/cloudflare.js +3 -3
  19. package/dist/cloudflare.js.map +1 -1
  20. package/dist/{core-CnwWj4MY.js → core-Cx5SeyKV.js} +91 -2
  21. package/dist/core-Cx5SeyKV.js.map +1 -0
  22. package/dist/core.d.ts +2 -2
  23. package/dist/core.js +2 -2
  24. package/dist/{index-B9Y1hVh6.d.ts → index-BStnggQO.d.ts} +126 -2
  25. package/dist/index-BStnggQO.d.ts.map +1 -0
  26. package/dist/{index-Bdyh3_xC.d.ts → index-DWIRrJMP.d.ts} +21 -50
  27. package/dist/index-DWIRrJMP.d.ts.map +1 -0
  28. package/dist/postgres.d.ts +7 -7
  29. package/dist/postgres.d.ts.map +1 -1
  30. package/dist/postgres.js +3 -3
  31. package/dist/postgres.js.map +1 -1
  32. package/dist/tanstack.d.ts +8 -1
  33. package/dist/tanstack.d.ts.map +1 -1
  34. package/dist/tanstack.js +13 -2
  35. package/dist/tanstack.js.map +1 -1
  36. package/dist/{unit-of-work-context-ml9L9cyY.d.ts → unit-of-work-context-CPz9rdSS.d.ts} +3 -2
  37. package/dist/unit-of-work-context-CPz9rdSS.d.ts.map +1 -0
  38. package/dist/{unit-of-work-context-C7vz5OYS.js → unit-of-work-context-DToDpAkv.js} +2 -2
  39. package/dist/unit-of-work-context-DToDpAkv.js.map +1 -0
  40. package/package.json +1 -1
  41. package/src/application/event-collector.ts +3 -3
  42. package/src/application/handlers.ts +7 -4
  43. package/src/application/index.ts +1 -1
  44. package/src/application/message-already-processed.error.ts +1 -3
  45. package/src/application/message-bus.ts +14 -8
  46. package/src/application/message-store.ts +2 -2
  47. package/src/application/registry.ts +14 -13
  48. package/src/application/unit-of-work.ts +2 -1
  49. package/src/core/define-messages.ts +136 -0
  50. package/src/core/entity.ts +33 -0
  51. package/src/core/identity.ts +29 -0
  52. package/src/core/index.ts +19 -0
  53. package/src/{application → core}/messages.ts +4 -4
  54. package/src/infrastructure/better-auth/options.ts +29 -8
  55. package/src/infrastructure/cloudflare/base-d1-repository.ts +2 -2
  56. package/src/infrastructure/cloudflare/d1-message-store.ts +3 -7
  57. package/src/infrastructure/cloudflare/d1-schema.ts +2 -2
  58. package/src/infrastructure/postgres/base-postgres-repository.ts +2 -2
  59. package/src/infrastructure/postgres/postgres-message-store.ts +3 -7
  60. package/src/infrastructure/postgres/postgres-schema.ts +2 -2
  61. package/src/infrastructure/tanstack/index.ts +1 -0
  62. package/src/infrastructure/tanstack/last-login-method.ts +13 -0
  63. package/src/infrastructure/unit-of-work-context.ts +3 -2
  64. package/dist/application-DEerezCl.js.map +0 -1
  65. package/dist/core-CnwWj4MY.js.map +0 -1
  66. package/dist/index-B9Y1hVh6.d.ts.map +0 -1
  67. package/dist/index-Bdyh3_xC.d.ts.map +0 -1
  68. package/dist/unit-of-work-context-C7vz5OYS.js.map +0 -1
  69. package/dist/unit-of-work-context-ml9L9cyY.d.ts.map +0 -1
@@ -1,4 +1,4 @@
1
- import { A as AppError, a as createLogger, f as unreachable, p as DatabaseError } from "./core-CnwWj4MY.js";
1
+ import { N as AppError, a as createLogger, g as DatabaseError, h as unreachable } from "./core-Cx5SeyKV.js";
2
2
  import { panic } from "better-result";
3
3
 
4
4
  //#region src/application/message-already-processed.error.ts
@@ -128,4 +128,4 @@ var UnitOfWorkAlreadyActiveError = class extends AppError {
128
128
 
129
129
  //#endregion
130
130
  export { MessageAlreadyProcessedError as i, UnitOfWork as n, MessageBus as r, UnitOfWorkAlreadyActiveError as t };
131
- //# sourceMappingURL=application-DEerezCl.js.map
131
+ //# sourceMappingURL=application-DjsQtpgw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-DjsQtpgw.js","names":[],"sources":["../src/application/message-already-processed.error.ts","../src/application/message-bus.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import { AppError, DatabaseError, type MessageId } from '@/core'\n\n/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */\nexport class MessageAlreadyProcessedError extends AppError {\n readonly _tag = 'MessageAlreadyProcessedError'\n readonly classification = 'terminal'\n\n constructor(\n readonly messageId: MessageId,\n options?: { readonly cause: unknown },\n ) {\n super(`message ${messageId} already processed`, options)\n }\n\n /** Recognize only the processed-message primary key; preserve other failures unchanged at the call site. */\n static from(cause: unknown, messageId: MessageId): MessageAlreadyProcessedError | undefined {\n if (cause instanceof MessageAlreadyProcessedError) return cause\n if (!(cause instanceof DatabaseError)) return undefined\n const details = cause.details\n const duplicate =\n details.database === 'd1'\n ? details.kind === 'unique' &&\n details.table === 'processed_messages' &&\n details.column === 'message_id'\n : details.code === '23505' &&\n details.table_name === 'processed_messages' &&\n details.constraint_name === 'processed_messages_pkey'\n if (!duplicate) return undefined\n return new MessageAlreadyProcessedError(messageId, { cause })\n }\n}\n","import { panic } from 'better-result'\n\nimport {\n createLogger,\n unreachable,\n type DomainCommand,\n type DomainEvent,\n type DomainMessage,\n type DomainQuery,\n} from '@/core'\n\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { Handlers, MessageResult, RegistryMessage } from './registry.ts'\n\nconst logger = createLogger('message-bus')\n\n/** The port an entrypoint dispatches through. `AppDeps` names this as `IMessageBus<typeof registry>`. */\nexport interface IMessageBus<TRegistry extends Handlers<never>> {\n handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>>\n}\n\n/**\n * Routes one message to its slot. The bus opens no transaction and keeps no outbox:\n * a handler that writes opens the unit of work itself.\n */\nexport class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMessageBus<TRegistry> {\n constructor(\n private readonly deps: Deps,\n private readonly registry: TRegistry,\n ) {}\n\n /** Resolves to the handler's own value for a command or query, and to void for an event. */\n async handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>> {\n // SAFETY: the slot for this name is what `MessageResult` names, and dispatch returns its value.\n return (await this.dispatch(message)) as MessageResult<TMessage, TRegistry>\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- one table holds every slot's value; `handle` names it.\n private dispatch(message: DomainMessage): Promise<unknown> {\n if (message.type === 'command') return this.handleCommand(message)\n if (message.type === 'query') return this.handleQuery(message)\n if (message.type === 'event') return this.handleEvent(message)\n // A message parsed at a boundary cannot reach here; one that does must not resolve silently.\n return unreachable(message)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleCommand(command: DomainCommand): Promise<unknown> {\n // `satisfies` proved the registry complete, so a missing slot is a broken registry object.\n const handler = this.registry.commands[command.name]\n if (handler === undefined) return panic(`No handler for command ${command.name}`)\n logger.debug('handling_command', { name: command.name, id: command.id })\n return handler(command, this.deps)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleQuery(query: DomainQuery): Promise<unknown> {\n const handler = this.registry.queries[query.name]\n if (handler === undefined) return panic(`No handler for query ${query.name}`)\n return handler(query, this.deps)\n }\n\n /**\n * Every subscriber runs to completion. One failure is rethrown as is. Several are thrown as\n * one `AggregateError`. A `MessageAlreadyProcessedError` means that subscriber already committed on\n * an earlier delivery, so it counts as delivered.\n */\n private async handleEvent(event: DomainEvent): Promise<void> {\n const subscriptions = this.registry.events[event.name] ?? []\n const outcomes = await Promise.allSettled(\n subscriptions.map((subscription) => subscription(event, this.deps)),\n )\n const failures: unknown[] = []\n for (const [subscriber, outcome] of outcomes.entries()) {\n if (outcome.status === 'fulfilled') continue\n if (outcome.reason instanceof MessageAlreadyProcessedError) {\n // The bus swallows this error, so this line is the only record that the skip happened.\n logger.info('duplicate_subscriber_ignored', { event: event.name, id: event.id, subscriber })\n continue\n }\n failures.push(outcome.reason)\n }\n if (failures.length === 1) throw failures[0]\n if (failures.length > 1) {\n throw new AggregateError(failures, `${event.name}: ${failures.length} subscribers failed`)\n }\n }\n}\n","import type { MessageId } from '@/core'\n\nimport type { IEventCollector } from './event-collector.ts'\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { IMessageStore } from './message-store.ts'\nimport type { IOutboxRelay } from './outbox-relay.ts'\nimport type { ITransaction } from './transaction.ts'\n\n/**\n * Commits the processed-message record, repository writes, and emitted events together before resolving.\n * An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.\n * Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.\n */\nexport interface IUnitOfWork<Repos> {\n run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>\n}\n\nexport type UnitOfWorkDeps<Tx, Repos> = {\n readonly transaction: ITransaction<Tx>\n /** The processed-messages and outbox repository for this transaction. */\n readonly messageStore: (tx: Tx) => IMessageStore\n /** Repositories participate in the current transaction. */\n readonly createRepositories: (tx: Tx) => Repos\n readonly eventCollector: IEventCollector\n readonly relay: IOutboxRelay\n}\n\n/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */\nexport class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {\n constructor(private readonly deps: UnitOfWorkDeps<Tx, Repos>) {}\n\n async run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T> {\n const { transaction, messageStore, createRepositories, eventCollector, relay } = this.deps\n let written = 0\n\n let value: T\n try {\n value = await transaction.transaction(async (tx) => {\n const store = messageStore(tx)\n await store.claim(messageId)\n const repos = createRepositories(tx)\n const result = await work(repos)\n const pending = eventCollector.collect()\n await store.persistEvents(pending)\n written = pending.length\n return result\n })\n } catch (error) {\n throw MessageAlreadyProcessedError.from(error, messageId) ?? error\n }\n\n if (written > 0) relay.wake()\n return value\n }\n}\n","import { AppError } from '@/core'\n\n/** A boundary was opened inside another. One message, one atomic scope. */\nexport class UnitOfWorkAlreadyActiveError extends AppError {\n readonly _tag = 'UnitOfWorkAlreadyActiveError'\n readonly classification = 'terminal'\n\n constructor() {\n super('a unit of work is already active')\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,3 @@
1
- import { C as Event, E as Query, S as Command, T as MessageId, _ as RegistryMessage, a as UnitOfWorkDeps, b as EventHandler, c as IOutboxRelay, d as CommandRegistry, f as EventRegistry, g as QueryRegistry, h as MessageResult, i as UnitOfWork, l as IMessageBus, m as MessageRegistry, n as MessageAlreadyProcessedError, o as IEventCollector, p as Handlers, r as IUnitOfWork, s as ITransaction, t as UnitOfWorkAlreadyActiveError, u as MessageBus, v as IMessageStore, w as Message, x as QueryHandler, y as CommandHandler } from "./index-Bdyh3_xC.js";
2
- export { type Command, type CommandHandler, type CommandRegistry, type Event, type EventHandler, type EventRegistry, type Handlers, type IEventCollector, type IMessageBus, type IMessageStore, type IOutboxRelay, type ITransaction, type IUnitOfWork, type Message, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type Query, type QueryHandler, type QueryRegistry, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps };
1
+ import { F as DomainQuery, I as MessageId, M as DomainCommand, N as DomainEvent, P as DomainMessage } from "./index-BStnggQO.js";
2
+ import { _ as RegistryMessage, a as UnitOfWorkDeps, b as EventHandler, c as IOutboxRelay, d as CommandRegistry, f as EventRegistry, g as QueryRegistry, h as MessageResult, i as UnitOfWork, l as IMessageBus, m as MessageRegistry, n as MessageAlreadyProcessedError, o as IEventCollector, p as Handlers, r as IUnitOfWork, s as ITransaction, t as UnitOfWorkAlreadyActiveError, u as MessageBus, v as IMessageStore, x as QueryHandler, y as CommandHandler } from "./index-DWIRrJMP.js";
3
+ export { type CommandHandler, type CommandRegistry, type DomainCommand, type DomainEvent, type DomainMessage, type DomainQuery, type EventHandler, type EventRegistry, type Handlers, type IEventCollector, type IMessageBus, type IMessageStore, type IOutboxRelay, type ITransaction, type IUnitOfWork, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type QueryHandler, type QueryRegistry, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps };
@@ -1,3 +1,3 @@
1
- import { i as MessageAlreadyProcessedError, n as UnitOfWork, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-DEerezCl.js";
1
+ import { i as MessageAlreadyProcessedError, n as UnitOfWork, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-DjsQtpgw.js";
2
2
 
3
3
  export { MessageAlreadyProcessedError, MessageBus, UnitOfWork, UnitOfWorkAlreadyActiveError };
@@ -1,4 +1,4 @@
1
- import { $ as AppError } from "../index-B9Y1hVh6.js";
1
+ import { vt as AppError } from "../index-BStnggQO.js";
2
2
  //#region src/infrastructure/better-auth/apple-client-secret.d.ts
3
3
  /** Apple sends its OAuth callback from this origin. Include it in trustedOrigins when enabling Apple. */
4
4
  export declare const APPLE_ORIGIN = "https://appleid.apple.com";
@@ -1,4 +1,4 @@
1
- import { A as AppError, a as createLogger } from "../core-CnwWj4MY.js";
1
+ import { N as AppError, a as createLogger } from "../core-Cx5SeyKV.js";
2
2
  import { SignJWT, importPKCS8 } from "jose";
3
3
 
4
4
  //#region src/infrastructure/better-auth/apple-client-secret.ts
@@ -1,4 +1,4 @@
1
- import { O as RateLimitError } from "../core-CnwWj4MY.js";
1
+ import { j as RateLimitError } from "../core-Cx5SeyKV.js";
2
2
  import { createAuthClient } from "better-auth/client";
3
3
 
4
4
  //#region src/infrastructure/better-auth/auth-client.ts
@@ -1,4 +1,4 @@
1
- import { $ as AppError } from "../index-B9Y1hVh6.js";
1
+ import { vt as AppError } from "../index-BStnggQO.js";
2
2
  import { BetterAuthRateLimitStorage } from "better-auth";
3
3
  //#region src/infrastructure/better-auth/rate-limit.d.ts
4
4
  /** Each rule must match its binding's deployed limit and period. Cloudflare accepts only 10 or 60 seconds. */
@@ -1,4 +1,4 @@
1
- import { A as AppError, a as createLogger } from "../core-CnwWj4MY.js";
1
+ import { N as AppError, a as createLogger } from "../core-Cx5SeyKV.js";
2
2
 
3
3
  //#region src/infrastructure/better-auth/rate-limit.ts
4
4
  /** A binding failure with no known retry classification. The cause preserves the original failure. */
@@ -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
- import { a as createLogger } from "./core-CnwWj4MY.js";
2
- import { isAPIError } from "better-auth/api";
3
- import { anonymous, bearer, captcha, deviceAuthorization, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
1
+ import { a as createLogger } from "./core-Cx5SeyKV.js";
4
2
  import { v7 } from "uuid";
3
+ import { isAPIError } from "better-auth/api";
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"}
@@ -1,4 +1,4 @@
1
- import { $ as AppError } from "../index-B9Y1hVh6.js";
1
+ import { vt as AppError } from "../index-BStnggQO.js";
2
2
  import { DurableObject } from "cloudflare:workers";
3
3
  //#region src/infrastructure/cloudflare/auth-secondary-storage.d.ts
4
4
  /** The supplied TTL cannot produce a valid expiry timestamp. Retrying the same input cannot fix it. */
@@ -1,4 +1,4 @@
1
- import { A as AppError } from "../core-CnwWj4MY.js";
1
+ import { N as AppError } from "../core-Cx5SeyKV.js";
2
2
  import { DurableObject } from "cloudflare:workers";
3
3
 
4
4
  //#region src/infrastructure/cloudflare/auth-secondary-storage.ts
@@ -1,6 +1,6 @@
1
- import { C as Event, T as MessageId, s as ITransaction, v as IMessageStore } from "./index-Bdyh3_xC.js";
2
- import { E as DatabaseFailure, T as DatabaseError, w as D1ConstraintKind } from "./index-B9Y1hVh6.js";
3
- import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-ml9L9cyY.js";
1
+ import { G as DatabaseFailure, I as MessageId, N as DomainEvent, U as D1ConstraintKind, W as DatabaseError } from "./index-BStnggQO.js";
2
+ import { s as ITransaction, v as IMessageStore } from "./index-DWIRrJMP.js";
3
+ import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-CPz9rdSS.js";
4
4
  import { BatchItem } from "drizzle-orm/batch";
5
5
  import { AnyD1Database, DrizzleD1Database } from "drizzle-orm/d1";
6
6
  //#region src/infrastructure/cloudflare/d1-database-types.d.ts
@@ -18,7 +18,7 @@ type D1Tx<Schema extends DrizzleSchema> = {
18
18
  //#region src/infrastructure/cloudflare/base-d1-repository.d.ts
19
19
  /** A domain aggregate that accumulates events for the unit of work to drain. */
20
20
  interface AggregateEvents {
21
- collectEvents(): ReadonlyArray<Event>;
21
+ collectEvents(): ReadonlyArray<DomainEvent>;
22
22
  clearEvents(): void;
23
23
  }
24
24
  /**
@@ -47,7 +47,7 @@ export declare function classifyD1Error(cause: unknown, operation?: string): Dat
47
47
  /** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */
48
48
  export declare class D1MessageStore<Schema extends DrizzleSchema> extends BaseD1Repository<Schema> implements IMessageStore {
49
49
  claim(messageId: MessageId): Promise<void>;
50
- persistEvents(events: ReadonlyArray<Event>): Promise<void>;
50
+ persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void>;
51
51
  }
52
52
  //#endregion
53
53
  //#region src/infrastructure/cloudflare/d1-schema.d.ts
@@ -124,7 +124,7 @@ export declare const messageOutbox: import("drizzle-orm/sqlite-core").SQLiteTabl
124
124
  tableName: "message_outbox";
125
125
  dataType: "json";
126
126
  columnType: "SQLiteTextJson";
127
- data: Event;
127
+ data: DomainEvent;
128
128
  driverParam: string;
129
129
  notNull: true;
130
130
  hasDefault: false;
@@ -136,7 +136,7 @@ export declare const messageOutbox: import("drizzle-orm/sqlite-core").SQLiteTabl
136
136
  identity: undefined;
137
137
  generated: undefined;
138
138
  }, {}, {
139
- $type: Event;
139
+ $type: DomainEvent;
140
140
  }>;
141
141
  createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
142
142
  name: "created_at";
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.d.ts","names":[],"sources":["../src/infrastructure/cloudflare/d1-database-types.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-database-execution.ts"],"mappings":";;;;;;;KAKY,eAAe;;KAGf,KAAK,eAAe,iBAAiB,kBAAkB;;KAGvD,SAAS,eAAe,iBAAiB,KACnD,KAAK;;KAKK,KAAK,eAAe;WACrB,IAAI,KAAK;;;;;UCRH;EACf,iBAAiB,cAAc;EAC/B;;;;;;8BAOoB,iBAAiB,eAAe;qBACrB,IAAI,KAAK;EAAxC,YAA+B,IAAI,KAAK;;YAG9B,MAAM,WAAW;;YAKjB,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;;;;wBCVtB,gBAAgB,gBAAgB,qBAAyB;;;;qBCT5D,eAAe,eAAe,uBACjC,iBAAiB,mBACd;EAEL,MAAM,WAAW,YAAY;EAc7B,cAAc,QAAQ,cAAc,SAAS;;;;;qBC1BxC,qDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAYA,iDAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCPb,mBAAmB,eAAe,0BAA0B,aACvE,KAAK;mBAEwB;EAA7B,YAA6B,IAAI,KAAK;EAEhC,YAAY,GAAG,OAAO,IAAI,KAAK,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ"}
1
+ {"version":3,"file":"cloudflare.d.ts","names":[],"sources":["../src/infrastructure/cloudflare/d1-database-types.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-database-execution.ts"],"mappings":";;;;;;;KAKY,eAAe;;KAGf,KAAK,eAAe,iBAAiB,kBAAkB;;KAGvD,SAAS,eAAe,iBAAiB,KACnD,KAAK;;KAKK,KAAK,eAAe;WACrB,IAAI,KAAK;;;;;UCRH;EACf,iBAAiB,cAAc;EAC/B;;;;;;8BAOoB,iBAAiB,eAAe;qBACrB,IAAI,KAAK;EAAxC,YAA+B,IAAI,KAAK;;YAG9B,MAAM,WAAW;;YAKjB,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;;;;wBCVtB,gBAAgB,gBAAgB,qBAAyB;;;;qBCb5D,eAAe,eAAe,uBACjC,iBAAiB,mBACd;EAEL,MAAM,WAAW,YAAY;EAc7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCtB9C,qDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAYA,iDAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCPb,mBAAmB,eAAe,0BAA0B,aACvE,KAAK;mBAEwB;EAA7B,YAA6B,IAAI,KAAK;EAEhC,YAAY,GAAG,OAAO,IAAI,KAAK,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ"}
@@ -1,6 +1,6 @@
1
- import { A as AppError, p as DatabaseError } from "./core-CnwWj4MY.js";
2
- import { i as MessageAlreadyProcessedError } from "./application-DEerezCl.js";
3
- import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-C7vz5OYS.js";
1
+ import { N as AppError, g as DatabaseError } from "./core-Cx5SeyKV.js";
2
+ import { i as MessageAlreadyProcessedError } from "./application-DjsQtpgw.js";
3
+ import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-DToDpAkv.js";
4
4
  import { DrizzleQueryError } from "drizzle-orm/errors";
5
5
  import { eq, sql } from "drizzle-orm";
6
6
  import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.js","names":[],"sources":["../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type D1ConstraintKind, type FailureClassification } from '@/core'\n\nconst KIND_BY_CODE: ReadonlyMap<string, D1ConstraintKind> = new Map([\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique'],\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key'],\n ['SQLITE_CONSTRAINT_CHECK', 'check'],\n])\n\nconst KIND_BY_PROSE: ReadonlyArray<readonly [string, D1ConstraintKind]> = [\n ['UNIQUE constraint failed', 'unique'],\n ['NOT NULL constraint failed', 'not_null'],\n ['FOREIGN KEY constraint failed', 'foreign_key'],\n ['CHECK constraint failed', 'check'],\n]\n\n/**\n * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.\n * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.\n */\nexport function classifyD1Error(cause: unknown, operation = 'D1 query'): DatabaseError | undefined {\n if (cause instanceof AppError || (cause instanceof Error && cause.name === 'AbortError'))\n return undefined\n const message = messagesOf(cause)\n const kind = kindOf(message)\n const classification = kind === undefined ? classificationOf(message) : 'terminal'\n const code = message.match(/\\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\\b/)?.[0]\n if (code === undefined && classification === 'unknown' && !(cause instanceof DrizzleQueryError))\n return undefined\n // A composite unique constraint must not be mistaken for the processed-message primary key.\n const location = message.match(/constraint failed: (\\w+)\\.(\\w+)(?=\\s*(?::\\s*SQLITE_|$|\\n))/)\n const check = kind === 'check' ? message.match(/CHECK constraint failed: ([A-Za-z_]\\w*)/) : null\n return new DatabaseError(\n operation,\n classification,\n {\n database: 'd1',\n code,\n kind,\n table: location?.[1],\n column: location?.[2],\n constraintName: check?.[1],\n },\n { cause },\n )\n}\n\nfunction kindOf(message: string): D1ConstraintKind | undefined {\n const code = message.match(/SQLITE_CONSTRAINT_\\w+/)?.[0]\n if (code !== undefined) return KIND_BY_CODE.get(code)\n return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1]\n}\n\n// Drizzle query messages contain SQL and values, which are not evidence about the database failure.\nfunction messagesOf(cause: unknown): string {\n const messages: string[] = []\n const seen = new Set<unknown>()\n let current = cause\n while (current instanceof Error && !seen.has(current)) {\n seen.add(current)\n if (!(current instanceof DrizzleQueryError)) messages.push(current.message)\n current = current.cause\n }\n // oxlint-disable-next-line anti-slop/no-runtime-typeof -- D1 can expose a raw string cause; this boundary accepts only that representation.\n if (typeof current === 'string') messages.push(current)\n return messages.join('\\n')\n}\n\n// Cloudflare documents these conditions separately from transient storage resets.\nconst TERMINAL_FAILURE =\n /\\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\\b|\\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i\nconst TRANSIENT_FAILURE =\n /Network connection lost\\.|Replica disconnected from primary\\.|D1 DB reset because its code was updated\\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\\.|Cannot resolve D1 DB due to transient issue on remote node\\.|Can't read from request stream because client disconnected\\./i\n\nfunction classificationOf(message: string): FailureClassification {\n if (TERMINAL_FAILURE.test(message)) return 'terminal'\n if (TRANSIENT_FAILURE.test(message)) return 'transient'\n return 'unknown'\n}\n","import { classifyD1Error } from './classify-d1-error.ts'\n\n/** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyD1Error(cause, operation) ?? cause\n }\n}\n","import type { BatchItem } from 'drizzle-orm/batch'\n\nimport type { Event } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector, stageD1Statement } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<Event>\n clearEvents(): void\n}\n\n/**\n * Shared plumbing for a repository that writes inside a D1 unit of work.\n * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.\n */\nexport abstract class BaseD1Repository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: D1Db<Schema>) {}\n\n /** Add a statement to the current D1 batch without executing it. */\n protected stage(statement: BatchItem<'sqlite'>): void {\n stageD1Statement(statement)\n }\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\nimport type { Event } from '@/application'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = sqliteTable(\n 'processed_messages',\n {\n messageId: text('message_id').primaryKey(),\n processedAt: integer('processed_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n },\n (table) => [index('idx_processed_messages_processed_at').on(table.processedAt)],\n)\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = sqliteTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: text('payload', { mode: 'json' }).$type<Event>().notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n availableAt: integer('available_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: integer('claim_expires_at', { mode: 'timestamp' }),\n publishedAt: integer('published_at', { mode: 'timestamp' }),\n failedAt: integer('failed_at', { mode: 'timestamp' }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(sql`${table.publishedAt} IS NULL`),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { eq } from 'drizzle-orm'\n\nimport {\n MessageAlreadyProcessedError,\n type Event,\n type IMessageStore,\n type MessageId,\n} from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { BaseD1Repository } from './base-d1-repository.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */\nexport class D1MessageStore<Schema extends DrizzleSchema>\n extends BaseD1Repository<Schema>\n implements IMessageStore\n{\n async claim(messageId: MessageId): Promise<void> {\n const processed = await this.read('claim message', () =>\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(eq(processedMessages.messageId, messageId))\n .get(),\n )\n if (processed !== undefined) throw new MessageAlreadyProcessedError(messageId)\n\n // Concurrent deliveries can pass the read; the primary key enforces the claim at commit.\n this.stage(this.db.insert(processedMessages).values({ messageId }))\n }\n\n async persistEvents(events: ReadonlyArray<Event>): Promise<void> {\n if (events.length === 0) return\n this.stage(\n this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event }))),\n )\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { collectD1Statements, runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db, D1Tx } from './d1-database-types.ts'\n\n/**\n * The D1 transaction boundary: every write is staged during `work` and committed as one\n * `db.batch()`. A throw inside `work` means the batch never runs.\n */\nexport class D1BatchTransaction<Schema extends DrizzleSchema> implements ITransaction<\n D1Tx<Schema>\n> {\n constructor(private readonly db: D1Db<Schema>) {}\n\n async transaction<T>(work: (tx: D1Tx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('d1', async () => {\n const value = await work({ db: this.db })\n const [first, ...rest] = collectD1Statements()\n if (first !== undefined) {\n await executeDatabase('commit unit of work', () => this.db.batch([first, ...rest]))\n }\n return value\n })\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cloudflare.js","names":[],"sources":["../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type D1ConstraintKind, type FailureClassification } from '@/core'\n\nconst KIND_BY_CODE: ReadonlyMap<string, D1ConstraintKind> = new Map([\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique'],\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key'],\n ['SQLITE_CONSTRAINT_CHECK', 'check'],\n])\n\nconst KIND_BY_PROSE: ReadonlyArray<readonly [string, D1ConstraintKind]> = [\n ['UNIQUE constraint failed', 'unique'],\n ['NOT NULL constraint failed', 'not_null'],\n ['FOREIGN KEY constraint failed', 'foreign_key'],\n ['CHECK constraint failed', 'check'],\n]\n\n/**\n * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.\n * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.\n */\nexport function classifyD1Error(cause: unknown, operation = 'D1 query'): DatabaseError | undefined {\n if (cause instanceof AppError || (cause instanceof Error && cause.name === 'AbortError'))\n return undefined\n const message = messagesOf(cause)\n const kind = kindOf(message)\n const classification = kind === undefined ? classificationOf(message) : 'terminal'\n const code = message.match(/\\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\\b/)?.[0]\n if (code === undefined && classification === 'unknown' && !(cause instanceof DrizzleQueryError))\n return undefined\n // A composite unique constraint must not be mistaken for the processed-message primary key.\n const location = message.match(/constraint failed: (\\w+)\\.(\\w+)(?=\\s*(?::\\s*SQLITE_|$|\\n))/)\n const check = kind === 'check' ? message.match(/CHECK constraint failed: ([A-Za-z_]\\w*)/) : null\n return new DatabaseError(\n operation,\n classification,\n {\n database: 'd1',\n code,\n kind,\n table: location?.[1],\n column: location?.[2],\n constraintName: check?.[1],\n },\n { cause },\n )\n}\n\nfunction kindOf(message: string): D1ConstraintKind | undefined {\n const code = message.match(/SQLITE_CONSTRAINT_\\w+/)?.[0]\n if (code !== undefined) return KIND_BY_CODE.get(code)\n return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1]\n}\n\n// Drizzle query messages contain SQL and values, which are not evidence about the database failure.\nfunction messagesOf(cause: unknown): string {\n const messages: string[] = []\n const seen = new Set<unknown>()\n let current = cause\n while (current instanceof Error && !seen.has(current)) {\n seen.add(current)\n if (!(current instanceof DrizzleQueryError)) messages.push(current.message)\n current = current.cause\n }\n // oxlint-disable-next-line anti-slop/no-runtime-typeof -- D1 can expose a raw string cause; this boundary accepts only that representation.\n if (typeof current === 'string') messages.push(current)\n return messages.join('\\n')\n}\n\n// Cloudflare documents these conditions separately from transient storage resets.\nconst TERMINAL_FAILURE =\n /\\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\\b|\\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i\nconst TRANSIENT_FAILURE =\n /Network connection lost\\.|Replica disconnected from primary\\.|D1 DB reset because its code was updated\\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\\.|Cannot resolve D1 DB due to transient issue on remote node\\.|Can't read from request stream because client disconnected\\./i\n\nfunction classificationOf(message: string): FailureClassification {\n if (TERMINAL_FAILURE.test(message)) return 'terminal'\n if (TRANSIENT_FAILURE.test(message)) return 'transient'\n return 'unknown'\n}\n","import { classifyD1Error } from './classify-d1-error.ts'\n\n/** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyD1Error(cause, operation) ?? cause\n }\n}\n","import type { BatchItem } from 'drizzle-orm/batch'\n\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector, stageD1Statement } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/**\n * Shared plumbing for a repository that writes inside a D1 unit of work.\n * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.\n */\nexport abstract class BaseD1Repository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: D1Db<Schema>) {}\n\n /** Add a statement to the current D1 batch without executing it. */\n protected stage(statement: BatchItem<'sqlite'>): void {\n stageD1Statement(statement)\n }\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = sqliteTable(\n 'processed_messages',\n {\n messageId: text('message_id').primaryKey(),\n processedAt: integer('processed_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n },\n (table) => [index('idx_processed_messages_processed_at').on(table.processedAt)],\n)\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = sqliteTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: text('payload', { mode: 'json' }).$type<DomainEvent>().notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n availableAt: integer('available_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: integer('claim_expires_at', { mode: 'timestamp' }),\n publishedAt: integer('published_at', { mode: 'timestamp' }),\n failedAt: integer('failed_at', { mode: 'timestamp' }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(sql`${table.publishedAt} IS NULL`),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { eq } from 'drizzle-orm'\n\nimport { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { BaseD1Repository } from './base-d1-repository.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */\nexport class D1MessageStore<Schema extends DrizzleSchema>\n extends BaseD1Repository<Schema>\n implements IMessageStore\n{\n async claim(messageId: MessageId): Promise<void> {\n const processed = await this.read('claim message', () =>\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(eq(processedMessages.messageId, messageId))\n .get(),\n )\n if (processed !== undefined) throw new MessageAlreadyProcessedError(messageId)\n\n // Concurrent deliveries can pass the read; the primary key enforces the claim at commit.\n this.stage(this.db.insert(processedMessages).values({ messageId }))\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n this.stage(\n this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event }))),\n )\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { collectD1Statements, runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db, D1Tx } from './d1-database-types.ts'\n\n/**\n * The D1 transaction boundary: every write is staged during `work` and committed as one\n * `db.batch()`. A throw inside `work` means the batch never runs.\n */\nexport class D1BatchTransaction<Schema extends DrizzleSchema> implements ITransaction<\n D1Tx<Schema>\n> {\n constructor(private readonly db: D1Db<Schema>) {}\n\n async transaction<T>(work: (tx: D1Tx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('d1', async () => {\n const value = await work({ db: this.db })\n const [first, ...rest] = collectD1Statements()\n if (first !== undefined) {\n await executeDatabase('commit unit of work', () => this.db.batch([first, ...rest]))\n }\n return value\n })\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { panic } from "better-result";
3
+ import { v7 } from "uuid";
3
4
 
4
5
  //#region src/core/app-error.ts
5
6
  /**
@@ -360,6 +361,94 @@ function notImplemented(what) {
360
361
  return panic(`Not implemented: ${what}`);
361
362
  }
362
363
 
364
+ //#endregion
365
+ //#region src/core/identity.ts
366
+ /**
367
+ * Define an id kind the application mints: `export const CallId = defineId('CallId')`.
368
+ * An id another system mints, such as a provider's call id, stays a plain branded schema.
369
+ */
370
+ function defineId(brand) {
371
+ return {
372
+ schema: z.uuidv7({ error: `Invalid ${brand}` }).brand(),
373
+ create: () => v7()
374
+ };
375
+ }
376
+
377
+ //#endregion
378
+ //#region src/core/define-messages.ts
379
+ const MessageIdSchema = z.string().min(1);
380
+ /** One full message schema: the routing fields joined to the payload. */
381
+ function messageSchema(type, name, payload) {
382
+ return (type === "query" ? z.object({
383
+ type: z.literal(type),
384
+ name: z.literal(name)
385
+ }) : z.object({
386
+ type: z.literal(type),
387
+ name: z.literal(name),
388
+ id: MessageIdSchema
389
+ })).and(payload);
390
+ }
391
+ /** A union over every message of one kind. Empty when the application has none of that kind. */
392
+ function unionOf(type, schemas) {
393
+ return z.union(Object.entries(schemas).map(([name, payload]) => messageSchema(type, name, payload)));
394
+ }
395
+ /**
396
+ * Define an application's messages once. The key is the name; the factories add the routing
397
+ * fields `type`, `name`, and `id`. A payload that declares one of them is a compile error.
398
+ */
399
+ function defineMessages(definition) {
400
+ const commandSchema = unionOf("command", definition.commands);
401
+ const eventSchema = unionOf("event", definition.events);
402
+ const querySchema = unionOf("query", definition.queries);
403
+ return {
404
+ definition,
405
+ createCommand: (name, payload, id = v7()) => ({
406
+ ...payload,
407
+ type: "command",
408
+ name,
409
+ id
410
+ }),
411
+ createEvent: (name, payload, id = v7()) => ({
412
+ ...payload,
413
+ type: "event",
414
+ name,
415
+ id
416
+ }),
417
+ createQuery: (name, payload) => ({
418
+ ...payload,
419
+ type: "query",
420
+ name
421
+ }),
422
+ commandSchema: commandSchema.pipe(z.custom()),
423
+ eventSchema: eventSchema.pipe(z.custom()),
424
+ querySchema: querySchema.pipe(z.custom()),
425
+ queueSchema: z.union([commandSchema, eventSchema]).pipe(z.custom())
426
+ };
427
+ }
428
+
429
+ //#endregion
430
+ //#region src/core/entity.ts
431
+ /**
432
+ * The base of an aggregate: its identity, the events it raised, and the plain data a repository
433
+ * stores. The subclass keeps its constructor private and declares `static restore(data: Data)`,
434
+ * the one door a repository rebuilds it through. A repository drains the events after `save`.
435
+ */
436
+ var Entity = class {
437
+ events = [];
438
+ /** Record one event with the state change that caused it. The unit of work commits both. */
439
+ addEvent(event) {
440
+ this.events.push(event);
441
+ }
442
+ /** A copy of the events raised since the last `clearEvents`. */
443
+ collectEvents() {
444
+ return [...this.events];
445
+ }
446
+ /** Forget the raised events once the boundary has taken them. */
447
+ clearEvents() {
448
+ this.events = [];
449
+ }
450
+ };
451
+
363
452
  //#endregion
364
453
  //#region src/core/failure-classification.ts
365
454
  /** Decide whether one retry owner may repeat a failed operation. */
@@ -616,5 +705,5 @@ function createLogger(module, config) {
616
705
  }
617
706
 
618
707
  //#endregion
619
- export { AppError as A, ValidationError as C, StaleSessionError as D, NotAuthorizedError as E, RateLimitError as O, UpgradeRequiredError as S, ServiceUnavailableError as T, ValidationProblemDetailsSchema as _, createLogger as a, RequestTimeoutError as b, isAbortError as c, notImplemented as d, unreachable as f, ProblemDetailsSchema as g, sharedErrors as h, consoleSink as i, AuthenticationError as k, isTransientTransportError as l, defineErrors as m, Logger as n, setLogSink as o, DatabaseError as p, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, shouldRetryFailure as u, ValidationProblemErrorSchema as v, InternalServerError as w, ResourceNotFoundError as x, MalformedRequestError as y };
620
- //# sourceMappingURL=core-CnwWj4MY.js.map
708
+ export { StaleSessionError as A, RequestTimeoutError as C, InternalServerError as D, ValidationError as E, AuthenticationError as M, AppError as N, ServiceUnavailableError as O, MalformedRequestError as S, UpgradeRequiredError as T, defineErrors as _, createLogger as a, ValidationProblemDetailsSchema as b, isAbortError as c, Entity as d, defineMessages as f, DatabaseError as g, unreachable as h, consoleSink as i, RateLimitError as j, NotAuthorizedError as k, isTransientTransportError as l, notImplemented as m, Logger as n, setLogSink as o, defineId as p, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, shouldRetryFailure as u, sharedErrors as v, ResourceNotFoundError as w, ValidationProblemErrorSchema as x, ProblemDetailsSchema as y };
709
+ //# sourceMappingURL=core-Cx5SeyKV.js.map