better-ship 0.17.0 → 0.18.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 (116) hide show
  1. package/dist/{application-Cf9SER-i.js → application-CNdzHIXk.js} +5 -7
  2. package/dist/{application-Cf9SER-i.js.map → application-CNdzHIXk.js.map} +1 -1
  3. package/dist/application.d.ts +2 -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 +2 -2
  7. package/dist/better-auth/apple-client-secret.js.map +1 -1
  8. package/dist/better-auth/auth-client.js +1 -1
  9. package/dist/better-auth/rate-limit.d.ts +1 -1
  10. package/dist/better-auth/rate-limit.js +4 -6
  11. package/dist/better-auth/rate-limit.js.map +1 -1
  12. package/dist/better-auth.js +1 -1
  13. package/dist/billing/client.d.ts +86 -0
  14. package/dist/billing/client.d.ts.map +1 -0
  15. package/dist/billing/client.js +77 -0
  16. package/dist/billing/client.js.map +1 -0
  17. package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
  18. package/dist/cloudflare/auth-secondary-storage.js +1 -1
  19. package/dist/cloudflare/durable-object-client.js +1 -1
  20. package/dist/cloudflare/durable-object-errors.d.ts +1 -1
  21. package/dist/cloudflare/durable-object-errors.js +1 -1
  22. package/dist/cloudflare/kv-client.js +1 -1
  23. package/dist/cloudflare/kv-errors.d.ts +1 -1
  24. package/dist/cloudflare/kv-errors.js +1 -1
  25. package/dist/cloudflare/queue.d.ts +2 -2
  26. package/dist/cloudflare/queue.d.ts.map +1 -1
  27. package/dist/cloudflare/queue.js +10 -12
  28. package/dist/cloudflare/queue.js.map +1 -1
  29. package/dist/cloudflare/r2-client.d.ts +70 -0
  30. package/dist/cloudflare/r2-client.d.ts.map +1 -0
  31. package/dist/cloudflare/r2-client.js +153 -0
  32. package/dist/cloudflare/r2-client.js.map +1 -0
  33. package/dist/cloudflare/r2-errors.d.ts +34 -0
  34. package/dist/cloudflare/r2-errors.d.ts.map +1 -0
  35. package/dist/cloudflare/r2-errors.js +90 -0
  36. package/dist/cloudflare/r2-errors.js.map +1 -0
  37. package/dist/cloudflare/scheduled.d.ts +1 -1
  38. package/dist/cloudflare/scheduled.js +2 -2
  39. package/dist/cloudflare/scheduled.js.map +1 -1
  40. package/dist/cloudflare.d.ts +3 -3
  41. package/dist/cloudflare.js +3 -3
  42. package/dist/{core-ClSP5-V9.js → core-UUW2unJd.js} +106 -195
  43. package/dist/core-UUW2unJd.js.map +1 -0
  44. package/dist/core.d.ts +2 -2
  45. package/dist/core.js +2 -2
  46. package/dist/http.d.ts +2 -2
  47. package/dist/http.js +3 -3
  48. package/dist/http.js.map +1 -1
  49. package/dist/{index-Cchdg2aQ.d.ts → index-CYHXOH08.d.ts} +2 -2
  50. package/dist/{index-Cchdg2aQ.d.ts.map → index-CYHXOH08.d.ts.map} +1 -1
  51. package/dist/{index-DvJUcSW1.d.ts → index-DUFS0SJ_.d.ts} +41 -102
  52. package/dist/index-DUFS0SJ_.d.ts.map +1 -0
  53. package/dist/{outbox-repository-options-ICOF8g2h.js → outbox-repository-options-CgdqU20T.js} +2 -2
  54. package/dist/{outbox-repository-options-ICOF8g2h.js.map → outbox-repository-options-CgdqU20T.js.map} +1 -1
  55. package/dist/{outbox-repository-options-AkihEaik.d.ts → outbox-repository-options-DjBgFhMi.d.ts} +3 -3
  56. package/dist/{outbox-repository-options-AkihEaik.d.ts.map → outbox-repository-options-DjBgFhMi.d.ts.map} +1 -1
  57. package/dist/postgres.d.ts +3 -3
  58. package/dist/postgres.js +3 -3
  59. package/dist/posthog/metrics.d.ts +1 -1
  60. package/dist/posthog/server.d.ts +2 -2
  61. package/dist/posthog/server.js +1 -1
  62. package/dist/resend.d.ts +3 -3
  63. package/dist/resend.js +2 -2
  64. package/dist/sentry/client.d.ts +1 -1
  65. package/dist/sentry/client.d.ts.map +1 -1
  66. package/dist/sentry/client.js +10 -4
  67. package/dist/sentry/client.js.map +1 -1
  68. package/dist/sentry/server.d.ts +2 -2
  69. package/dist/sentry/server.js +5 -5
  70. package/dist/sentry/server.js.map +1 -1
  71. package/dist/stripe/payment-service.d.ts +52 -0
  72. package/dist/stripe/payment-service.d.ts.map +1 -0
  73. package/dist/stripe/payment-service.error.d.ts +13 -0
  74. package/dist/stripe/payment-service.error.d.ts.map +1 -0
  75. package/dist/stripe/payment-service.error.js +30 -0
  76. package/dist/stripe/payment-service.error.js.map +1 -0
  77. package/dist/stripe/payment-service.js +72 -0
  78. package/dist/stripe/payment-service.js.map +1 -0
  79. package/dist/stripe/payment-webhook.d.ts +18 -0
  80. package/dist/stripe/payment-webhook.d.ts.map +1 -0
  81. package/dist/stripe/payment-webhook.js +47 -0
  82. package/dist/stripe/payment-webhook.js.map +1 -0
  83. package/dist/tanstack.d.ts +1 -1
  84. package/dist/tanstack.js +4 -4
  85. package/dist/tanstack.js.map +1 -1
  86. package/dist/testing/outbox-repository.d.ts +2 -2
  87. package/dist/testing/payment-service.d.ts +10 -0
  88. package/dist/testing/payment-service.d.ts.map +1 -0
  89. package/dist/testing/payment-service.js +76 -0
  90. package/dist/testing/payment-service.js.map +1 -0
  91. package/dist/{webhook-verification-D6aGQfNi.js → webhook-verification-BBmjkHgI.js} +2 -2
  92. package/dist/{webhook-verification-D6aGQfNi.js.map → webhook-verification-BBmjkHgI.js.map} +1 -1
  93. package/dist/{webhook-verification-BcQQG-vY.d.ts → webhook-verification-BuxPzdr4.d.ts} +2 -2
  94. package/dist/{webhook-verification-BcQQG-vY.d.ts.map → webhook-verification-BuxPzdr4.d.ts.map} +1 -1
  95. package/package.json +13 -6
  96. package/src/application/outbox-relay.ts +3 -1
  97. package/src/core/console-log-sink.ts +65 -0
  98. package/src/core/index.ts +1 -4
  99. package/src/core/logger.ts +80 -281
  100. package/src/infrastructure/better-auth/apple-client-secret.ts +1 -1
  101. package/src/infrastructure/better-auth/rate-limit.ts +3 -1
  102. package/src/infrastructure/billing/client.ts +173 -0
  103. package/src/infrastructure/cloudflare/queue/queue-consumer.ts +8 -9
  104. package/src/infrastructure/cloudflare/r2-client.ts +162 -0
  105. package/src/infrastructure/cloudflare/r2-errors.ts +111 -0
  106. package/src/infrastructure/cloudflare/scheduled/scheduled-handler.ts +1 -1
  107. package/src/infrastructure/http/webhook-handler.ts +1 -1
  108. package/src/infrastructure/sentry/client.ts +6 -3
  109. package/src/infrastructure/sentry/server.ts +4 -4
  110. package/src/infrastructure/stripe/payment-service.error.ts +42 -0
  111. package/src/infrastructure/stripe/payment-service.ts +129 -0
  112. package/src/infrastructure/stripe/payment-webhook.ts +49 -0
  113. package/src/infrastructure/tanstack/resolve-failure.ts +3 -2
  114. package/src/infrastructure/testing/payment-service-contract.ts +91 -0
  115. package/dist/core-ClSP5-V9.js.map +0 -1
  116. package/dist/index-DvJUcSW1.d.ts.map +0 -1
@@ -1,4 +1,4 @@
1
- import { O as unreachable, a as createLogger, f as classifyFailure, k as DatabaseError, rt as AppError } from "./core-ClSP5-V9.js";
1
+ import { D as unreachable, O as DatabaseError, d as classifyFailure, nt as AppError, r as createLogger } from "./core-UUW2unJd.js";
2
2
  import { panic } from "better-result";
3
3
 
4
4
  //#region src/application/analytics-service.ts
@@ -200,11 +200,9 @@ var OutboxRelay = class {
200
200
  await this.repository.deadLetter(claim, messages, reason, errorMessage);
201
201
  logger.error("outbox_messages_dead_lettered", {
202
202
  error: cause,
203
- details: {
204
- claimToken: claim.token,
205
- count: messages.length,
206
- reason
207
- }
203
+ claimToken: claim.token,
204
+ count: messages.length,
205
+ reason
208
206
  });
209
207
  }
210
208
  };
@@ -288,4 +286,4 @@ var UnitOfWorkAlreadyActiveError = class extends AppError {
288
286
 
289
287
  //#endregion
290
288
  export { MessageBus as a, RecordingEmailService as c, OutboxRelay as i, NULL_METRICS_SERVICE as l, UnitOfWork as n, MessageAlreadyProcessedError as o, OutboxRetention as r, recordEmailEvent as s, UnitOfWorkAlreadyActiveError as t, NULL_ANALYTICS as u };
291
- //# sourceMappingURL=application-Cf9SER-i.js.map
289
+ //# sourceMappingURL=application-CNdzHIXk.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"application-Cf9SER-i.js","names":["logger"],"sources":["../src/application/analytics-service.ts","../src/application/metrics-service.ts","../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/outbox-relay.ts","../src/application/outbox-retention.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import type { AnalyticsEventIdentity, AnalyticsEventInput, AnalyticsProperties } from '@/core'\n\n/** Product analytics without an SDK dependency, ported from Typist and Porte. */\nexport interface AnalyticsService<TEvent extends AnalyticsEventInput = AnalyticsEventInput> {\n /** Attach traits to one person. */\n identify(distinctId: string, properties?: AnalyticsProperties): void\n /** Merge a previous ID into this person; the application must establish that both identify the same person. */\n alias(distinctId: string, previousDistinctId: string): void\n /** Schedule an event for a person; returning does not confirm remote acceptance. */\n track(event: TEvent, distinctId: string, identity?: AnalyticsEventIdentity): void\n /** Schedule an event without creating a person profile for its distinct ID. */\n trackAnonymous(event: TEvent, distinctId: string, identity?: AnalyticsEventIdentity): void\n}\n\n/** Reports to nobody. The composition root selects this service when analytics is disabled. */\nexport const NULL_ANALYTICS: AnalyticsService = {\n identify: () => {},\n alias: () => {},\n track: () => {},\n trackAnonymous: () => {},\n}\n","/** A metric dimension; use a small, bounded value set and never user or request IDs. */\nexport type MetricAttributeValue = string | number | boolean\n\n/** Dimensions shared by observations in one time series. */\nexport type MetricAttributes = Readonly<Record<string, MetricAttributeValue>>\n\n/** Unit and dimensions for an operational measurement. */\nexport interface MetricOptions {\n readonly attributes?: MetricAttributes\n readonly unit?: string\n}\n\n/** Operational measurements, ported from Typist's metrics-service.ts; applications own metric names. */\nexport interface MetricsService<TName extends string = string> {\n /** Add a nonnegative value to a total; the default increment is one. */\n count(name: TName, value?: number, options?: MetricOptions): void\n /** Record a current value. */\n gauge(name: TName, value: number, options?: MetricOptions): void\n /** Record one observation in a distribution. */\n histogram(name: TName, value: number, options?: MetricOptions): void\n}\n\n/** Records nothing when the composition root disables metrics. */\nexport const NULL_METRICS_SERVICE: MetricsService = {\n count: () => {},\n gauge: () => {},\n histogram: () => {},\n}\n","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 /** Override the default for this email, for example \"Display Name <address@domain.com>\". */\n readonly from?: string\n readonly to: string | readonly string[]\n readonly subject: string\n readonly html: string\n /** Optional stable identity of this send. Resend deduplicates supplied keys 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 adapter supplies the default sender and maps provider errors. Each email can override the sender.\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 { classifyFailure, createLogger, type DomainEvent } from '@/core'\n\nimport type {\n DeadLetterReason,\n IOutboxRepository,\n OutboxClaim,\n OutboxMessage,\n} from './outbox-repository.ts'\n\n/**\n * Delivers committed outbox rows. The unit of work only wakes it after a commit that wrote\n * events; the app decides what a wake is: `waitUntil`, a Durable Object alarm, nothing.\n * The sweep is the source of truth, so a lost wake loses no events.\n */\nexport interface IOutboxRelay {\n wake(): void\n}\n\n/** The publisher sends valid events and returns oversized events. QueueClient satisfies this port directly. */\nexport interface IOutboxPublisher {\n /** Resolves after queue acceptance; a rejected batch may already have sent some events. */\n sendBatchPartial(messages: ReadonlyArray<DomainEvent>): Promise<ReadonlyArray<DomainEvent>>\n}\n\n/** Optional limits for one relay; durable attempts exclude the publisher's immediate retries. */\nexport type OutboxRelayOptions = {\n readonly batchSize?: number\n readonly maxBatches?: number\n readonly maxAttempts?: number\n}\n\nconst logger = createLogger('outbox-relay')\n\n/**\n * Publishes committed events with bounded durable retries. Ported from Typist's relay with per-row exhaustion.\n * Send failures are recorded; repository failures propagate to the wake or scheduled boundary.\n */\nexport class OutboxRelay {\n private readonly options: Required<OutboxRelayOptions>\n\n constructor(\n private readonly publisher: IOutboxPublisher,\n private readonly repository: IOutboxRepository,\n options: OutboxRelayOptions = {},\n ) {\n this.options = {\n batchSize: options.batchSize ?? 50,\n maxBatches: options.maxBatches ?? 10,\n maxAttempts: options.maxAttempts ?? 10,\n }\n }\n\n /** Return the claimed count, including released and dead-lettered rows. Zero means no due rows. */\n async flush(): Promise<number> {\n const claim = await this.repository.claimPending(this.options.batchSize)\n const exhausted = claim.messages.filter((row) => row.attempts > this.options.maxAttempts)\n await this.deadLetter(claim, exhausted, 'exhausted', 'Claim attempt limit exceeded')\n const pending = claim.messages.filter((row) => row.attempts <= this.options.maxAttempts)\n if (pending.length === 0) return claim.messages.length\n\n let oversized: ReadonlyArray<DomainEvent>\n try {\n oversized = await this.publisher.sendBatchPartial(pending.map((row) => row.payload))\n } catch (error) {\n await this.handleSendFailure(claim, pending, error)\n return claim.messages.length\n }\n\n const oversizedIds = new Set(oversized.map((event) => event.id))\n await this.deadLetter(\n claim,\n pending.filter((row) => oversizedIds.has(row.payload.id)),\n 'oversized',\n 'Exceeds queue message size limit',\n )\n const sent = pending.filter((row) => !oversizedIds.has(row.payload.id))\n if (sent.length > 0) await this.repository.markPublished(claim, sent)\n return claim.messages.length\n }\n\n /** Drain until an empty claim or the batch limit; return the total claimed count. */\n async drain(): Promise<number> {\n let total = 0\n for (let batch = 0; batch < this.options.maxBatches; batch += 1) {\n // oxlint-disable-next-line no-await-in-loop -- An empty claim must stop the drain before another claim starts.\n const claimed = await this.flush()\n total += claimed\n if (claimed === 0) break\n }\n return total\n }\n\n private async handleSendFailure(\n claim: OutboxClaim,\n pending: ReadonlyArray<OutboxMessage>,\n cause: unknown,\n ): Promise<void> {\n const classification = classifyFailure(cause)\n const message =\n cause instanceof Error ? cause.message : 'Publisher rejected with a non-error value'\n if (classification === 'terminal') {\n await this.deadLetter(claim, pending, 'terminal', message, cause)\n return\n }\n const exhausted = pending.filter((row) => row.attempts >= this.options.maxAttempts)\n await this.deadLetter(claim, exhausted, 'exhausted', message, cause)\n const retry = pending.filter((row) => row.attempts < this.options.maxAttempts)\n if (retry.length > 0) await this.repository.releaseClaim(claim, retry)\n }\n\n private async deadLetter(\n claim: OutboxClaim,\n messages: ReadonlyArray<OutboxMessage>,\n reason: DeadLetterReason,\n errorMessage: string,\n cause?: unknown,\n ): Promise<void> {\n if (messages.length === 0) return\n await this.repository.deadLetter(claim, messages, reason, errorMessage)\n logger.error('outbox_messages_dead_lettered', {\n error: cause,\n details: { claimToken: claim.token, count: messages.length, reason },\n })\n }\n}\n","import type { IOutboxRetentionRepository } from './outbox-repository.ts'\n\n/** Typist's retention defaults; receipt retention must cover the application's supported replay period. */\nexport type OutboxRetentionOptions = {\n readonly publishedRetentionMs?: number\n readonly failedRetentionMs?: number\n readonly receiptRetentionMs?: number\n readonly batchSize?: number\n readonly maxBatches?: number\n}\n\n/** Deleted records per category during one bounded sweep. */\nexport type OutboxRetentionResult = {\n readonly published: number\n readonly failed: number\n readonly receipts: number\n}\n\n/** Bounded cleanup ported from Typist's message retention handler. The application schedules each sweep. */\nexport class OutboxRetention {\n private readonly options: Required<OutboxRetentionOptions>\n\n constructor(\n private readonly repository: IOutboxRetentionRepository,\n options: OutboxRetentionOptions = {},\n ) {\n this.options = {\n publishedRetentionMs: options.publishedRetentionMs ?? 86_400_000,\n failedRetentionMs: options.failedRetentionMs ?? 30 * 86_400_000,\n receiptRetentionMs: options.receiptRetentionMs ?? 7 * 86_400_000,\n batchSize: options.batchSize ?? 1_000,\n maxBatches: options.maxBatches ?? 10,\n }\n }\n\n /** Calculate all cutoffs from one supplied time; propagate repository failures to the calling boundary. */\n async sweep(at: Date): Promise<OutboxRetentionResult> {\n const { publishedRetentionMs, failedRetentionMs, receiptRetentionMs } = this.options\n const published = await this.deleteBatches((size) =>\n this.repository.deletePublishedBefore(new Date(at.getTime() - publishedRetentionMs), size),\n )\n const failed = await this.deleteBatches((size) =>\n this.repository.deleteFailedBefore(new Date(at.getTime() - failedRetentionMs), size),\n )\n const receipts = await this.deleteBatches((size) =>\n this.repository.deleteReceiptsBefore(new Date(at.getTime() - receiptRetentionMs), size),\n )\n return { published, failed, receipts }\n }\n\n private async deleteBatches(deleteBatch: (size: number) => Promise<number>): Promise<number> {\n let total = 0\n for (let batch = 0; batch < this.options.maxBatches; batch += 1) {\n // oxlint-disable-next-line no-await-in-loop -- The delete count decides whether another batch is needed.\n const deleted = await deleteBatch(this.options.batchSize)\n total += deleted\n if (deleted < this.options.batchSize) break\n }\n return total\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
+ {"version":3,"file":"application-CNdzHIXk.js","names":["logger"],"sources":["../src/application/analytics-service.ts","../src/application/metrics-service.ts","../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/outbox-relay.ts","../src/application/outbox-retention.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import type { AnalyticsEventIdentity, AnalyticsEventInput, AnalyticsProperties } from '@/core'\n\n/** Product analytics without an SDK dependency, ported from Typist and Porte. */\nexport interface AnalyticsService<TEvent extends AnalyticsEventInput = AnalyticsEventInput> {\n /** Attach traits to one person. */\n identify(distinctId: string, properties?: AnalyticsProperties): void\n /** Merge a previous ID into this person; the application must establish that both identify the same person. */\n alias(distinctId: string, previousDistinctId: string): void\n /** Schedule an event for a person; returning does not confirm remote acceptance. */\n track(event: TEvent, distinctId: string, identity?: AnalyticsEventIdentity): void\n /** Schedule an event without creating a person profile for its distinct ID. */\n trackAnonymous(event: TEvent, distinctId: string, identity?: AnalyticsEventIdentity): void\n}\n\n/** Reports to nobody. The composition root selects this service when analytics is disabled. */\nexport const NULL_ANALYTICS: AnalyticsService = {\n identify: () => {},\n alias: () => {},\n track: () => {},\n trackAnonymous: () => {},\n}\n","/** A metric dimension; use a small, bounded value set and never user or request IDs. */\nexport type MetricAttributeValue = string | number | boolean\n\n/** Dimensions shared by observations in one time series. */\nexport type MetricAttributes = Readonly<Record<string, MetricAttributeValue>>\n\n/** Unit and dimensions for an operational measurement. */\nexport interface MetricOptions {\n readonly attributes?: MetricAttributes\n readonly unit?: string\n}\n\n/** Operational measurements, ported from Typist's metrics-service.ts; applications own metric names. */\nexport interface MetricsService<TName extends string = string> {\n /** Add a nonnegative value to a total; the default increment is one. */\n count(name: TName, value?: number, options?: MetricOptions): void\n /** Record a current value. */\n gauge(name: TName, value: number, options?: MetricOptions): void\n /** Record one observation in a distribution. */\n histogram(name: TName, value: number, options?: MetricOptions): void\n}\n\n/** Records nothing when the composition root disables metrics. */\nexport const NULL_METRICS_SERVICE: MetricsService = {\n count: () => {},\n gauge: () => {},\n histogram: () => {},\n}\n","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 /** Override the default for this email, for example \"Display Name <address@domain.com>\". */\n readonly from?: string\n readonly to: string | readonly string[]\n readonly subject: string\n readonly html: string\n /** Optional stable identity of this send. Resend deduplicates supplied keys 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 adapter supplies the default sender and maps provider errors. Each email can override the sender.\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 { classifyFailure, createLogger, type DomainEvent } from '@/core'\n\nimport type {\n DeadLetterReason,\n IOutboxRepository,\n OutboxClaim,\n OutboxMessage,\n} from './outbox-repository.ts'\n\n/**\n * Delivers committed outbox rows. The unit of work only wakes it after a commit that wrote\n * events; the app decides what a wake is: `waitUntil`, a Durable Object alarm, nothing.\n * The sweep is the source of truth, so a lost wake loses no events.\n */\nexport interface IOutboxRelay {\n wake(): void\n}\n\n/** The publisher sends valid events and returns oversized events. QueueClient satisfies this port directly. */\nexport interface IOutboxPublisher {\n /** Resolves after queue acceptance; a rejected batch may already have sent some events. */\n sendBatchPartial(messages: ReadonlyArray<DomainEvent>): Promise<ReadonlyArray<DomainEvent>>\n}\n\n/** Optional limits for one relay; durable attempts exclude the publisher's immediate retries. */\nexport type OutboxRelayOptions = {\n readonly batchSize?: number\n readonly maxBatches?: number\n readonly maxAttempts?: number\n}\n\nconst logger = createLogger('outbox-relay')\n\n/**\n * Publishes committed events with bounded durable retries. Ported from Typist's relay with per-row exhaustion.\n * Send failures are recorded; repository failures propagate to the wake or scheduled boundary.\n */\nexport class OutboxRelay {\n private readonly options: Required<OutboxRelayOptions>\n\n constructor(\n private readonly publisher: IOutboxPublisher,\n private readonly repository: IOutboxRepository,\n options: OutboxRelayOptions = {},\n ) {\n this.options = {\n batchSize: options.batchSize ?? 50,\n maxBatches: options.maxBatches ?? 10,\n maxAttempts: options.maxAttempts ?? 10,\n }\n }\n\n /** Return the claimed count, including released and dead-lettered rows. Zero means no due rows. */\n async flush(): Promise<number> {\n const claim = await this.repository.claimPending(this.options.batchSize)\n const exhausted = claim.messages.filter((row) => row.attempts > this.options.maxAttempts)\n await this.deadLetter(claim, exhausted, 'exhausted', 'Claim attempt limit exceeded')\n const pending = claim.messages.filter((row) => row.attempts <= this.options.maxAttempts)\n if (pending.length === 0) return claim.messages.length\n\n let oversized: ReadonlyArray<DomainEvent>\n try {\n oversized = await this.publisher.sendBatchPartial(pending.map((row) => row.payload))\n } catch (error) {\n await this.handleSendFailure(claim, pending, error)\n return claim.messages.length\n }\n\n const oversizedIds = new Set(oversized.map((event) => event.id))\n await this.deadLetter(\n claim,\n pending.filter((row) => oversizedIds.has(row.payload.id)),\n 'oversized',\n 'Exceeds queue message size limit',\n )\n const sent = pending.filter((row) => !oversizedIds.has(row.payload.id))\n if (sent.length > 0) await this.repository.markPublished(claim, sent)\n return claim.messages.length\n }\n\n /** Drain until an empty claim or the batch limit; return the total claimed count. */\n async drain(): Promise<number> {\n let total = 0\n for (let batch = 0; batch < this.options.maxBatches; batch += 1) {\n // oxlint-disable-next-line no-await-in-loop -- An empty claim must stop the drain before another claim starts.\n const claimed = await this.flush()\n total += claimed\n if (claimed === 0) break\n }\n return total\n }\n\n private async handleSendFailure(\n claim: OutboxClaim,\n pending: ReadonlyArray<OutboxMessage>,\n cause: unknown,\n ): Promise<void> {\n const classification = classifyFailure(cause)\n const message =\n cause instanceof Error ? cause.message : 'Publisher rejected with a non-error value'\n if (classification === 'terminal') {\n await this.deadLetter(claim, pending, 'terminal', message, cause)\n return\n }\n const exhausted = pending.filter((row) => row.attempts >= this.options.maxAttempts)\n await this.deadLetter(claim, exhausted, 'exhausted', message, cause)\n const retry = pending.filter((row) => row.attempts < this.options.maxAttempts)\n if (retry.length > 0) await this.repository.releaseClaim(claim, retry)\n }\n\n private async deadLetter(\n claim: OutboxClaim,\n messages: ReadonlyArray<OutboxMessage>,\n reason: DeadLetterReason,\n errorMessage: string,\n cause?: unknown,\n ): Promise<void> {\n if (messages.length === 0) return\n await this.repository.deadLetter(claim, messages, reason, errorMessage)\n logger.error('outbox_messages_dead_lettered', {\n error: cause,\n claimToken: claim.token,\n count: messages.length,\n reason,\n })\n }\n}\n","import type { IOutboxRetentionRepository } from './outbox-repository.ts'\n\n/** Typist's retention defaults; receipt retention must cover the application's supported replay period. */\nexport type OutboxRetentionOptions = {\n readonly publishedRetentionMs?: number\n readonly failedRetentionMs?: number\n readonly receiptRetentionMs?: number\n readonly batchSize?: number\n readonly maxBatches?: number\n}\n\n/** Deleted records per category during one bounded sweep. */\nexport type OutboxRetentionResult = {\n readonly published: number\n readonly failed: number\n readonly receipts: number\n}\n\n/** Bounded cleanup ported from Typist's message retention handler. The application schedules each sweep. */\nexport class OutboxRetention {\n private readonly options: Required<OutboxRetentionOptions>\n\n constructor(\n private readonly repository: IOutboxRetentionRepository,\n options: OutboxRetentionOptions = {},\n ) {\n this.options = {\n publishedRetentionMs: options.publishedRetentionMs ?? 86_400_000,\n failedRetentionMs: options.failedRetentionMs ?? 30 * 86_400_000,\n receiptRetentionMs: options.receiptRetentionMs ?? 7 * 86_400_000,\n batchSize: options.batchSize ?? 1_000,\n maxBatches: options.maxBatches ?? 10,\n }\n }\n\n /** Calculate all cutoffs from one supplied time; propagate repository failures to the calling boundary. */\n async sweep(at: Date): Promise<OutboxRetentionResult> {\n const { publishedRetentionMs, failedRetentionMs, receiptRetentionMs } = this.options\n const published = await this.deleteBatches((size) =>\n this.repository.deletePublishedBefore(new Date(at.getTime() - publishedRetentionMs), size),\n )\n const failed = await this.deleteBatches((size) =>\n this.repository.deleteFailedBefore(new Date(at.getTime() - failedRetentionMs), size),\n )\n const receipts = await this.deleteBatches((size) =>\n this.repository.deleteReceiptsBefore(new Date(at.getTime() - receiptRetentionMs), size),\n )\n return { published, failed, receipts }\n }\n\n private async deleteBatches(deleteBatch: (size: number) => Promise<number>): Promise<number> {\n let total = 0\n for (let batch = 0; batch < this.options.maxBatches; batch += 1) {\n // oxlint-disable-next-line no-await-in-loop -- The delete count decides whether another batch is needed.\n const deleted = await deleteBatch(this.options.batchSize)\n total += deleted\n if (deleted < this.options.batchSize) break\n }\n return total\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 { J as DomainMessage, K as DomainCommand, X as MessageId, Y as DomainQuery, q as DomainEvent } from "./index-DvJUcSW1.js";
2
- import { $ as EventHandler, A as IOutboxRepository, B as OutboundEmail, C as UnitOfWorkDeps, D as OutboxRelay, E as IOutboxRelay, F as IEventCollector, G as MetricAttributeValue, H as ObservabilityScope, I as EmailAccepted, J as MetricsService, K as MetricAttributes, L as EmailAttachment, M as OutboxClaim, N as OutboxMessage, O as OutboxRelayOptions, P as IMessageStore, Q as CommandHandler, R as IEmailService, S as UnitOfWork, T as IOutboxPublisher, U as ObservabilityService, V as RecordingEmailService, W as ObservabilityUser, X as AnalyticsService, Y as NULL_METRICS_SERVICE, Z as NULL_ANALYTICS, _ as RecordEmailEventCommand, a as OutboxRetentionResult, b as recordEmailEvent, c as CommandRegistry, d as MessageRegistry, et as QueryHandler, f as MessageResult, g as IEmailDeliverabilityStore, h as EmailBounceRecord, i as OutboxRetentionOptions, j as IOutboxRetentionRepository, k as DeadLetterReason, l as EventRegistry, m as RegistryMessage, n as MessageAlreadyProcessedError, o as IMessageBus, p as QueryRegistry, q as MetricOptions, r as OutboxRetention, s as MessageBus, t as UnitOfWorkAlreadyActiveError, u as Handlers, v as RecordEmailEventDeps, w as ITransaction, x as IUnitOfWork, y as RecordEmailEventRepos, z as MimeType } from "./index-Cchdg2aQ.js";
1
+ import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, q as MessageId } from "./index-DUFS0SJ_.js";
2
+ import { $ as EventHandler, A as IOutboxRepository, B as OutboundEmail, C as UnitOfWorkDeps, D as OutboxRelay, E as IOutboxRelay, F as IEventCollector, G as MetricAttributeValue, H as ObservabilityScope, I as EmailAccepted, J as MetricsService, K as MetricAttributes, L as EmailAttachment, M as OutboxClaim, N as OutboxMessage, O as OutboxRelayOptions, P as IMessageStore, Q as CommandHandler, R as IEmailService, S as UnitOfWork, T as IOutboxPublisher, U as ObservabilityService, V as RecordingEmailService, W as ObservabilityUser, X as AnalyticsService, Y as NULL_METRICS_SERVICE, Z as NULL_ANALYTICS, _ as RecordEmailEventCommand, a as OutboxRetentionResult, b as recordEmailEvent, c as CommandRegistry, d as MessageRegistry, et as QueryHandler, f as MessageResult, g as IEmailDeliverabilityStore, h as EmailBounceRecord, i as OutboxRetentionOptions, j as IOutboxRetentionRepository, k as DeadLetterReason, l as EventRegistry, m as RegistryMessage, n as MessageAlreadyProcessedError, o as IMessageBus, p as QueryRegistry, q as MetricOptions, r as OutboxRetention, s as MessageBus, t as UnitOfWorkAlreadyActiveError, u as Handlers, v as RecordEmailEventDeps, w as ITransaction, x as IUnitOfWork, y as RecordEmailEventRepos, z as MimeType } from "./index-CYHXOH08.js";
3
3
  export { type AnalyticsService, type CommandHandler, type CommandRegistry, type DeadLetterReason, 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 IOutboxPublisher, type IOutboxRelay, type IOutboxRepository, type IOutboxRetentionRepository, type ITransaction, type IUnitOfWork, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type MetricAttributeValue, type MetricAttributes, type MetricOptions, type MetricsService, type MimeType, NULL_ANALYTICS, NULL_METRICS_SERVICE, type ObservabilityScope, type ObservabilityService, type ObservabilityUser, type OutboundEmail, type OutboxClaim, type OutboxMessage, OutboxRelay, type OutboxRelayOptions, OutboxRetention, type OutboxRetentionOptions, type OutboxRetentionResult, type QueryHandler, type QueryRegistry, type RecordEmailEventCommand, type RecordEmailEventDeps, type RecordEmailEventRepos, RecordingEmailService, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps, recordEmailEvent };
@@ -1,3 +1,3 @@
1
- import { a as MessageBus, c as RecordingEmailService, i as OutboxRelay, l as NULL_METRICS_SERVICE, n as UnitOfWork, o as MessageAlreadyProcessedError, r as OutboxRetention, s as recordEmailEvent, t as UnitOfWorkAlreadyActiveError, u as NULL_ANALYTICS } from "./application-Cf9SER-i.js";
1
+ import { a as MessageBus, c as RecordingEmailService, i as OutboxRelay, l as NULL_METRICS_SERVICE, n as UnitOfWork, o as MessageAlreadyProcessedError, r as OutboxRetention, s as recordEmailEvent, t as UnitOfWorkAlreadyActiveError, u as NULL_ANALYTICS } from "./application-CNdzHIXk.js";
2
2
 
3
3
  export { MessageAlreadyProcessedError, MessageBus, NULL_ANALYTICS, NULL_METRICS_SERVICE, OutboxRelay, OutboxRetention, RecordingEmailService, UnitOfWork, UnitOfWorkAlreadyActiveError, recordEmailEvent };
@@ -1,4 +1,4 @@
1
- import { $t as AppError } from "../index-DvJUcSW1.js";
1
+ import { Xt as AppError } from "../index-DUFS0SJ_.js";
2
2
  //#region src/infrastructure/better-auth/apple-client-secret.d.ts
3
3
  /**
4
4
  * Apple's issuer and OAuth callback origin, and the `aud` claim of the client secret. Include it in
@@ -1,4 +1,4 @@
1
- import { a as createLogger, rt as AppError } from "../core-ClSP5-V9.js";
1
+ import { nt as AppError, r as createLogger } from "../core-UUW2unJd.js";
2
2
  import { Buffer } from "node:buffer";
3
3
  import { createPrivateKey, sign } from "node:crypto";
4
4
 
@@ -40,7 +40,7 @@ function generateAppleClientSecret(config) {
40
40
  appleKeyFailureLogged = true;
41
41
  logger.error("apple_private_key_invalid", {
42
42
  error,
43
- details: { keyId: config.appleKeyId }
43
+ keyId: config.appleKeyId
44
44
  });
45
45
  }
46
46
  return "";
@@ -1 +1 @@
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
+ {"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, 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,OAAO,OAAO;GAAW,CAAC;EAC/E;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 { W as RateLimitError } from "../core-ClSP5-V9.js";
1
+ import { U as RateLimitError } from "../core-UUW2unJd.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 { $t as AppError } from "../index-DvJUcSW1.js";
1
+ import { Xt as AppError } from "../index-DUFS0SJ_.js";
2
2
  import { BetterAuthOptions, BetterAuthRateLimitStorage } from "better-auth";
3
3
  //#region src/infrastructure/better-auth/rate-limit.d.ts
4
4
  /** A rule Better Auth passes to storage: a window in seconds and the requests it allows. */
@@ -1,4 +1,4 @@
1
- import { a as createLogger, rt as AppError } from "../core-ClSP5-V9.js";
1
+ import { nt as AppError, r as createLogger } from "../core-UUW2unJd.js";
2
2
 
3
3
  //#region src/infrastructure/better-auth/rate-limit.ts
4
4
  /**
@@ -76,11 +76,9 @@ function createAuthRateLimitStorage(bindings, { failureMode = "allow" } = {}) {
76
76
  if (failureMode === "block") throw new AuthRateLimitError(cause);
77
77
  logger.error("rate_limit_binding_failed", {
78
78
  error: cause,
79
- details: {
80
- window: rule.window,
81
- max: rule.max,
82
- failureMode
83
- }
79
+ window: rule.window,
80
+ max: rule.max,
81
+ failureMode
84
82
  });
85
83
  return {
86
84
  allowed: true,
@@ -1 +1 @@
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
+ {"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 window: rule.window,\n max: rule.max,\n 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,QAAQ,KAAK;IACb,KAAK,KAAK;IACV;GACF,CAAC;GACD,OAAO;IAAE,SAAS;IAAM,YAAY;GAAK;EAC3C;CACF,EACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { a as createLogger } from "./core-ClSP5-V9.js";
1
+ import { r as createLogger } from "./core-UUW2unJd.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";
@@ -0,0 +1,86 @@
1
+ import { Xt as AppError, Zt as FailureClassification } from "../index-DUFS0SJ_.js";
2
+ import { createAuthClient } from "better-auth/client";
3
+ import { stripeClient } from "@better-auth/stripe/client";
4
+ //#region src/infrastructure/billing/client.d.ts
5
+ type AuthClient = ReturnType<typeof createAuthClient<{
6
+ plugins: [ReturnType<typeof stripeClient<{
7
+ subscription: true;
8
+ }>>];
9
+ }>>;
10
+ /** Native Better Auth subscription operations used by the browser billing service. */
11
+ export type SubscriptionBillingClient = Pick<AuthClient['subscription'], 'upgrade' | 'billingPortal'>;
12
+ /** Native upgrade input. Navigation remains with the UI. */
13
+ export type ChangePlanInput = Omit<NonNullable<Parameters<SubscriptionBillingClient['upgrade']>[0]>, 'disableRedirect'>;
14
+ /** Native portal input. Navigation remains with the UI. */
15
+ export type BillingPortalInput = Omit<NonNullable<Parameters<SubscriptionBillingClient['billingPortal']>[0]>, 'disableRedirect'>;
16
+ /** Native successful subscription response, including portal or Checkout data. */
17
+ export type ChangePlanResult = NonNullable<Awaited<ReturnType<SubscriptionBillingClient['upgrade']>>['data']>;
18
+ /** Native successful portal response. */
19
+ export type BillingPortalResult = NonNullable<Awaited<ReturnType<SubscriptionBillingClient['billingPortal']>>['data']>;
20
+ /** Native Better Auth billing failure; its code remains available to application UI. */
21
+ export type BillingApiError = NonNullable<Awaited<ReturnType<SubscriptionBillingClient['upgrade']>>['error']>;
22
+ /** A Better Auth billing endpoint rejected the request. The browser boundary owns reporting. */
23
+ export declare class BillingRequestError extends AppError {
24
+ readonly operation: 'subscription.upgrade' | 'subscription.billingPortal';
25
+ readonly _tag = "BillingRequestError";
26
+ readonly classification: FailureClassification;
27
+ readonly code: BillingApiError['code'];
28
+ /** Preserve the native code and cause without copying provider metadata. */
29
+ constructor(operation: 'subscription.upgrade' | 'subscription.billingPortal', cause: BillingApiError);
30
+ }
31
+ type BillingCallback = (this: void, ...args: never[]) => Promise<unknown>;
32
+ /**
33
+ * Supply only the capabilities the application uses. API callbacks must not depend on `this`.
34
+ * Their parameters, results, abort signals, and typed rejections remain application-owned.
35
+ */
36
+ export type BillingOptions = {
37
+ readonly subscriptions?: SubscriptionBillingClient;
38
+ readonly createTopUp?: BillingCallback;
39
+ readonly getAttempt?: BillingCallback;
40
+ readonly configureAutomaticTopUp?: BillingCallback;
41
+ readonly setupPaymentMethod?: BillingCallback;
42
+ };
43
+ type ConfiguredMethod<Options extends BillingOptions, Key extends PropertyKey> = Options extends BillingOptions ? (Key extends keyof Options ? Options[Key] : undefined) : never;
44
+ type SubscriptionMethod<Options extends BillingOptions, Method> = Options extends {
45
+ subscriptions: SubscriptionBillingClient;
46
+ } ? Method : 'subscriptions' extends keyof Options ? Method | undefined : undefined;
47
+ type ChangePlan = (input: ChangePlanInput) => Promise<ChangePlanResult>;
48
+ type CreateBillingPortal = (input: BillingPortalInput) => Promise<BillingPortalResult>;
49
+ /** One browser billing API; unconfigured operations are undefined and cannot be called. */
50
+ export interface IBillingService<Options extends BillingOptions> {
51
+ /** Start or change a subscription; return native data without redirecting. */
52
+ readonly changePlan: SubscriptionMethod<Options, ChangePlan>;
53
+ /** Create portal access; return native data without redirecting. */
54
+ readonly createBillingPortal: SubscriptionMethod<Options, CreateBillingPortal>;
55
+ /** Call the application's top-up API with its exact input and output types. */
56
+ readonly createTopUp: ConfiguredMethod<Options, 'createTopUp'>;
57
+ /** Read the application's persisted attempt state. */
58
+ readonly getAttempt: ConfiguredMethod<Options, 'getAttempt'>;
59
+ /** Persist automatic top-up settings through the application's API. */
60
+ readonly configureAutomaticTopUp: ConfiguredMethod<Options, 'configureAutomaticTopUp'>;
61
+ /** Start payment method setup through the application's API. */
62
+ readonly setupPaymentMethod: ConfiguredMethod<Options, 'setupPaymentMethod'>;
63
+ }
64
+ /**
65
+ * Ported from Typist's CheckoutService: one browser API for Better Auth and application payments.
66
+ * Unwraps Better Auth failures; preserves application callbacks, cancellations, and existing errors.
67
+ * Does not parse payloads, retry requests, log, navigate, or credit balances.
68
+ */
69
+ export declare class BillingService<const Options extends BillingOptions> implements IBillingService<Options> {
70
+ /** Configured Better Auth upgrade operation. */
71
+ readonly changePlan: SubscriptionMethod<Options, ChangePlan>;
72
+ /** Configured Better Auth portal operation. */
73
+ readonly createBillingPortal: SubscriptionMethod<Options, CreateBillingPortal>;
74
+ /** Exact application checkout callback, or undefined. */
75
+ readonly createTopUp: ConfiguredMethod<Options, 'createTopUp'>;
76
+ /** Exact application attempt callback, or undefined. */
77
+ readonly getAttempt: ConfiguredMethod<Options, 'getAttempt'>;
78
+ /** Exact application settings callback, or undefined. */
79
+ readonly configureAutomaticTopUp: ConfiguredMethod<Options, 'configureAutomaticTopUp'>;
80
+ /** Exact application setup callback, or undefined. */
81
+ readonly setupPaymentMethod: ConfiguredMethod<Options, 'setupPaymentMethod'>;
82
+ /** Inject configured Better Auth methods and standalone application API callbacks. */
83
+ constructor(options: Options);
84
+ }
85
+ //#endregion
86
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../../src/infrastructure/billing/client.ts"],"mappings":";;;;KAKK,aAAa,kBACT;EACL,UAAU,kBAAkB;IAAe;;;;YAKnC,4BAA4B,KACtC;;YAKU,kBAAkB,KAC5B,YAAY,WAAW;;YAKb,qBAAqB,KAC/B,YAAY,WAAW;;YAKb,mBAAmB,YAC7B,QAAQ,WAAW;;YAIT,sBAAsB,YAChC,QAAQ,WAAW;;YAIT,kBAAkB,YAC5B,QAAQ,WAAW;;qBAIR,4BAA4B;WAO5B;WANF;WACA,gBAAgB;WAChB,MAAM;;EAGf,YACW,kEACT,OAAO;;KASN,mBAAmB,eAAe,kBAAkB;;;;;YAM7C;WACD,gBAAgB;WAChB,cAAc;WACd,aAAa;WACb,0BAA0B;WAC1B,qBAAqB;;KAG3B,iBACH,gBAAgB,gBAChB,YAAY,eACV,gBAAgB,kBAAkB,kBAAkB,UAAU,QAAQ;KACrE,mBAAmB,gBAAgB,gBAAgB,UAAU;EAChE,eAAe;IAEb,uCAC8B,UAC5B;KAED,cAAc,OAAO,oBAAoB,QAAQ;KACjD,uBAAuB,OAAO,uBAAuB,QAAQ;;iBAGjD,gBAAgB,gBAAgB;;WAEtC,YAAY,mBAAmB,SAAS;;WAExC,qBAAqB,mBAAmB,SAAS;;WAEjD,aAAa,iBAAiB;;WAE9B,YAAY,iBAAiB;;WAE7B,yBAAyB,iBAAiB;;WAE1C,oBAAoB,iBAAiB;;;;;;;qBAQnC,qBACL,gBAAgB,2BACX,gBAAgB;;WAElB,YAAY,mBAAmB,SAAS;;WAExC,qBAAqB,mBAAmB,SAAS;;WAEjD,aAAa,iBAAiB;;WAE9B,YAAY,iBAAiB;;WAE7B,yBAAyB,iBAAiB;;WAE1C,oBAAoB,iBAAiB;;EAG9C,YAAY,SAAS"}
@@ -0,0 +1,77 @@
1
+ import { nt as AppError } from "../core-UUW2unJd.js";
2
+
3
+ //#region src/infrastructure/billing/client.ts
4
+ /** A Better Auth billing endpoint rejected the request. The browser boundary owns reporting. */
5
+ var BillingRequestError = class extends AppError {
6
+ operation;
7
+ _tag = "BillingRequestError";
8
+ classification;
9
+ code;
10
+ /** Preserve the native code and cause without copying provider metadata. */
11
+ constructor(operation, cause) {
12
+ super("Billing request failed", { cause });
13
+ this.operation = operation;
14
+ this.code = cause.code;
15
+ this.classification = cause.status === 429 || cause.status >= 500 ? "transient" : "terminal";
16
+ }
17
+ };
18
+ /**
19
+ * Ported from Typist's CheckoutService: one browser API for Better Auth and application payments.
20
+ * Unwraps Better Auth failures; preserves application callbacks, cancellations, and existing errors.
21
+ * Does not parse payloads, retry requests, log, navigate, or credit balances.
22
+ */
23
+ var BillingService = class {
24
+ /** Configured Better Auth upgrade operation. */
25
+ changePlan;
26
+ /** Configured Better Auth portal operation. */
27
+ createBillingPortal;
28
+ /** Exact application checkout callback, or undefined. */
29
+ createTopUp;
30
+ /** Exact application attempt callback, or undefined. */
31
+ getAttempt;
32
+ /** Exact application settings callback, or undefined. */
33
+ configureAutomaticTopUp;
34
+ /** Exact application setup callback, or undefined. */
35
+ setupPaymentMethod;
36
+ /** Inject configured Better Auth methods and standalone application API callbacks. */
37
+ constructor(options) {
38
+ const subscriptions = options.subscriptions;
39
+ const changePlan = subscriptions && (async (input) => {
40
+ const result = await subscriptions.upgrade({
41
+ ...input,
42
+ disableRedirect: true,
43
+ fetchOptions: {
44
+ ...input.fetchOptions,
45
+ throw: false
46
+ }
47
+ });
48
+ if (result.error) throw new BillingRequestError("subscription.upgrade", result.error);
49
+ return result.data;
50
+ });
51
+ const createBillingPortal = subscriptions && (async (input) => {
52
+ const result = await subscriptions.billingPortal({
53
+ ...input,
54
+ disableRedirect: true,
55
+ fetchOptions: {
56
+ ...input.fetchOptions,
57
+ throw: false
58
+ }
59
+ });
60
+ if (result.error) throw new BillingRequestError("subscription.billingPortal", result.error);
61
+ return result.data;
62
+ });
63
+ this.changePlan = changePlan;
64
+ this.createBillingPortal = createBillingPortal;
65
+ this.createTopUp = configuredMethod(options, "createTopUp");
66
+ this.getAttempt = configuredMethod(options, "getAttempt");
67
+ this.configureAutomaticTopUp = configuredMethod(options, "configureAutomaticTopUp");
68
+ this.setupPaymentMethod = configuredMethod(options, "setupPaymentMethod");
69
+ }
70
+ };
71
+ function configuredMethod(options, key) {
72
+ return options[key];
73
+ }
74
+
75
+ //#endregion
76
+ export { BillingRequestError, BillingService };
77
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../src/infrastructure/billing/client.ts"],"sourcesContent":["import type { stripeClient } from '@better-auth/stripe/client'\nimport type { createAuthClient } from 'better-auth/client'\n\nimport { AppError, type FailureClassification } from '@/core'\n\ntype AuthClient = ReturnType<\n typeof createAuthClient<{\n plugins: [ReturnType<typeof stripeClient<{ subscription: true }>>]\n }>\n>\n\n/** Native Better Auth subscription operations used by the browser billing service. */\nexport type SubscriptionBillingClient = Pick<\n AuthClient['subscription'],\n 'upgrade' | 'billingPortal'\n>\n\n/** Native upgrade input. Navigation remains with the UI. */\nexport type ChangePlanInput = Omit<\n NonNullable<Parameters<SubscriptionBillingClient['upgrade']>[0]>,\n 'disableRedirect'\n>\n\n/** Native portal input. Navigation remains with the UI. */\nexport type BillingPortalInput = Omit<\n NonNullable<Parameters<SubscriptionBillingClient['billingPortal']>[0]>,\n 'disableRedirect'\n>\n\n/** Native successful subscription response, including portal or Checkout data. */\nexport type ChangePlanResult = NonNullable<\n Awaited<ReturnType<SubscriptionBillingClient['upgrade']>>['data']\n>\n\n/** Native successful portal response. */\nexport type BillingPortalResult = NonNullable<\n Awaited<ReturnType<SubscriptionBillingClient['billingPortal']>>['data']\n>\n\n/** Native Better Auth billing failure; its code remains available to application UI. */\nexport type BillingApiError = NonNullable<\n Awaited<ReturnType<SubscriptionBillingClient['upgrade']>>['error']\n>\n\n/** A Better Auth billing endpoint rejected the request. The browser boundary owns reporting. */\nexport class BillingRequestError extends AppError {\n readonly _tag = 'BillingRequestError'\n readonly classification: FailureClassification\n readonly code: BillingApiError['code']\n\n /** Preserve the native code and cause without copying provider metadata. */\n constructor(\n readonly operation: 'subscription.upgrade' | 'subscription.billingPortal',\n cause: BillingApiError,\n ) {\n super('Billing request failed', { cause })\n this.code = cause.code\n this.classification = cause.status === 429 || cause.status >= 500 ? 'transient' : 'terminal'\n }\n}\n\n// oxlint-disable-next-line anti-slop/no-unknown-returns -- Generic constraint only; service methods retain each callback's exact return type.\ntype BillingCallback = (this: void, ...args: never[]) => Promise<unknown>\n\n/**\n * Supply only the capabilities the application uses. API callbacks must not depend on `this`.\n * Their parameters, results, abort signals, and typed rejections remain application-owned.\n */\nexport type BillingOptions = {\n readonly subscriptions?: SubscriptionBillingClient\n readonly createTopUp?: BillingCallback\n readonly getAttempt?: BillingCallback\n readonly configureAutomaticTopUp?: BillingCallback\n readonly setupPaymentMethod?: BillingCallback\n}\n\ntype ConfiguredMethod<\n Options extends BillingOptions,\n Key extends PropertyKey,\n> = Options extends BillingOptions ? (Key extends keyof Options ? Options[Key] : undefined) : never\ntype SubscriptionMethod<Options extends BillingOptions, Method> = Options extends {\n subscriptions: SubscriptionBillingClient\n}\n ? Method\n : 'subscriptions' extends keyof Options\n ? Method | undefined\n : undefined\ntype ChangePlan = (input: ChangePlanInput) => Promise<ChangePlanResult>\ntype CreateBillingPortal = (input: BillingPortalInput) => Promise<BillingPortalResult>\n\n/** One browser billing API; unconfigured operations are undefined and cannot be called. */\nexport interface IBillingService<Options extends BillingOptions> {\n /** Start or change a subscription; return native data without redirecting. */\n readonly changePlan: SubscriptionMethod<Options, ChangePlan>\n /** Create portal access; return native data without redirecting. */\n readonly createBillingPortal: SubscriptionMethod<Options, CreateBillingPortal>\n /** Call the application's top-up API with its exact input and output types. */\n readonly createTopUp: ConfiguredMethod<Options, 'createTopUp'>\n /** Read the application's persisted attempt state. */\n readonly getAttempt: ConfiguredMethod<Options, 'getAttempt'>\n /** Persist automatic top-up settings through the application's API. */\n readonly configureAutomaticTopUp: ConfiguredMethod<Options, 'configureAutomaticTopUp'>\n /** Start payment method setup through the application's API. */\n readonly setupPaymentMethod: ConfiguredMethod<Options, 'setupPaymentMethod'>\n}\n\n/**\n * Ported from Typist's CheckoutService: one browser API for Better Auth and application payments.\n * Unwraps Better Auth failures; preserves application callbacks, cancellations, and existing errors.\n * Does not parse payloads, retry requests, log, navigate, or credit balances.\n */\nexport class BillingService<\n const Options extends BillingOptions,\n> implements IBillingService<Options> {\n /** Configured Better Auth upgrade operation. */\n readonly changePlan: SubscriptionMethod<Options, ChangePlan>\n /** Configured Better Auth portal operation. */\n readonly createBillingPortal: SubscriptionMethod<Options, CreateBillingPortal>\n /** Exact application checkout callback, or undefined. */\n readonly createTopUp: ConfiguredMethod<Options, 'createTopUp'>\n /** Exact application attempt callback, or undefined. */\n readonly getAttempt: ConfiguredMethod<Options, 'getAttempt'>\n /** Exact application settings callback, or undefined. */\n readonly configureAutomaticTopUp: ConfiguredMethod<Options, 'configureAutomaticTopUp'>\n /** Exact application setup callback, or undefined. */\n readonly setupPaymentMethod: ConfiguredMethod<Options, 'setupPaymentMethod'>\n\n /** Inject configured Better Auth methods and standalone application API callbacks. */\n constructor(options: Options) {\n const subscriptions = options.subscriptions\n const changePlan: ChangePlan | undefined =\n subscriptions &&\n (async (input) => {\n const result = await subscriptions.upgrade({\n ...input,\n disableRedirect: true,\n fetchOptions: { ...input.fetchOptions, throw: false },\n })\n if (result.error) throw new BillingRequestError('subscription.upgrade', result.error)\n return result.data\n })\n const createBillingPortal: CreateBillingPortal | undefined =\n subscriptions &&\n (async (input) => {\n const result = await subscriptions.billingPortal({\n ...input,\n disableRedirect: true,\n fetchOptions: { ...input.fetchOptions, throw: false },\n })\n if (result.error) throw new BillingRequestError('subscription.billingPortal', result.error)\n return result.data\n })\n // SAFETY: Each subscription method exists exactly when the required subscriptions dependency exists.\n this.changePlan = changePlan as SubscriptionMethod<Options, ChangePlan>\n // SAFETY: Each subscription method exists exactly when the required subscriptions dependency exists.\n this.createBillingPortal = createBillingPortal as SubscriptionMethod<\n Options,\n CreateBillingPortal\n >\n this.createTopUp = configuredMethod(options, 'createTopUp')\n this.getAttempt = configuredMethod(options, 'getAttempt')\n this.configureAutomaticTopUp = configuredMethod(options, 'configureAutomaticTopUp')\n this.setupPaymentMethod = configuredMethod(options, 'setupPaymentMethod')\n }\n}\n\nfunction configuredMethod<Options extends BillingOptions, Key extends keyof BillingOptions>(\n options: Options,\n key: Key,\n): ConfiguredMethod<Options, Key> {\n // SAFETY: Optional callbacks are returned unchanged; absent keys read as undefined.\n return options[key] as ConfiguredMethod<Options, Key>\n}\n"],"mappings":";;;;AA6CA,IAAa,sBAAb,cAAyC,SAAS;CAOrC;CANX,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;;CAGT,YACE,AAAS,WACT,OACA;EACA,MAAM,0BAA0B,EAAE,MAAM,CAAC;EAHhC;EAIT,KAAK,OAAO,MAAM;EAClB,KAAK,iBAAiB,MAAM,WAAW,OAAO,MAAM,UAAU,MAAM,cAAc;CACpF;AACF;;;;;;AAoDA,IAAa,iBAAb,MAEsC;;CAEpC,AAAS;;CAET,AAAS;;CAET,AAAS;;CAET,AAAS;;CAET,AAAS;;CAET,AAAS;;CAGT,YAAY,SAAkB;EAC5B,MAAM,gBAAgB,QAAQ;EAC9B,MAAM,aACJ,kBACC,OAAO,UAAU;GAChB,MAAM,SAAS,MAAM,cAAc,QAAQ;IACzC,GAAG;IACH,iBAAiB;IACjB,cAAc;KAAE,GAAG,MAAM;KAAc,OAAO;IAAM;GACtD,CAAC;GACD,IAAI,OAAO,OAAO,MAAM,IAAI,oBAAoB,wBAAwB,OAAO,KAAK;GACpF,OAAO,OAAO;EAChB;EACF,MAAM,sBACJ,kBACC,OAAO,UAAU;GAChB,MAAM,SAAS,MAAM,cAAc,cAAc;IAC/C,GAAG;IACH,iBAAiB;IACjB,cAAc;KAAE,GAAG,MAAM;KAAc,OAAO;IAAM;GACtD,CAAC;GACD,IAAI,OAAO,OAAO,MAAM,IAAI,oBAAoB,8BAA8B,OAAO,KAAK;GAC1F,OAAO,OAAO;EAChB;EAEF,KAAK,aAAa;EAElB,KAAK,sBAAsB;EAI3B,KAAK,cAAc,iBAAiB,SAAS,aAAa;EAC1D,KAAK,aAAa,iBAAiB,SAAS,YAAY;EACxD,KAAK,0BAA0B,iBAAiB,SAAS,yBAAyB;EAClF,KAAK,qBAAqB,iBAAiB,SAAS,oBAAoB;CAC1E;AACF;AAEA,SAAS,iBACP,SACA,KACgC;CAEhC,OAAO,QAAQ;AACjB"}
@@ -1,4 +1,4 @@
1
- import { $t as AppError } from "../index-DvJUcSW1.js";
1
+ import { Xt as AppError } from "../index-DUFS0SJ_.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 { rt as AppError } from "../core-ClSP5-V9.js";
1
+ import { nt as AppError } from "../core-UUW2unJd.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 { x as shouldRetryFailure, y as isAbortError } from "../core-ClSP5-V9.js";
1
+ import { b as shouldRetryFailure, v as isAbortError } from "../core-UUW2unJd.js";
2
2
  import { DurableObjectCallError, classifyDurableObjectError, isClassifiedDurableObjectError } from "./durable-object-errors.js";
3
3
  import { Result } from "better-result";
4
4
 
@@ -1,4 +1,4 @@
1
- import { $t as AppError, en as FailureClassification } from "../index-DvJUcSW1.js";
1
+ import { Xt as AppError, Zt as FailureClassification } from "../index-DUFS0SJ_.js";
2
2
  import { z } from "zod";
3
3
  //#region src/infrastructure/cloudflare/durable-object-errors.d.ts
4
4
  /** Stable tag for a failed Durable Object call. */
@@ -1,4 +1,4 @@
1
- import { _ as isCloudflareRetryableError, b as isTransientTransportError, g as isCloudflareOverloadedError, rt as AppError, v as isTransientCloudflareRuntimeError, y as isAbortError } from "../core-ClSP5-V9.js";
1
+ import { _ as isTransientCloudflareRuntimeError, g as isCloudflareRetryableError, h as isCloudflareOverloadedError, nt as AppError, v as isAbortError, y as isTransientTransportError } from "../core-UUW2unJd.js";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/infrastructure/cloudflare/durable-object-errors.ts
@@ -1,4 +1,4 @@
1
- import { c as exponentialDelayCapMs, u as spreadJitterMs } from "../core-ClSP5-V9.js";
1
+ import { l as spreadJitterMs, s as exponentialDelayCapMs } from "../core-UUW2unJd.js";
2
2
  import { KVError, decideKVRetry, isKVPutRateLimitError } from "./kv-errors.js";
3
3
  import { Result } from "better-result";
4
4
 
@@ -1,4 +1,4 @@
1
- import { $t as AppError, en as FailureClassification } from "../index-DvJUcSW1.js";
1
+ import { Xt as AppError, Zt as FailureClassification } from "../index-DUFS0SJ_.js";
2
2
  //#region src/infrastructure/cloudflare/kv-errors.d.ts
3
3
  /** KV operations recognized in binding error messages. */
4
4
  export type KVOperation = 'get' | 'put' | 'delete' | 'list';
@@ -1,4 +1,4 @@
1
- import { _ as isCloudflareRetryableError, b as isTransientTransportError, g as isCloudflareOverloadedError, rt as AppError } from "../core-ClSP5-V9.js";
1
+ import { g as isCloudflareRetryableError, h as isCloudflareOverloadedError, nt as AppError, y as isTransientTransportError } from "../core-UUW2unJd.js";
2
2
 
3
3
  //#region src/infrastructure/cloudflare/kv-errors.ts
4
4
  /** Extract Typist's verified KV message formats, including nested causes. */
@@ -1,5 +1,5 @@
1
- import { $t as AppError, K as DomainCommand, en as FailureClassification, q as DomainEvent } from "../index-DvJUcSW1.js";
2
- import { D as OutboxRelay, E as IOutboxRelay } from "../index-Cchdg2aQ.js";
1
+ import { U as DomainCommand, W as DomainEvent, Xt as AppError, Zt as FailureClassification } from "../index-DUFS0SJ_.js";
2
+ import { D as OutboxRelay, E as IOutboxRelay } from "../index-CYHXOH08.js";
3
3
  import { z } from "zod";
4
4
  //#region src/infrastructure/cloudflare/queue/queue-message.d.ts
5
5
  /** A message a queue carries: a command from outside the request, or an event from the outbox. */
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.ts","names":[],"sources":["../../src/infrastructure/cloudflare/queue/queue-message.ts","../../src/infrastructure/cloudflare/queue/queue-client.ts","../../src/infrastructure/cloudflare/queue/queue-consumer.ts","../../src/infrastructure/cloudflare/queue/queue-limits.ts","../../src/infrastructure/cloudflare/queue/queue.errors.ts","../../src/infrastructure/cloudflare/queue/redelivery-policy.ts","../../src/infrastructure/cloudflare/queue/wait-until-wake.ts"],"mappings":";;;;;KAGY,eAAe,gBAAgB;;;;KCa/B;WAA8B;;;KAG9B,aAAa,QAAQ,KAAK,MAAM;;;;;KAMhC,mBAAmB,iBAAiB,cAAc;WACnD,QAAQ,SAAS,OAAO,MAAM,aAAa;;WAE3C,YAAY,KAAK;WACjB,SAAS,SAAS,aAAa;;;UAIzB,aAAa,iBAAiB;EAC7C,KAAK,SAAS,UAAU,UAAU,mBAAmB;EACrD,UAAU,UAAU,cAAc,YAAY;;EAE9C,iBAAiB,aAAa,UAC5B,UAAU,cAAc,QACvB,QAAQ,cAAc;;EAEzB,UAAU,SAAS,UAAU;;;;;;qBA8BlB,YACX,iBAAiB,cACjB,gCACW,aAAa;mBACK;EAA7B,YAA6B,SAAS,mBAAmB,UAAU;;EAG7D,KAAK,SAAS,UAAU,UAAU,mBAAmB;;EA4BrD,UAAU,UAAU,cAAc,YAAY;EAO9C,iBAAiB,aAAa,UAClC,UAAU,cAAc,QACvB,QAAQ,cAAc;EAMnB,UAAU,SAAS,UAAU;;UAQrB;UA4BN;UAIM;;;;;KCpJJ,gBAAgB,iBAAiB,iBAC3C,SAAS,UACT,iBACA,WAAW,kBACR;;KAGO,YAAY,iBAAiB;EAEvC,OAAO,SAAS,WAAW;;;;;;;qBAQhB,cAAc,iBAAiB;mBAQvB;mBACA;mBACA;mBACA;;;;;;;EAJnB,YACmB,KAAK,YAAY,WACjB,aAAa,KAAK,aAAa,yBAC/B,QAAQ,EAAE,QAAQ,WAClB,eAAc,gBAAgB;;EAI3C,aAAa,OAAO,eAAe;UAc3B;;UA0BA;;UAMA;;;;;UAaN;;;;;;;;;qBCjGG;WACX;WACA;WACA;;;wBAIc,kBAAkB,SAAS;;KAK/B,aAAa;WAAuB,SAAS;WAAmB;;;wBAG5D,aAAa,iBAAiB,cAC5C,UAAU,cAAc,YACvB,aAAa;;wBAKA,YAAY,OAAO,aAAa;;wBAKhC,cAAc,UAC5B,OAAO,cAAc,aAAa,aACjC;;;;KCjCS;;wBA4CI,mBAAmB,iBAAiB;;qBASvC,uBAAuB;WACzB;WACA,gBAAgB;WAChB,WAAW;EAEpB,YAAY;aAAiB,WAAW;aAAyB;;;;;;;qBAWtD,8BAA8B;WAChC;WACA;WACA;WACA;WACA;EAET,YAAY;aACD;aACA;aACA;;;;;;;qBAeA,mCAAmC;WACrC;WACA;EAET,YAAY;;;;;KCrFF;WACG;WAAwB;;WAExB;WACA,gBAAgB,QAAQ;WACxB;;;;;;;;wBASC,iBAAiB,gBAAgB,kBAAkB;;;;wBCxBnD,cACd,SAAS,KAAK,gCACd,OAAO,KAAK,wBACX"}
1
+ {"version":3,"file":"queue.d.ts","names":[],"sources":["../../src/infrastructure/cloudflare/queue/queue-message.ts","../../src/infrastructure/cloudflare/queue/queue-client.ts","../../src/infrastructure/cloudflare/queue/queue-consumer.ts","../../src/infrastructure/cloudflare/queue/queue-limits.ts","../../src/infrastructure/cloudflare/queue/queue.errors.ts","../../src/infrastructure/cloudflare/queue/redelivery-policy.ts","../../src/infrastructure/cloudflare/queue/wait-until-wake.ts"],"mappings":";;;;;KAGY,eAAe,gBAAgB;;;;KCa/B;WAA8B;;;KAG9B,aAAa,QAAQ,KAAK,MAAM;;;;;KAMhC,mBAAmB,iBAAiB,cAAc;WACnD,QAAQ,SAAS,OAAO,MAAM,aAAa;;WAE3C,YAAY,KAAK;WACjB,SAAS,SAAS,aAAa;;;UAIzB,aAAa,iBAAiB;EAC7C,KAAK,SAAS,UAAU,UAAU,mBAAmB;EACrD,UAAU,UAAU,cAAc,YAAY;;EAE9C,iBAAiB,aAAa,UAC5B,UAAU,cAAc,QACvB,QAAQ,cAAc;;EAEzB,UAAU,SAAS,UAAU;;;;;;qBA8BlB,YACX,iBAAiB,cACjB,gCACW,aAAa;mBACK;EAA7B,YAA6B,SAAS,mBAAmB,UAAU;;EAG7D,KAAK,SAAS,UAAU,UAAU,mBAAmB;;EA4BrD,UAAU,UAAU,cAAc,YAAY;EAO9C,iBAAiB,aAAa,UAClC,UAAU,cAAc,QACvB,QAAQ,cAAc;EAMnB,UAAU,SAAS,UAAU;;UAQrB;UA4BN;UAIM;;;;;KCpJJ,gBAAgB,iBAAiB,iBAC3C,SAAS,UACT,iBACA,WAAW,kBACR;;KAGO,YAAY,iBAAiB;EAEvC,OAAO,SAAS,WAAW;;;;;;;qBAQhB,cAAc,iBAAiB;mBAQvB;mBACA;mBACA;mBACA;;;;;;;EAJnB,YACmB,KAAK,YAAY,WACjB,aAAa,KAAK,aAAa,yBAC/B,QAAQ,EAAE,QAAQ,WAClB,eAAc,gBAAgB;;EAI3C,aAAa,OAAO,eAAe;UAc3B;;UA2BA;;UAMA;;;;;UAaN;;;;;;;;;qBClGG;WACX;WACA;WACA;;;wBAIc,kBAAkB,SAAS;;KAK/B,aAAa;WAAuB,SAAS;WAAmB;;;wBAG5D,aAAa,iBAAiB,cAC5C,UAAU,cAAc,YACvB,aAAa;;wBAKA,YAAY,OAAO,aAAa;;wBAKhC,cAAc,UAC5B,OAAO,cAAc,aAAa,aACjC;;;;KCjCS;;wBA4CI,mBAAmB,iBAAiB;;qBASvC,uBAAuB;WACzB;WACA,gBAAgB;WAChB,WAAW;EAEpB,YAAY;aAAiB,WAAW;aAAyB;;;;;;;qBAWtD,8BAA8B;WAChC;WACA;WACA;WACA;WACA;EAET,YAAY;aACD;aACA;aACA;;;;;;;qBAeA,mCAAmC;WACrC;WACA;EAET,YAAY;;;;;KCrFF;WACG;WAAwB;;WAExB;WACA,gBAAgB,QAAQ;WACxB;;;;;;;;wBASC,iBAAiB,gBAAgB,kBAAkB;;;;wBCxBnD,cACd,SAAS,KAAK,gCACd,OAAO,KAAK,wBACX"}