better-ship 0.8.0 → 0.11.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 (88) hide show
  1. package/dist/{application-DYfcAy-Q.js → application-CIxo_BpK.js} +32 -3
  2. package/dist/application-CIxo_BpK.js.map +1 -0
  3. package/dist/application.d.ts +3 -3
  4. package/dist/application.js +2 -2
  5. package/dist/better-auth/apple-client-secret.d.ts +11 -4
  6. package/dist/better-auth/apple-client-secret.d.ts.map +1 -1
  7. package/dist/better-auth/apple-client-secret.js +39 -11
  8. package/dist/better-auth/apple-client-secret.js.map +1 -1
  9. package/dist/better-auth/auth-client.js +1 -1
  10. package/dist/better-auth/rate-limit.d.ts +21 -2
  11. package/dist/better-auth/rate-limit.d.ts.map +1 -1
  12. package/dist/better-auth/rate-limit.js +38 -2
  13. package/dist/better-auth/rate-limit.js.map +1 -1
  14. package/dist/better-auth.js +1 -1
  15. package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
  16. package/dist/cloudflare/auth-secondary-storage.js +1 -1
  17. package/dist/cloudflare/queue.d.ts +1 -1
  18. package/dist/cloudflare/queue.js +2 -2
  19. package/dist/cloudflare/scheduled.d.ts +36 -0
  20. package/dist/cloudflare/scheduled.d.ts.map +1 -0
  21. package/dist/cloudflare/scheduled.js +37 -0
  22. package/dist/cloudflare/scheduled.js.map +1 -0
  23. package/dist/cloudflare.d.ts +3 -3
  24. package/dist/cloudflare.js +3 -3
  25. package/dist/{core-DC_4pOrI.js → core-DG_cC8YT.js} +110 -2
  26. package/dist/{core-DC_4pOrI.js.map → core-DG_cC8YT.js.map} +1 -1
  27. package/dist/core.d.ts +2 -2
  28. package/dist/core.js +2 -2
  29. package/dist/http.d.ts +19 -0
  30. package/dist/http.d.ts.map +1 -0
  31. package/dist/http.js +35 -0
  32. package/dist/http.js.map +1 -0
  33. package/dist/{index-6XMPJu7u.d.ts → index-Bl0K1RQW.d.ts} +90 -2
  34. package/dist/index-Bl0K1RQW.d.ts.map +1 -0
  35. package/dist/{index-_Cihfq1J.d.ts → index-DgDyeR5s.d.ts} +135 -55
  36. package/dist/index-DgDyeR5s.d.ts.map +1 -0
  37. package/dist/postgres.d.ts +3 -3
  38. package/dist/postgres.js +3 -3
  39. package/dist/resend.d.ts +73 -0
  40. package/dist/resend.d.ts.map +1 -0
  41. package/dist/resend.js +284 -0
  42. package/dist/resend.js.map +1 -0
  43. package/dist/tanstack/server.d.ts +8 -0
  44. package/dist/tanstack/server.d.ts.map +1 -0
  45. package/dist/tanstack/server.js +16 -0
  46. package/dist/tanstack/server.js.map +1 -0
  47. package/dist/tanstack.d.ts +18 -14
  48. package/dist/tanstack.d.ts.map +1 -1
  49. package/dist/tanstack.js +17 -23
  50. package/dist/tanstack.js.map +1 -1
  51. package/dist/{unit-of-work-context-BFCmU92Z.js → unit-of-work-context-BPE1In2f.js} +2 -2
  52. package/dist/{unit-of-work-context-BFCmU92Z.js.map → unit-of-work-context-BPE1In2f.js.map} +1 -1
  53. package/dist/{unit-of-work-context-BWTJfYPW.d.ts → unit-of-work-context-BzmnjlDa.d.ts} +3 -3
  54. package/dist/{unit-of-work-context-BWTJfYPW.d.ts.map → unit-of-work-context-BzmnjlDa.d.ts.map} +1 -1
  55. package/dist/webhook-verification-BQ9qZnVK.js +18 -0
  56. package/dist/webhook-verification-BQ9qZnVK.js.map +1 -0
  57. package/dist/webhook-verification-sZtwTI5n.d.ts +29 -0
  58. package/dist/webhook-verification-sZtwTI5n.d.ts.map +1 -0
  59. package/package.json +11 -6
  60. package/src/application/email-service.ts +55 -0
  61. package/src/application/index.ts +16 -0
  62. package/src/application/record-email-event.ts +53 -0
  63. package/src/core/email-address.ts +7 -0
  64. package/src/core/email-deliverability.ts +31 -0
  65. package/src/core/email-idempotency-key.ts +34 -0
  66. package/src/core/email-service.error.ts +51 -0
  67. package/src/core/index.ts +18 -0
  68. package/src/core/record-email-event.ts +23 -0
  69. package/src/infrastructure/better-auth/apple-client-secret.ts +41 -14
  70. package/src/infrastructure/better-auth/rate-limit.ts +44 -1
  71. package/src/infrastructure/cloudflare/scheduled/index.ts +8 -0
  72. package/src/infrastructure/cloudflare/scheduled/scheduled-handler.ts +62 -0
  73. package/src/infrastructure/http/index.ts +6 -0
  74. package/src/infrastructure/http/webhook-handler.ts +35 -0
  75. package/src/infrastructure/http/webhook-verification.ts +27 -0
  76. package/src/infrastructure/resend/email-event.ts +142 -0
  77. package/src/infrastructure/resend/index.ts +3 -0
  78. package/src/infrastructure/resend/resend-email-service.ts +125 -0
  79. package/src/infrastructure/resend/resend-webhook.ts +70 -0
  80. package/src/infrastructure/tanstack/auth.middleware.ts +14 -6
  81. package/src/infrastructure/tanstack/error.middleware.ts +7 -8
  82. package/src/infrastructure/tanstack/index.ts +0 -1
  83. package/src/infrastructure/tanstack/request-context.ts +11 -0
  84. package/src/infrastructure/tanstack/server.ts +7 -0
  85. package/src/infrastructure/tanstack/session-cookies.ts +6 -4
  86. package/dist/application-DYfcAy-Q.js.map +0 -1
  87. package/dist/index-6XMPJu7u.d.ts.map +0 -1
  88. package/dist/index-_Cihfq1J.d.ts.map +0 -1
@@ -1,6 +1,35 @@
1
- import { E as DatabaseError, T as unreachable, U as AppError, a as createLogger } from "./core-DC_4pOrI.js";
1
+ import { E as DatabaseError, Q as AppError, T as unreachable, a as createLogger } from "./core-DG_cC8YT.js";
2
2
  import { panic } from "better-result";
3
3
 
4
+ //#region src/application/email-service.ts
5
+ /** Records every send for a test. Provider ids count up as `recorded-1`, `recorded-2`. */
6
+ var RecordingEmailService = class {
7
+ /** Sends in call order, including repeated idempotency keys. */
8
+ sent = [];
9
+ /** Record the send before returning its provider id. */
10
+ async sendEmail(email) {
11
+ this.sent.push(email);
12
+ return { providerId: `recorded-${this.sent.length}` };
13
+ }
14
+ };
15
+
16
+ //#endregion
17
+ //#region src/application/record-email-event.ts
18
+ /**
19
+ * Record a bounce or a complaint for every recipient inside the command's unit of work.
20
+ * The command id is the provider's event id, so a redelivered webhook is `MessageAlreadyProcessedError`.
21
+ */
22
+ async function recordEmailEvent(command, deps) {
23
+ await deps.uow.run(command.id, async ({ emailDeliverability }) => {
24
+ for (const address of command.recipients) if (command.event.type === "bounced") await emailDeliverability.recordBounce(address, {
25
+ type: command.event.bounceType,
26
+ subType: command.event.bounceSubType
27
+ });
28
+ else await emailDeliverability.recordComplaint(address);
29
+ });
30
+ }
31
+
32
+ //#endregion
4
33
  //#region src/application/message-already-processed.error.ts
5
34
  /** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */
6
35
  var MessageAlreadyProcessedError = class MessageAlreadyProcessedError extends AppError {
@@ -127,5 +156,5 @@ var UnitOfWorkAlreadyActiveError = class extends AppError {
127
156
  };
128
157
 
129
158
  //#endregion
130
- export { MessageAlreadyProcessedError as i, UnitOfWork as n, MessageBus as r, UnitOfWorkAlreadyActiveError as t };
131
- //# sourceMappingURL=application-DYfcAy-Q.js.map
159
+ export { recordEmailEvent as a, MessageAlreadyProcessedError as i, UnitOfWork as n, RecordingEmailService as o, MessageBus as r, UnitOfWorkAlreadyActiveError as t };
160
+ //# sourceMappingURL=application-CIxo_BpK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-CIxo_BpK.js","names":[],"sources":["../src/application/email-service.ts","../src/application/record-email-event.ts","../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 type { EmailIdempotencyKey } from '@/core'\n\n/** A media type such as `application/pdf`. */\nexport type MimeType = `${string}/${string}`\n\n/** A file to attach. Binary content is bytes; the adapter encodes it for the provider. */\nexport type EmailAttachment = {\n readonly filename: string\n readonly content: string | Uint8Array\n /** Optional MIME metadata; Resend accepts filename and content without it. */\n readonly contentType?: MimeType\n}\n\n/**\n * One transactional email. Resend accepts 50 recipients, 75 tags with ASCII names and values,\n * and 40 MB of attachments after base64. Sources: https://resend.com/docs/api-reference/emails/send-email\n * and https://resend.com/docs/dashboard/emails/tags\n */\nexport type OutboundEmail = {\n readonly to: string | readonly string[]\n readonly subject: string\n readonly html: string\n /** The stable identity of this send. Resend deduplicates it for 24 hours. */\n readonly idempotencyKey: EmailIdempotencyKey\n /** Resend generates plain text from html when omitted. An empty string disables that generation. */\n readonly text?: string\n readonly replyTo?: string\n /** Labels the provider stores with the email and returns on every delivery event. */\n readonly tags?: Readonly<Record<string, string>>\n readonly attachments?: readonly EmailAttachment[]\n}\n\n/** The provider accepted the email and assigned its id. */\nexport type EmailAccepted = { readonly providerId: string }\n\n/**\n * The port a handler sends through. The adapter owns the sender address and the provider error mapping.\n * A provider rejection throws `EmailServiceError`; unexpected SDK rejections propagate unchanged.\n */\nexport interface IEmailService {\n /** Resolve after provider acceptance; this does not confirm delivery to the recipient. */\n sendEmail(email: OutboundEmail): Promise<EmailAccepted>\n}\n\n/** Records every send for a test. Provider ids count up as `recorded-1`, `recorded-2`. */\nexport class RecordingEmailService implements IEmailService {\n /** Sends in call order, including repeated idempotency keys. */\n readonly sent: OutboundEmail[] = []\n\n /** Record the send before returning its provider id. */\n async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {\n this.sent.push(email)\n return { providerId: `recorded-${this.sent.length}` }\n }\n}\n","import type { BounceType, DomainCommand, EmailAddress, RecordEmailEventPayload } from '@/core'\n\nimport type { IUnitOfWork } from './unit-of-work.ts'\n\n/** What a bounce records for one address. */\nexport type EmailBounceRecord = {\n readonly type: BounceType\n readonly subType: string | null\n}\n\n/**\n * The application's deliverability state per address. An implementation keeps the higher bounce\n * severity with `shouldUpdateBounce`, and treats an unknown address as nothing to record.\n */\nexport interface IEmailDeliverabilityStore {\n /** Keep the higher severity atomically. Equal severity permits replacement; the store owns timestamp ordering. */\n recordBounce(address: EmailAddress, bounce: EmailBounceRecord): Promise<void>\n /** Record a complaint without clearing existing suppression state. */\n recordComplaint(address: EmailAddress): Promise<void>\n}\n\n/** The repositories the handler needs from the unit of work. */\nexport type RecordEmailEventRepos = { readonly emailDeliverability: IEmailDeliverabilityStore }\n\n/** The container slice the handler reads. */\nexport type RecordEmailEventDeps = { readonly uow: IUnitOfWork<RecordEmailEventRepos> }\n\n/** The command, under whatever name the application registered the payload. */\nexport type RecordEmailEventCommand = DomainCommand & RecordEmailEventPayload\n\n/**\n * Record a bounce or a complaint for every recipient inside the command's unit of work.\n * The command id is the provider's event id, so a redelivered webhook is `MessageAlreadyProcessedError`.\n */\nexport async function recordEmailEvent(\n command: RecordEmailEventCommand,\n deps: RecordEmailEventDeps,\n): Promise<void> {\n await deps.uow.run(command.id, async ({ emailDeliverability }) => {\n for (const address of command.recipients) {\n if (command.event.type === 'bounced') {\n // oxlint-disable-next-line no-await-in-loop -- Finish each transaction write before starting another; a failure must leave no pending writes.\n await emailDeliverability.recordBounce(address, {\n type: command.event.bounceType,\n subType: command.event.bounceSubType,\n })\n } else {\n // oxlint-disable-next-line no-await-in-loop -- A rejected write must stop the transaction before another write starts.\n await emailDeliverability.recordComplaint(address)\n }\n }\n })\n}\n","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,3 +1,3 @@
1
- import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, q as MessageId } from "./index-6XMPJu7u.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-_Cihfq1J.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
+ import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, q as MessageId } from "./index-Bl0K1RQW.js";
2
+ import { A as RecordingEmailService, C as IMessageStore, D as IEmailService, E as EmailAttachment, M as EventHandler, N as QueryHandler, O as MimeType, S as IOutboxRelay, T as EmailAccepted, _ as recordEmailEvent, a as CommandRegistry, b as UnitOfWorkDeps, c as MessageRegistry, d as RegistryMessage, f as EmailBounceRecord, g as RecordEmailEventRepos, h as RecordEmailEventDeps, i as MessageBus, j as CommandHandler, k as OutboundEmail, l as MessageResult, m as RecordEmailEventCommand, n as MessageAlreadyProcessedError, o as EventRegistry, p as IEmailDeliverabilityStore, r as IMessageBus, s as Handlers, t as UnitOfWorkAlreadyActiveError, u as QueryRegistry, v as IUnitOfWork, w as IEventCollector, x as ITransaction, y as UnitOfWork } from "./index-DgDyeR5s.js";
3
+ export { type CommandHandler, type CommandRegistry, type DomainCommand, type DomainEvent, type DomainMessage, type DomainQuery, type EmailAccepted, type EmailAttachment, type EmailBounceRecord, type EventHandler, type EventRegistry, type Handlers, type IEmailDeliverabilityStore, type IEmailService, type IEventCollector, type IMessageBus, type IMessageStore, type IOutboxRelay, type ITransaction, type IUnitOfWork, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type MimeType, type OutboundEmail, type QueryHandler, type QueryRegistry, type RecordEmailEventCommand, type RecordEmailEventDeps, type RecordEmailEventRepos, RecordingEmailService, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps, recordEmailEvent };
@@ -1,3 +1,3 @@
1
- import { i as MessageAlreadyProcessedError, n as UnitOfWork, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-DYfcAy-Q.js";
1
+ import { a as recordEmailEvent, i as MessageAlreadyProcessedError, n as UnitOfWork, o as RecordingEmailService, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-CIxo_BpK.js";
2
2
 
3
- export { MessageAlreadyProcessedError, MessageBus, UnitOfWork, UnitOfWorkAlreadyActiveError };
3
+ export { MessageAlreadyProcessedError, MessageBus, RecordingEmailService, UnitOfWork, UnitOfWorkAlreadyActiveError, recordEmailEvent };
@@ -1,6 +1,9 @@
1
- import { kt as AppError } from "../index-6XMPJu7u.js";
1
+ import { Ut as AppError } from "../index-Bl0K1RQW.js";
2
2
  //#region src/infrastructure/better-auth/apple-client-secret.d.ts
3
- /** Apple sends its OAuth callback from this origin. Include it in trustedOrigins when enabling Apple. */
3
+ /**
4
+ * Apple's issuer and OAuth callback origin, and the `aud` claim of the client secret. Include it in
5
+ * trustedOrigins when enabling Apple. Source: the same Apple page as `SECRET_LIFETIME_SECONDS`.
6
+ */
4
7
  export declare const APPLE_ORIGIN = "https://appleid.apple.com";
5
8
  /** Apple credentials used to sign a client secret. Ported from Porte's apple-client-secret.ts. */
6
9
  export type AppleKeyConfig = {
@@ -15,7 +18,11 @@ export declare class AppleClientSecretError extends AppError {
15
18
  readonly classification = "terminal";
16
19
  constructor(cause: unknown);
17
20
  }
18
- /** Sign Apple's six-month client secret. Invalid private keys return an empty secret and log once per isolate, as in Typist. */
19
- export declare function generateAppleClientSecret(config: AppleKeyConfig): Promise<string>;
21
+ /**
22
+ * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.
23
+ * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.
24
+ * Invalid private keys return an empty secret and log once per isolate, as in Typist.
25
+ */
26
+ export declare function generateAppleClientSecret(config: AppleKeyConfig): string;
20
27
  //#endregion
21
28
  //# sourceMappingURL=apple-client-secret.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apple-client-secret.d.ts","names":[],"sources":["../../src/infrastructure/better-auth/apple-client-secret.ts"],"mappings":";;;qBASa;;YAGD;WACD;WACA;WACA;WACA;;;qBAIE,+BAA+B;WACjC;WACA;EAET,YAAY;;;wBAMQ,0BAA0B,QAAQ,iBAAiB"}
1
+ {"version":3,"file":"apple-client-secret.d.ts","names":[],"sources":["../../src/infrastructure/better-auth/apple-client-secret.ts"],"mappings":";;;;;;qBAkBa;;YAGD;WACD;WACA;WACA;WACA;;;qBAIE,+BAA+B;WACjC;WACA;EAET,YAAY;;;;;;;wBAiBE,0BAA0B,QAAQ"}
@@ -1,10 +1,19 @@
1
- import { U as AppError, a as createLogger } from "../core-DC_4pOrI.js";
2
- import { SignJWT, importPKCS8 } from "jose";
1
+ import { Q as AppError, a as createLogger } from "../core-DG_cC8YT.js";
2
+ import { Buffer } from "node:buffer";
3
+ import { createPrivateKey, sign } from "node:crypto";
3
4
 
4
5
  //#region src/infrastructure/better-auth/apple-client-secret.ts
5
6
  const logger = createLogger("auth");
6
7
  let appleKeyFailureLogged = false;
7
- /** Apple sends its OAuth callback from this origin. Include it in trustedOrigins when enabling Apple. */
8
+ /**
9
+ * Apple caps `exp` at 15,777,000 seconds after `iat`; 180 days stays under it. Source:
10
+ * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
11
+ */
12
+ const SECRET_LIFETIME_SECONDS = 15552e3;
13
+ /**
14
+ * Apple's issuer and OAuth callback origin, and the `aud` claim of the client secret. Include it in
15
+ * trustedOrigins when enabling Apple. Source: the same Apple page as `SECRET_LIFETIME_SECONDS`.
16
+ */
8
17
  const APPLE_ORIGIN = "https://appleid.apple.com";
9
18
  /** Apple signing failed. The application's error boundary owns reporting. */
10
19
  var AppleClientSecretError = class extends AppError {
@@ -14,11 +23,18 @@ var AppleClientSecretError = class extends AppError {
14
23
  super("Apple client secret signing failed", { cause });
15
24
  }
16
25
  };
17
- /** Sign Apple's six-month client secret. Invalid private keys return an empty secret and log once per isolate, as in Typist. */
18
- async function generateAppleClientSecret(config) {
19
- let key;
26
+ function base64UrlJson(segment) {
27
+ return Buffer.from(JSON.stringify(segment)).toString("base64url");
28
+ }
29
+ /**
30
+ * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.
31
+ * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.
32
+ * Invalid private keys return an empty secret and log once per isolate, as in Typist.
33
+ */
34
+ function generateAppleClientSecret(config) {
35
+ const key = config.applePrivateKey.replace(/\\n/g, "\n");
20
36
  try {
21
- key = await importPKCS8(config.applePrivateKey.replace(/\\n/g, "\n"), "ES256");
37
+ createPrivateKey(key);
22
38
  } catch (error) {
23
39
  if (!appleKeyFailureLogged) {
24
40
  appleKeyFailureLogged = true;
@@ -29,11 +45,23 @@ async function generateAppleClientSecret(config) {
29
45
  }
30
46
  return "";
31
47
  }
48
+ const issuedAt = Math.floor(Date.now() / 1e3);
49
+ const input = `${base64UrlJson({
50
+ alg: "ES256",
51
+ kid: config.appleKeyId
52
+ })}.${base64UrlJson({
53
+ iss: config.appleTeamId,
54
+ sub: config.appleClientId,
55
+ aud: APPLE_ORIGIN,
56
+ iat: issuedAt,
57
+ exp: issuedAt + SECRET_LIFETIME_SECONDS
58
+ })}`;
32
59
  try {
33
- return await new SignJWT({}).setProtectedHeader({
34
- alg: "ES256",
35
- kid: config.appleKeyId
36
- }).setIssuer(config.appleTeamId).setSubject(config.appleClientId).setAudience(APPLE_ORIGIN).setIssuedAt().setExpirationTime("180d").sign(key);
60
+ const signature = sign("sha256", Buffer.from(input), {
61
+ key,
62
+ dsaEncoding: "ieee-p1363"
63
+ });
64
+ return `${input}.${Buffer.from(signature).toString("base64url")}`;
37
65
  } catch (cause) {
38
66
  throw new AppleClientSecretError(cause);
39
67
  }
@@ -1 +1 @@
1
- {"version":3,"file":"apple-client-secret.js","names":[],"sources":["../../src/infrastructure/better-auth/apple-client-secret.ts"],"sourcesContent":["import { importPKCS8, SignJWT } from 'jose'\n\nimport { AppError, createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n// Typist reports an invalid key once per isolate because session checks repeatedly construct auth options.\nlet appleKeyFailureLogged = false\n\n/** Apple sends its OAuth callback from this origin. Include it in trustedOrigins when enabling Apple. */\nexport const APPLE_ORIGIN = 'https://appleid.apple.com'\n\n/** Apple credentials used to sign a client secret. Ported from Porte's apple-client-secret.ts. */\nexport type AppleKeyConfig = {\n readonly appleClientId: string\n readonly appleTeamId: string\n readonly appleKeyId: string\n readonly applePrivateKey: string\n}\n\n/** Apple signing failed. The application's error boundary owns reporting. */\nexport class AppleClientSecretError extends AppError {\n readonly _tag = 'AppleClientSecretError'\n readonly classification = 'terminal'\n\n constructor(cause: unknown) {\n super('Apple client secret signing failed', { cause })\n }\n}\n\n/** Sign Apple's six-month client secret. Invalid private keys return an empty secret and log once per isolate, as in Typist. */\nexport async function generateAppleClientSecret(config: AppleKeyConfig): Promise<string> {\n let key: Awaited<ReturnType<typeof importPKCS8>>\n try {\n key = await importPKCS8(config.applePrivateKey.replace(/\\\\n/g, '\\n'), 'ES256')\n } catch (error) {\n if (!appleKeyFailureLogged) {\n appleKeyFailureLogged = true\n logger.error('apple_private_key_invalid', { error, details: { keyId: config.appleKeyId } })\n }\n return ''\n }\n try {\n return await new SignJWT({})\n .setProtectedHeader({ alg: 'ES256', kid: config.appleKeyId })\n .setIssuer(config.appleTeamId)\n .setSubject(config.appleClientId)\n .setAudience(APPLE_ORIGIN)\n .setIssuedAt()\n .setExpirationTime('180d')\n .sign(key)\n } catch (cause) {\n throw new AppleClientSecretError(cause)\n }\n}\n"],"mappings":";;;;AAIA,MAAM,SAAS,aAAa,MAAM;AAElC,IAAI,wBAAwB;;AAG5B,MAAa,eAAe;;AAW5B,IAAa,yBAAb,cAA4C,SAAS;CACnD,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YAAY,OAAgB;EAC1B,MAAM,sCAAsC,EAAE,MAAM,CAAC;CACvD;AACF;;AAGA,eAAsB,0BAA0B,QAAyC;CACvF,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,YAAY,OAAO,gBAAgB,QAAQ,QAAQ,IAAI,GAAG,OAAO;CAC/E,SAAS,OAAO;EACd,IAAI,CAAC,uBAAuB;GAC1B,wBAAwB;GACxB,OAAO,MAAM,6BAA6B;IAAE;IAAO,SAAS,EAAE,OAAO,OAAO,WAAW;GAAE,CAAC;EAC5F;EACA,OAAO;CACT;CACA,IAAI;EACF,OAAO,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CACzB,mBAAmB;GAAE,KAAK;GAAS,KAAK,OAAO;EAAW,CAAC,CAAC,CAC5D,UAAU,OAAO,WAAW,CAAC,CAC7B,WAAW,OAAO,aAAa,CAAC,CAChC,YAAY,YAAY,CAAC,CACzB,YAAY,CAAC,CACb,kBAAkB,MAAM,CAAC,CACzB,KAAK,GAAG;CACb,SAAS,OAAO;EACd,MAAM,IAAI,uBAAuB,KAAK;CACxC;AACF"}
1
+ {"version":3,"file":"apple-client-secret.js","names":[],"sources":["../../src/infrastructure/better-auth/apple-client-secret.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\nimport { createPrivateKey, sign } from 'node:crypto'\n\nimport { AppError, createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n// Typist reports an invalid key once per isolate because session checks repeatedly construct auth options.\nlet appleKeyFailureLogged = false\n/**\n * Apple caps `exp` at 15,777,000 seconds after `iat`; 180 days stays under it. Source:\n * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret\n */\nconst SECRET_LIFETIME_SECONDS = 180 * 24 * 60 * 60\n\n/**\n * Apple's issuer and OAuth callback origin, and the `aud` claim of the client secret. Include it in\n * trustedOrigins when enabling Apple. Source: the same Apple page as `SECRET_LIFETIME_SECONDS`.\n */\nexport const APPLE_ORIGIN = 'https://appleid.apple.com'\n\n/** Apple credentials used to sign a client secret. Ported from Porte's apple-client-secret.ts. */\nexport type AppleKeyConfig = {\n readonly appleClientId: string\n readonly appleTeamId: string\n readonly appleKeyId: string\n readonly applePrivateKey: string\n}\n\n/** Apple signing failed. The application's error boundary owns reporting. */\nexport class AppleClientSecretError extends AppError {\n readonly _tag = 'AppleClientSecretError'\n readonly classification = 'terminal'\n\n constructor(cause: unknown) {\n super('Apple client secret signing failed', { cause })\n }\n}\n\n/** One JWS segment before encoding: the protected header or the claims. */\ntype JwsSegment = Readonly<Record<string, string | number>>\n\nfunction base64UrlJson(segment: JwsSegment): string {\n return Buffer.from(JSON.stringify(segment)).toString('base64url')\n}\n\n/**\n * Sign Apple's six-month client secret synchronously, so Better Auth options can be built without awaiting it.\n * Signing uses `node:crypto` as Typist does, so the Worker needs the `nodejs_compat` flag.\n * Invalid private keys return an empty secret and log once per isolate, as in Typist.\n */\nexport function generateAppleClientSecret(config: AppleKeyConfig): string {\n const key = config.applePrivateKey.replace(/\\\\n/g, '\\n')\n // Validation only: workerd's sign() takes the PEM string, not the KeyObject.\n try {\n createPrivateKey(key)\n } catch (error) {\n if (!appleKeyFailureLogged) {\n appleKeyFailureLogged = true\n logger.error('apple_private_key_invalid', { error, details: { keyId: config.appleKeyId } })\n }\n return ''\n }\n const issuedAt = Math.floor(Date.now() / 1000)\n const header = base64UrlJson({ alg: 'ES256', kid: config.appleKeyId })\n const claims = base64UrlJson({\n iss: config.appleTeamId,\n sub: config.appleClientId,\n aud: APPLE_ORIGIN,\n iat: issuedAt,\n exp: issuedAt + SECRET_LIFETIME_SECONDS,\n })\n const input = `${header}.${claims}`\n try {\n // A JWS carries the raw r||s signature; OpenSSL emits DER unless told otherwise.\n const signature = sign('sha256', Buffer.from(input), { key, dsaEncoding: 'ieee-p1363' })\n // TypeScript 7 types the returned buffer without an encoding parameter on toString.\n return `${input}.${Buffer.from(signature).toString('base64url')}`\n } catch (cause) {\n throw new AppleClientSecretError(cause)\n }\n}\n"],"mappings":";;;;;AAKA,MAAM,SAAS,aAAa,MAAM;AAElC,IAAI,wBAAwB;;;;;AAK5B,MAAM,0BAA0B;;;;;AAMhC,MAAa,eAAe;;AAW5B,IAAa,yBAAb,cAA4C,SAAS;CACnD,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YAAY,OAAgB;EAC1B,MAAM,sCAAsC,EAAE,MAAM,CAAC;CACvD;AACF;AAKA,SAAS,cAAc,SAA6B;CAClD,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,SAAS,WAAW;AAClE;;;;;;AAOA,SAAgB,0BAA0B,QAAgC;CACxE,MAAM,MAAM,OAAO,gBAAgB,QAAQ,QAAQ,IAAI;CAEvD,IAAI;EACF,iBAAiB,GAAG;CACtB,SAAS,OAAO;EACd,IAAI,CAAC,uBAAuB;GAC1B,wBAAwB;GACxB,OAAO,MAAM,6BAA6B;IAAE;IAAO,SAAS,EAAE,OAAO,OAAO,WAAW;GAAE,CAAC;EAC5F;EACA,OAAO;CACT;CACA,MAAM,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAS7C,MAAM,QAAQ,GARC,cAAc;EAAE,KAAK;EAAS,KAAK,OAAO;CAAW,CAQ9C,EAAE,GAPT,cAAc;EAC3B,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK;EACL,KAAK;EACL,KAAK,WAAW;CAClB,CACgC;CAChC,IAAI;EAEF,MAAM,YAAY,KAAK,UAAU,OAAO,KAAK,KAAK,GAAG;GAAE;GAAK,aAAa;EAAa,CAAC;EAEvF,OAAO,GAAG,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,WAAW;CAChE,SAAS,OAAO;EACd,MAAM,IAAI,uBAAuB,KAAK;CACxC;AACF"}
@@ -1,4 +1,4 @@
1
- import { V as RateLimitError } from "../core-DC_4pOrI.js";
1
+ import { V as RateLimitError } from "../core-DG_cC8YT.js";
2
2
  import { createAuthClient } from "better-auth/client";
3
3
 
4
4
  //#region src/infrastructure/better-auth/auth-client.ts
@@ -1,12 +1,31 @@
1
- import { kt as AppError } from "../index-6XMPJu7u.js";
2
- import { BetterAuthRateLimitStorage } from "better-auth";
1
+ import { Ut as AppError } from "../index-Bl0K1RQW.js";
2
+ import { BetterAuthOptions, BetterAuthRateLimitStorage } from "better-auth";
3
3
  //#region src/infrastructure/better-auth/rate-limit.d.ts
4
+ /** A rule Better Auth passes to storage: a window in seconds and the requests it allows. */
5
+ export type AuthRateLimitRule = {
6
+ readonly window: number;
7
+ readonly max: number;
8
+ };
4
9
  /** Each rule must match its binding's deployed limit and period. Cloudflare accepts only 10 or 60 seconds. */
5
10
  export type AuthRateLimitBinding = {
6
11
  readonly binding: Pick<RateLimit, 'limit'>;
7
12
  readonly window: 10 | 60;
8
13
  readonly max: number;
9
14
  };
15
+ /**
16
+ * Better Auth's built-in rules, which it does not export. Sign-in, sign-up, change-password, and
17
+ * change-email paths get 3 requests per 10 seconds. Password reset, verification email, forget-password,
18
+ * and email OTP send paths get 3 per 60 seconds. A package test pins both against the installed version.
19
+ * Source: `getDefaultSpecialRules` in
20
+ * https://github.com/better-auth/better-auth/blob/v1.7.4/packages/better-auth/src/api/rate-limiter/index.ts
21
+ */
22
+ export declare const BUILT_IN_AUTH_RATE_LIMIT_RULES: readonly AuthRateLimitRule[];
23
+ /**
24
+ * Every rule the configured instance can pass to storage, deduplicated: the general rule, the built-in
25
+ * rules, each plugin's rules, and static custom rules. Function-valued custom rules decide per request
26
+ * and are not listed. Disabled rate limiting returns no rules.
27
+ */
28
+ export declare function authRateLimitRules(options: Pick<BetterAuthOptions, 'rateLimit' | 'plugins'>): readonly AuthRateLimitRule[];
10
29
  /** A binding failure with no known retry classification. The cause preserves the original failure. */
11
30
  export declare class AuthRateLimitError extends AppError {
12
31
  readonly _tag = "AuthRateLimitError";
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.d.ts","names":[],"sources":["../../src/infrastructure/better-auth/rate-limit.ts"],"mappings":";;;;YAKY;WACD,SAAS,KAAK;WACd;WACA;;;qBAIE,2BAA2B;WAC7B;WACA;EAET,YAAY;;;qBAMD,0CAA0C;WAK1C;WACA;WALF;WACA;EAET,YACW,gBACA;;;YAOD;WACD;;;;;;wBAOK,2BACd,mBAAmB,0BACjB,gBAAyB,uBAC1B"}
1
+ {"version":3,"file":"rate-limit.d.ts","names":[],"sources":["../../src/infrastructure/better-auth/rate-limit.ts"],"mappings":";;;;YAKY;WACD;WACA;;;YAIC;WACD,SAAS,KAAK;WACd;WACA;;;;;;;;;qBAUE,yCAAyC;;;;;;wBAUtC,mBACd,SAAS,KAAK,uDACJ;;qBAmBC,2BAA2B;WAC7B;WACA;EAET,YAAY;;;qBAMD,0CAA0C;WAK1C;WACA;WALF;WACA;EAET,YACW,gBACA;;;YAOD;WACD;;;;;;wBAOK,2BACd,mBAAmB,0BACjB,gBAAyB,uBAC1B"}
@@ -1,6 +1,42 @@
1
- import { U as AppError, a as createLogger } from "../core-DC_4pOrI.js";
1
+ import { Q as AppError, a as createLogger } from "../core-DG_cC8YT.js";
2
2
 
3
3
  //#region src/infrastructure/better-auth/rate-limit.ts
4
+ /**
5
+ * Better Auth's built-in rules, which it does not export. Sign-in, sign-up, change-password, and
6
+ * change-email paths get 3 requests per 10 seconds. Password reset, verification email, forget-password,
7
+ * and email OTP send paths get 3 per 60 seconds. A package test pins both against the installed version.
8
+ * Source: `getDefaultSpecialRules` in
9
+ * https://github.com/better-auth/better-auth/blob/v1.7.4/packages/better-auth/src/api/rate-limiter/index.ts
10
+ */
11
+ const BUILT_IN_AUTH_RATE_LIMIT_RULES = [{
12
+ window: 10,
13
+ max: 3
14
+ }, {
15
+ window: 60,
16
+ max: 3
17
+ }];
18
+ /**
19
+ * Every rule the configured instance can pass to storage, deduplicated: the general rule, the built-in
20
+ * rules, each plugin's rules, and static custom rules. Function-valued custom rules decide per request
21
+ * and are not listed. Disabled rate limiting returns no rules.
22
+ */
23
+ function authRateLimitRules(options) {
24
+ const rateLimit = options.rateLimit;
25
+ if (rateLimit?.enabled === false) return [];
26
+ const rules = [{
27
+ window: rateLimit?.window ?? 10,
28
+ max: rateLimit?.max ?? 100
29
+ }, ...BUILT_IN_AUTH_RATE_LIMIT_RULES];
30
+ for (const plugin of options.plugins ?? []) for (const rule of plugin.rateLimit ?? []) rules.push({
31
+ window: rule.window,
32
+ max: rule.max
33
+ });
34
+ for (const rule of Object.values(rateLimit?.customRules ?? {})) if (rule !== false && !(rule instanceof Function)) rules.push({
35
+ window: rule.window,
36
+ max: rule.max
37
+ });
38
+ return [...new Map(rules.map((rule) => [`${rule.window}:${rule.max}`, rule])).values()];
39
+ }
4
40
  /** A binding failure with no known retry classification. The cause preserves the original failure. */
5
41
  var AuthRateLimitError = class extends AppError {
6
42
  _tag = "AuthRateLimitError";
@@ -55,5 +91,5 @@ function createAuthRateLimitStorage(bindings, { failureMode = "allow" } = {}) {
55
91
  }
56
92
 
57
93
  //#endregion
58
- export { AuthRateLimitError, UnsupportedAuthRateLimitRuleError, createAuthRateLimitStorage };
94
+ export { AuthRateLimitError, BUILT_IN_AUTH_RATE_LIMIT_RULES, UnsupportedAuthRateLimitRuleError, authRateLimitRules, createAuthRateLimitStorage };
59
95
  //# sourceMappingURL=rate-limit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.js","names":[],"sources":["../../src/infrastructure/better-auth/rate-limit.ts"],"sourcesContent":["import type { BetterAuthRateLimitStorage } from 'better-auth'\n\nimport { AppError, createLogger } from '@/core'\n\n/** Each rule must match its binding's deployed limit and period. Cloudflare accepts only 10 or 60 seconds. */\nexport type AuthRateLimitBinding = {\n readonly binding: Pick<RateLimit, 'limit'>\n readonly window: 10 | 60\n readonly max: number\n}\n\n/** A binding failure with no known retry classification. The cause preserves the original failure. */\nexport class AuthRateLimitError extends AppError {\n readonly _tag = 'AuthRateLimitError'\n readonly classification = 'unknown'\n\n constructor(cause: unknown) {\n super('Authentication rate limiter is unavailable', { cause })\n }\n}\n\n/** No configured binding matches this Better Auth rule. Outage policy cannot bypass a configuration error. */\nexport class UnsupportedAuthRateLimitRuleError extends AppError {\n readonly _tag = 'UnsupportedAuthRateLimitRuleError'\n readonly classification = 'terminal'\n\n constructor(\n readonly window: number,\n readonly max: number,\n ) {\n super('No Cloudflare binding supports the authentication rate limit rule')\n }\n}\n\n/** Typist allows requests during binding outages. Select block to propagate an error instead. */\nexport type AuthRateLimitOptions = {\n readonly failureMode?: 'allow' | 'block'\n}\n\n/**\n * Port Typist and Porte's binding adapters with explicit rule matching and configurable outage behavior.\n * Bindings enforce approximate limits per Cloudflare location; retryAfter is a conservative full window.\n */\nexport function createAuthRateLimitStorage(\n bindings: readonly AuthRateLimitBinding[],\n { failureMode = 'allow' }: AuthRateLimitOptions = {},\n): BetterAuthRateLimitStorage {\n const logger = createLogger('auth-rate-limit')\n return {\n async consume(key, rule) {\n const configured = bindings.find(\n (entry) => entry.window === rule.window && entry.max === rule.max,\n )\n if (configured === undefined)\n throw new UnsupportedAuthRateLimitRuleError(rule.window, rule.max)\n try {\n const { success } = await configured.binding.limit({ key })\n return { allowed: success, retryAfter: success ? null : rule.window }\n } catch (cause) {\n if (failureMode === 'block') throw new AuthRateLimitError(cause)\n logger.error('rate_limit_binding_failed', {\n error: cause,\n details: { window: rule.window, max: rule.max, failureMode },\n })\n return { allowed: true, retryAfter: null }\n }\n },\n }\n}\n"],"mappings":";;;;AAYA,IAAa,qBAAb,cAAwC,SAAS;CAC/C,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YAAY,OAAgB;EAC1B,MAAM,8CAA8C,EAAE,MAAM,CAAC;CAC/D;AACF;;AAGA,IAAa,oCAAb,cAAuD,SAAS;CAKnD;CACA;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YACE,AAAS,QACT,AAAS,KACT;EACA,MAAM,mEAAmE;EAHhE;EACA;CAGX;AACF;;;;;AAWA,SAAgB,2BACd,UACA,EAAE,cAAc,YAAkC,CAAC,GACvB;CAC5B,MAAM,SAAS,aAAa,iBAAiB;CAC7C,OAAO,EACL,MAAM,QAAQ,KAAK,MAAM;EACvB,MAAM,aAAa,SAAS,MACzB,UAAU,MAAM,WAAW,KAAK,UAAU,MAAM,QAAQ,KAAK,GAChE;EACA,IAAI,eAAe,QACjB,MAAM,IAAI,kCAAkC,KAAK,QAAQ,KAAK,GAAG;EACnE,IAAI;GACF,MAAM,EAAE,YAAY,MAAM,WAAW,QAAQ,MAAM,EAAE,IAAI,CAAC;GAC1D,OAAO;IAAE,SAAS;IAAS,YAAY,UAAU,OAAO,KAAK;GAAO;EACtE,SAAS,OAAO;GACd,IAAI,gBAAgB,SAAS,MAAM,IAAI,mBAAmB,KAAK;GAC/D,OAAO,MAAM,6BAA6B;IACxC,OAAO;IACP,SAAS;KAAE,QAAQ,KAAK;KAAQ,KAAK,KAAK;KAAK;IAAY;GAC7D,CAAC;GACD,OAAO;IAAE,SAAS;IAAM,YAAY;GAAK;EAC3C;CACF,EACF;AACF"}
1
+ {"version":3,"file":"rate-limit.js","names":[],"sources":["../../src/infrastructure/better-auth/rate-limit.ts"],"sourcesContent":["import type { BetterAuthOptions, BetterAuthRateLimitStorage } from 'better-auth'\n\nimport { AppError, createLogger } from '@/core'\n\n/** A rule Better Auth passes to storage: a window in seconds and the requests it allows. */\nexport type AuthRateLimitRule = {\n readonly window: number\n readonly max: number\n}\n\n/** Each rule must match its binding's deployed limit and period. Cloudflare accepts only 10 or 60 seconds. */\nexport type AuthRateLimitBinding = {\n readonly binding: Pick<RateLimit, 'limit'>\n readonly window: 10 | 60\n readonly max: number\n}\n\n/**\n * Better Auth's built-in rules, which it does not export. Sign-in, sign-up, change-password, and\n * change-email paths get 3 requests per 10 seconds. Password reset, verification email, forget-password,\n * and email OTP send paths get 3 per 60 seconds. A package test pins both against the installed version.\n * Source: `getDefaultSpecialRules` in\n * https://github.com/better-auth/better-auth/blob/v1.7.4/packages/better-auth/src/api/rate-limiter/index.ts\n */\nexport const BUILT_IN_AUTH_RATE_LIMIT_RULES: readonly AuthRateLimitRule[] = [\n { window: 10, max: 3 },\n { window: 60, max: 3 },\n]\n\n/**\n * Every rule the configured instance can pass to storage, deduplicated: the general rule, the built-in\n * rules, each plugin's rules, and static custom rules. Function-valued custom rules decide per request\n * and are not listed. Disabled rate limiting returns no rules.\n */\nexport function authRateLimitRules(\n options: Pick<BetterAuthOptions, 'rateLimit' | 'plugins'>,\n): readonly AuthRateLimitRule[] {\n const rateLimit = options.rateLimit\n if (rateLimit?.enabled === false) return []\n // Better Auth's own defaults when the general rule is not set: window 10, max 100. Source:\n // https://github.com/better-auth/better-auth/blob/v1.7.4/packages/better-auth/src/context/create-context.ts\n const rules: AuthRateLimitRule[] = [\n { window: rateLimit?.window ?? 10, max: rateLimit?.max ?? 100 },\n ...BUILT_IN_AUTH_RATE_LIMIT_RULES,\n ]\n for (const plugin of options.plugins ?? [])\n for (const rule of plugin.rateLimit ?? []) rules.push({ window: rule.window, max: rule.max })\n for (const rule of Object.values(rateLimit?.customRules ?? {}))\n if (rule !== false && !(rule instanceof Function))\n rules.push({ window: rule.window, max: rule.max })\n const unique = new Map(rules.map((rule) => [`${rule.window}:${rule.max}`, rule]))\n return [...unique.values()]\n}\n\n/** A binding failure with no known retry classification. The cause preserves the original failure. */\nexport class AuthRateLimitError extends AppError {\n readonly _tag = 'AuthRateLimitError'\n readonly classification = 'unknown'\n\n constructor(cause: unknown) {\n super('Authentication rate limiter is unavailable', { cause })\n }\n}\n\n/** No configured binding matches this Better Auth rule. Outage policy cannot bypass a configuration error. */\nexport class UnsupportedAuthRateLimitRuleError extends AppError {\n readonly _tag = 'UnsupportedAuthRateLimitRuleError'\n readonly classification = 'terminal'\n\n constructor(\n readonly window: number,\n readonly max: number,\n ) {\n super('No Cloudflare binding supports the authentication rate limit rule')\n }\n}\n\n/** Typist allows requests during binding outages. Select block to propagate an error instead. */\nexport type AuthRateLimitOptions = {\n readonly failureMode?: 'allow' | 'block'\n}\n\n/**\n * Port Typist and Porte's binding adapters with explicit rule matching and configurable outage behavior.\n * Bindings enforce approximate limits per Cloudflare location; retryAfter is a conservative full window.\n */\nexport function createAuthRateLimitStorage(\n bindings: readonly AuthRateLimitBinding[],\n { failureMode = 'allow' }: AuthRateLimitOptions = {},\n): BetterAuthRateLimitStorage {\n const logger = createLogger('auth-rate-limit')\n return {\n async consume(key, rule) {\n const configured = bindings.find(\n (entry) => entry.window === rule.window && entry.max === rule.max,\n )\n if (configured === undefined)\n throw new UnsupportedAuthRateLimitRuleError(rule.window, rule.max)\n try {\n const { success } = await configured.binding.limit({ key })\n return { allowed: success, retryAfter: success ? null : rule.window }\n } catch (cause) {\n if (failureMode === 'block') throw new AuthRateLimitError(cause)\n logger.error('rate_limit_binding_failed', {\n error: cause,\n details: { window: rule.window, max: rule.max, failureMode },\n })\n return { allowed: true, retryAfter: null }\n }\n },\n }\n}\n"],"mappings":";;;;;;;;;;AAwBA,MAAa,iCAA+D,CAC1E;CAAE,QAAQ;CAAI,KAAK;AAAE,GACrB;CAAE,QAAQ;CAAI,KAAK;AAAE,CACvB;;;;;;AAOA,SAAgB,mBACd,SAC8B;CAC9B,MAAM,YAAY,QAAQ;CAC1B,IAAI,WAAW,YAAY,OAAO,OAAO,CAAC;CAG1C,MAAM,QAA6B,CACjC;EAAE,QAAQ,WAAW,UAAU;EAAI,KAAK,WAAW,OAAO;CAAI,GAC9D,GAAG,8BACL;CACA,KAAK,MAAM,UAAU,QAAQ,WAAW,CAAC,GACvC,KAAK,MAAM,QAAQ,OAAO,aAAa,CAAC,GAAG,MAAM,KAAK;EAAE,QAAQ,KAAK;EAAQ,KAAK,KAAK;CAAI,CAAC;CAC9F,KAAK,MAAM,QAAQ,OAAO,OAAO,WAAW,eAAe,CAAC,CAAC,GAC3D,IAAI,SAAS,SAAS,EAAE,gBAAgB,WACtC,MAAM,KAAK;EAAE,QAAQ,KAAK;EAAQ,KAAK,KAAK;CAAI,CAAC;CAErD,OAAO,CAAC,GAAG,IADQ,IAAI,MAAM,KAAK,SAAS,CAAC,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO,IAAI,CAAC,CAC/D,CAAC,CAAC,OAAO,CAAC;AAC5B;;AAGA,IAAa,qBAAb,cAAwC,SAAS;CAC/C,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YAAY,OAAgB;EAC1B,MAAM,8CAA8C,EAAE,MAAM,CAAC;CAC/D;AACF;;AAGA,IAAa,oCAAb,cAAuD,SAAS;CAKnD;CACA;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YACE,AAAS,QACT,AAAS,KACT;EACA,MAAM,mEAAmE;EAHhE;EACA;CAGX;AACF;;;;;AAWA,SAAgB,2BACd,UACA,EAAE,cAAc,YAAkC,CAAC,GACvB;CAC5B,MAAM,SAAS,aAAa,iBAAiB;CAC7C,OAAO,EACL,MAAM,QAAQ,KAAK,MAAM;EACvB,MAAM,aAAa,SAAS,MACzB,UAAU,MAAM,WAAW,KAAK,UAAU,MAAM,QAAQ,KAAK,GAChE;EACA,IAAI,eAAe,QACjB,MAAM,IAAI,kCAAkC,KAAK,QAAQ,KAAK,GAAG;EACnE,IAAI;GACF,MAAM,EAAE,YAAY,MAAM,WAAW,QAAQ,MAAM,EAAE,IAAI,CAAC;GAC1D,OAAO;IAAE,SAAS;IAAS,YAAY,UAAU,OAAO,KAAK;GAAO;EACtE,SAAS,OAAO;GACd,IAAI,gBAAgB,SAAS,MAAM,IAAI,mBAAmB,KAAK;GAC/D,OAAO,MAAM,6BAA6B;IACxC,OAAO;IACP,SAAS;KAAE,QAAQ,KAAK;KAAQ,KAAK,KAAK;KAAK;IAAY;GAC7D,CAAC;GACD,OAAO;IAAE,SAAS;IAAM,YAAY;GAAK;EAC3C;CACF,EACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { a as createLogger } from "./core-DC_4pOrI.js";
1
+ import { a as createLogger } from "./core-DG_cC8YT.js";
2
2
  import { v7 } from "uuid";
3
3
  import { isAPIError } from "better-auth/api";
4
4
  import { anonymous, bearer, captcha, deviceAuthorization, emailOTP, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
@@ -1,4 +1,4 @@
1
- import { kt as AppError } from "../index-6XMPJu7u.js";
1
+ import { Ut as AppError } from "../index-Bl0K1RQW.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 { U as AppError } from "../core-DC_4pOrI.js";
1
+ import { Q as AppError } from "../core-DG_cC8YT.js";
2
2
  import { DurableObject } from "cloudflare:workers";
3
3
 
4
4
  //#region src/infrastructure/cloudflare/auth-secondary-storage.ts
@@ -1,4 +1,4 @@
1
- import { At as FailureClassification, U as DomainCommand, W as DomainEvent, kt as AppError } from "../index-6XMPJu7u.js";
1
+ import { U as DomainCommand, Ut as AppError, W as DomainEvent, Wt as FailureClassification } from "../index-Bl0K1RQW.js";
2
2
  import { z } from "zod";
3
3
  //#region src/infrastructure/cloudflare/queue/queue-message.d.ts
4
4
  /** A message a queue carries: a command from outside the request, or an event from the outbox. */
@@ -1,5 +1,5 @@
1
- import { U as AppError, a as createLogger, b as shouldRetryFailure, c as exponentialEqualJitterMs, d as classifyFailure, f as retryAfterSecondsOf, l as spreadJitterMs, u as classifyBoundaryError } from "../core-DC_4pOrI.js";
2
- import { i as MessageAlreadyProcessedError } from "../application-DYfcAy-Q.js";
1
+ import { Q as AppError, a as createLogger, b as shouldRetryFailure, c as exponentialEqualJitterMs, d as classifyFailure, f as retryAfterSecondsOf, l as spreadJitterMs, u as classifyBoundaryError } from "../core-DG_cC8YT.js";
2
+ import { i as MessageAlreadyProcessedError } from "../application-CIxo_BpK.js";
3
3
  import { Result } from "better-result";
4
4
 
5
5
  //#region src/infrastructure/cloudflare/queue/queue-limits.ts
@@ -0,0 +1,36 @@
1
+ import { q as MessageId } from "../index-Bl0K1RQW.js";
2
+ import { z } from "zod";
3
+ //#region src/infrastructure/cloudflare/scheduled/scheduled-handler.d.ts
4
+ /** The work one schedule runs. Nothing returns to the platform, so each run reports its own outcome. */
5
+ type ScheduledRun<Deps> = (deps: Deps, at: Date) => Promise<void>;
6
+ /** The cron expressions of a schedule map, as literal types. */
7
+ type ScheduleExpression<Schedules extends Readonly<Record<string, string>>> = Schedules[keyof Schedules];
8
+ /** The Cloudflare `scheduled` export, with the application's dependencies as its second argument. */
9
+ type ScheduledHandler<Deps> = (controller: ScheduledController, deps: Deps) => Promise<void>;
10
+ /** What `defineSchedules` returns: the map, its parser, and the handler factory. */
11
+ type AppSchedules<Schedules extends Readonly<Record<string, string>>> = {
12
+ readonly schedules: Schedules;
13
+ /** Parses `controller.cron`, a bare string from the platform, into a known expression. */
14
+ readonly schema: z.ZodType<ScheduleExpression<Schedules>>;
15
+ /**
16
+ * Route each schedule to its work. One run per expression, so a schedule added without
17
+ * work fails to compile. An expression Wrangler fires that the map does not know is logged
18
+ * once and ignored. A run that throws rejects, so Cloudflare marks the invocation failed.
19
+ */
20
+ handler<Deps>(runs: Readonly<Record<ScheduleExpression<Schedules>, ScheduledRun<Deps>>>): ScheduledHandler<Deps>;
21
+ };
22
+ /**
23
+ * Define the application's schedules once: `EVERY_MINUTE: '* * * * *'`. Keys name the time,
24
+ * not the work, because one schedule may drive several commands. Wrangler's `triggers.crons`
25
+ * must hold exactly these expressions. Cron does not retry: a run that sweeps many items
26
+ * catches each item's failure itself.
27
+ */
28
+ export declare function defineSchedules<const Schedules extends Readonly<Record<string, string>>>(schedules: Schedules): AppSchedules<Schedules>;
29
+ /**
30
+ * The id of a command a schedule issues: `cron:<Name>:<scheduledTime>`. A doubled trigger for
31
+ * the same time is then a duplicate by construction, and the unit of work skips it.
32
+ */
33
+ export declare function scheduledMessageId(name: string, at: Date): MessageId;
34
+ //#endregion
35
+ export type { AppSchedules, ScheduleExpression, ScheduledHandler, ScheduledRun };
36
+ //# sourceMappingURL=scheduled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduled.d.ts","names":[],"sources":["../../src/infrastructure/cloudflare/scheduled/scheduled-handler.ts"],"mappings":";;;;KAOY,aAAa,SAAS,MAAM,MAAM,IAAI,SAAS;;KAG/C,mBAAmB,kBAAkB,SAAS,2BACxD,gBAAgB;;KAGN,iBAAiB,SAAS,YAAY,qBAAqB,MAAM,SAAS;;KAG1E,aAAa,kBAAkB,SAAS;WACzC,WAAW;;WAEX,QAAQ,EAAE,QAAQ,mBAAmB;;;;;;EAM9C,QAAQ,MACN,MAAM,SAAS,OAAO,mBAAmB,YAAY,aAAa,UACjE,iBAAiB;;;;;;;;wBASN,sBAAsB,kBAAkB,SAAS,yBAC/D,WAAW,YACV,aAAa;;;;;wBAoBA,mBAAmB,cAAc,IAAI,OAAO"}
@@ -0,0 +1,37 @@
1
+ import { a as createLogger } from "../core-DG_cC8YT.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/infrastructure/cloudflare/scheduled/scheduled-handler.ts
5
+ const logger = createLogger("scheduled");
6
+ /**
7
+ * Define the application's schedules once: `EVERY_MINUTE: '* * * * *'`. Keys name the time,
8
+ * not the work, because one schedule may drive several commands. Wrangler's `triggers.crons`
9
+ * must hold exactly these expressions. Cron does not retry: a run that sweeps many items
10
+ * catches each item's failure itself.
11
+ */
12
+ function defineSchedules(schedules) {
13
+ const schema = z.enum(schedules);
14
+ return {
15
+ schedules,
16
+ schema,
17
+ handler: (runs) => async (controller, deps) => {
18
+ const { cron } = controller;
19
+ if (!schema.validate(cron)) {
20
+ logger.error("scheduled_unknown_cron", { details: { cron } });
21
+ return;
22
+ }
23
+ await runs[cron](deps, new Date(controller.scheduledTime));
24
+ }
25
+ };
26
+ }
27
+ /**
28
+ * The id of a command a schedule issues: `cron:<Name>:<scheduledTime>`. A doubled trigger for
29
+ * the same time is then a duplicate by construction, and the unit of work skips it.
30
+ */
31
+ function scheduledMessageId(name, at) {
32
+ return `cron:${name}:${String(at.getTime())}`;
33
+ }
34
+
35
+ //#endregion
36
+ export { defineSchedules, scheduledMessageId };
37
+ //# sourceMappingURL=scheduled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduled.js","names":[],"sources":["../../src/infrastructure/cloudflare/scheduled/scheduled-handler.ts"],"sourcesContent":["import { z } from 'zod'\n\nimport { createLogger, type MessageId } from '@/core'\n\nconst logger = createLogger('scheduled')\n\n/** The work one schedule runs. Nothing returns to the platform, so each run reports its own outcome. */\nexport type ScheduledRun<Deps> = (deps: Deps, at: Date) => Promise<void>\n\n/** The cron expressions of a schedule map, as literal types. */\nexport type ScheduleExpression<Schedules extends Readonly<Record<string, string>>> =\n Schedules[keyof Schedules]\n\n/** The Cloudflare `scheduled` export, with the application's dependencies as its second argument. */\nexport type ScheduledHandler<Deps> = (controller: ScheduledController, deps: Deps) => Promise<void>\n\n/** What `defineSchedules` returns: the map, its parser, and the handler factory. */\nexport type AppSchedules<Schedules extends Readonly<Record<string, string>>> = {\n readonly schedules: Schedules\n /** Parses `controller.cron`, a bare string from the platform, into a known expression. */\n readonly schema: z.ZodType<ScheduleExpression<Schedules>>\n /**\n * Route each schedule to its work. One run per expression, so a schedule added without\n * work fails to compile. An expression Wrangler fires that the map does not know is logged\n * once and ignored. A run that throws rejects, so Cloudflare marks the invocation failed.\n */\n handler<Deps>(\n runs: Readonly<Record<ScheduleExpression<Schedules>, ScheduledRun<Deps>>>,\n ): ScheduledHandler<Deps>\n}\n\n/**\n * Define the application's schedules once: `EVERY_MINUTE: '* * * * *'`. Keys name the time,\n * not the work, because one schedule may drive several commands. Wrangler's `triggers.crons`\n * must hold exactly these expressions. Cron does not retry: a run that sweeps many items\n * catches each item's failure itself.\n */\nexport function defineSchedules<const Schedules extends Readonly<Record<string, string>>>(\n schedules: Schedules,\n): AppSchedules<Schedules> {\n const schema = z.enum(schedules)\n return {\n schedules,\n schema,\n handler: (runs) => async (controller, deps) => {\n const { cron } = controller\n if (!schema.validate(cron)) {\n logger.error('scheduled_unknown_cron', { details: { cron } })\n return\n }\n await runs[cron](deps, new Date(controller.scheduledTime))\n },\n }\n}\n\n/**\n * The id of a command a schedule issues: `cron:<Name>:<scheduledTime>`. A doubled trigger for\n * the same time is then a duplicate by construction, and the unit of work skips it.\n */\nexport function scheduledMessageId(name: string, at: Date): MessageId {\n return `cron:${name}:${String(at.getTime())}`\n}\n"],"mappings":";;;;AAIA,MAAM,SAAS,aAAa,WAAW;;;;;;;AAiCvC,SAAgB,gBACd,WACyB;CACzB,MAAM,SAAS,EAAE,KAAK,SAAS;CAC/B,OAAO;EACL;EACA;EACA,UAAU,SAAS,OAAO,YAAY,SAAS;GAC7C,MAAM,EAAE,SAAS;GACjB,IAAI,CAAC,OAAO,SAAS,IAAI,GAAG;IAC1B,OAAO,MAAM,0BAA0B,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC5D;GACF;GACA,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,KAAK,WAAW,aAAa,CAAC;EAC3D;CACF;AACF;;;;;AAMA,SAAgB,mBAAmB,MAAc,IAAqB;CACpE,OAAO,QAAQ,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAC5C"}
@@ -1,6 +1,6 @@
1
- import { W as DomainEvent, et as D1ConstraintKind, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-6XMPJu7u.js";
2
- import { s as ITransaction, v as IMessageStore } from "./index-_Cihfq1J.js";
3
- import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-BWTJfYPW.js";
1
+ import { W as DomainEvent, et as D1ConstraintKind, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-Bl0K1RQW.js";
2
+ import { C as IMessageStore, x as ITransaction } from "./index-DgDyeR5s.js";
3
+ import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-BzmnjlDa.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
@@ -1,6 +1,6 @@
1
- import { E as DatabaseError, U as AppError } from "./core-DC_4pOrI.js";
2
- import { i as MessageAlreadyProcessedError } from "./application-DYfcAy-Q.js";
3
- import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-BFCmU92Z.js";
1
+ import { E as DatabaseError, Q as AppError } from "./core-DG_cC8YT.js";
2
+ import { i as MessageAlreadyProcessedError } from "./application-CIxo_BpK.js";
3
+ import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-BPE1In2f.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";