better-ship 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{application-DYfcAy-Q.js → application-CIxo_BpK.js} +32 -3
- package/dist/application-CIxo_BpK.js.map +1 -0
- package/dist/application.d.ts +3 -3
- package/dist/application.js +2 -2
- package/dist/better-auth/apple-client-secret.d.ts +1 -1
- package/dist/better-auth/apple-client-secret.js +1 -1
- package/dist/better-auth/auth-client.js +1 -1
- package/dist/better-auth/rate-limit.d.ts +1 -1
- package/dist/better-auth/rate-limit.js +1 -1
- package/dist/better-auth.js +1 -1
- package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
- package/dist/cloudflare/auth-secondary-storage.js +1 -1
- package/dist/cloudflare/queue.d.ts +1 -1
- package/dist/cloudflare/queue.js +2 -2
- package/dist/cloudflare/scheduled.d.ts +1 -1
- package/dist/cloudflare/scheduled.js +1 -1
- package/dist/cloudflare.d.ts +3 -3
- package/dist/cloudflare.js +3 -3
- package/dist/{core-DC_4pOrI.js → core-DG_cC8YT.js} +110 -2
- package/dist/{core-DC_4pOrI.js.map → core-DG_cC8YT.js.map} +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/http.d.ts +19 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +35 -0
- package/dist/http.js.map +1 -0
- package/dist/{index-6XMPJu7u.d.ts → index-Bl0K1RQW.d.ts} +90 -2
- package/dist/index-Bl0K1RQW.d.ts.map +1 -0
- package/dist/{index-_Cihfq1J.d.ts → index-DwWxIdQP.d.ts} +137 -55
- package/dist/index-DwWxIdQP.d.ts.map +1 -0
- package/dist/postgres.d.ts +3 -3
- package/dist/postgres.js +3 -3
- package/dist/resend.d.ts +73 -0
- package/dist/resend.d.ts.map +1 -0
- package/dist/resend.js +284 -0
- package/dist/resend.js.map +1 -0
- package/dist/tanstack/server.d.ts +8 -0
- package/dist/tanstack/server.d.ts.map +1 -0
- package/dist/tanstack/server.js +16 -0
- package/dist/tanstack/server.js.map +1 -0
- package/dist/tanstack.d.ts +1 -8
- package/dist/tanstack.d.ts.map +1 -1
- package/dist/tanstack.js +15 -21
- package/dist/tanstack.js.map +1 -1
- package/dist/{unit-of-work-context-BFCmU92Z.js → unit-of-work-context-BPE1In2f.js} +2 -2
- package/dist/{unit-of-work-context-BFCmU92Z.js.map → unit-of-work-context-BPE1In2f.js.map} +1 -1
- package/dist/{unit-of-work-context-BWTJfYPW.d.ts → unit-of-work-context-D5ytJIa-.d.ts} +3 -3
- package/dist/{unit-of-work-context-BWTJfYPW.d.ts.map → unit-of-work-context-D5ytJIa-.d.ts.map} +1 -1
- package/dist/webhook-verification-BQ9qZnVK.js +18 -0
- package/dist/webhook-verification-BQ9qZnVK.js.map +1 -0
- package/dist/webhook-verification-sZtwTI5n.d.ts +29 -0
- package/dist/webhook-verification-sZtwTI5n.d.ts.map +1 -0
- package/package.json +10 -1
- package/src/application/email-service.ts +57 -0
- package/src/application/index.ts +16 -0
- package/src/application/record-email-event.ts +53 -0
- package/src/core/email-address.ts +7 -0
- package/src/core/email-deliverability.ts +31 -0
- package/src/core/email-idempotency-key.ts +34 -0
- package/src/core/email-service.error.ts +51 -0
- package/src/core/index.ts +18 -0
- package/src/core/record-email-event.ts +23 -0
- package/src/infrastructure/http/index.ts +6 -0
- package/src/infrastructure/http/webhook-handler.ts +35 -0
- package/src/infrastructure/http/webhook-verification.ts +27 -0
- package/src/infrastructure/resend/email-event.ts +142 -0
- package/src/infrastructure/resend/index.ts +3 -0
- package/src/infrastructure/resend/resend-email-service.ts +125 -0
- package/src/infrastructure/resend/resend-webhook.ts +70 -0
- package/src/infrastructure/tanstack/auth.middleware.ts +9 -3
- package/src/infrastructure/tanstack/error.middleware.ts +1 -1
- package/src/infrastructure/tanstack/index.ts +0 -1
- package/src/infrastructure/tanstack/server.ts +7 -0
- package/src/infrastructure/tanstack/session-cookies.ts +6 -4
- package/dist/application-DYfcAy-Q.js.map +0 -1
- package/dist/index-6XMPJu7u.d.ts.map +0 -1
- package/dist/index-_Cihfq1J.d.ts.map +0 -1
|
@@ -1,6 +1,35 @@
|
|
|
1
|
-
import { E as DatabaseError,
|
|
1
|
+
import { E as DatabaseError, Q as AppError, T as unreachable, a as createLogger } from "./core-DG_cC8YT.js";
|
|
2
2
|
import { panic } from "better-result";
|
|
3
3
|
|
|
4
|
+
//#region src/application/email-service.ts
|
|
5
|
+
/** Records every send for a test. Provider ids count up as `recorded-1`, `recorded-2`. */
|
|
6
|
+
var RecordingEmailService = class {
|
|
7
|
+
/** Sends in call order, including repeated idempotency keys. */
|
|
8
|
+
sent = [];
|
|
9
|
+
/** Record the send before returning its provider id. */
|
|
10
|
+
async sendEmail(email) {
|
|
11
|
+
this.sent.push(email);
|
|
12
|
+
return { providerId: `recorded-${this.sent.length}` };
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/application/record-email-event.ts
|
|
18
|
+
/**
|
|
19
|
+
* Record a bounce or a complaint for every recipient inside the command's unit of work.
|
|
20
|
+
* The command id is the provider's event id, so a redelivered webhook is `MessageAlreadyProcessedError`.
|
|
21
|
+
*/
|
|
22
|
+
async function recordEmailEvent(command, deps) {
|
|
23
|
+
await deps.uow.run(command.id, async ({ emailDeliverability }) => {
|
|
24
|
+
for (const address of command.recipients) if (command.event.type === "bounced") await emailDeliverability.recordBounce(address, {
|
|
25
|
+
type: command.event.bounceType,
|
|
26
|
+
subType: command.event.bounceSubType
|
|
27
|
+
});
|
|
28
|
+
else await emailDeliverability.recordComplaint(address);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
4
33
|
//#region src/application/message-already-processed.error.ts
|
|
5
34
|
/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */
|
|
6
35
|
var MessageAlreadyProcessedError = class MessageAlreadyProcessedError extends AppError {
|
|
@@ -127,5 +156,5 @@ var UnitOfWorkAlreadyActiveError = class extends AppError {
|
|
|
127
156
|
};
|
|
128
157
|
|
|
129
158
|
//#endregion
|
|
130
|
-
export { MessageAlreadyProcessedError as i, UnitOfWork as n, MessageBus as r, UnitOfWorkAlreadyActiveError as t };
|
|
131
|
-
//# sourceMappingURL=application-
|
|
159
|
+
export { recordEmailEvent as a, MessageAlreadyProcessedError as i, UnitOfWork as n, RecordingEmailService as o, MessageBus as r, UnitOfWorkAlreadyActiveError as t };
|
|
160
|
+
//# sourceMappingURL=application-CIxo_BpK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"application-CIxo_BpK.js","names":[],"sources":["../src/application/email-service.ts","../src/application/record-email-event.ts","../src/application/message-already-processed.error.ts","../src/application/message-bus.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import type { EmailIdempotencyKey } from '@/core'\n\n/** A media type such as `application/pdf`. */\nexport type MimeType = `${string}/${string}`\n\n/** A file to attach. Binary content is bytes; the adapter encodes it for the provider. */\nexport type EmailAttachment = {\n readonly filename: string\n readonly content: string | Uint8Array\n /** Optional MIME metadata; Resend accepts filename and content without it. */\n readonly contentType?: MimeType\n}\n\n/**\n * One transactional email. Resend accepts 50 recipients, 75 tags with ASCII names and values,\n * and 40 MB of attachments after base64. Sources: https://resend.com/docs/api-reference/emails/send-email\n * and https://resend.com/docs/dashboard/emails/tags\n */\nexport type OutboundEmail = {\n /** 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 /** The stable identity of this send. Resend deduplicates it for 24 hours. */\n readonly idempotencyKey: EmailIdempotencyKey\n /** Resend generates plain text from html when omitted. An empty string disables that generation. */\n readonly text?: string\n readonly replyTo?: string\n /** Labels the provider stores with the email and returns on every delivery event. */\n readonly tags?: Readonly<Record<string, string>>\n readonly attachments?: readonly EmailAttachment[]\n}\n\n/** The provider accepted the email and assigned its id. */\nexport type EmailAccepted = { readonly providerId: string }\n\n/**\n * The 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 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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"}
|
package/dist/application.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, q as MessageId } from "./index-
|
|
2
|
-
import { _ as
|
|
3
|
-
export { type CommandHandler, type CommandRegistry, type DomainCommand, type DomainEvent, type DomainMessage, type DomainQuery, type EventHandler, type EventRegistry, type Handlers, type IEventCollector, type IMessageBus, type IMessageStore, type IOutboxRelay, type ITransaction, type IUnitOfWork, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type QueryHandler, type QueryRegistry, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps };
|
|
1
|
+
import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, q as MessageId } from "./index-Bl0K1RQW.js";
|
|
2
|
+
import { A as RecordingEmailService, C as IMessageStore, D as IEmailService, E as EmailAttachment, M as EventHandler, N as QueryHandler, O as MimeType, S as IOutboxRelay, T as EmailAccepted, _ as recordEmailEvent, a as CommandRegistry, b as UnitOfWorkDeps, c as MessageRegistry, d as RegistryMessage, f as EmailBounceRecord, g as RecordEmailEventRepos, h as RecordEmailEventDeps, i as MessageBus, j as CommandHandler, k as OutboundEmail, l as MessageResult, m as RecordEmailEventCommand, n as MessageAlreadyProcessedError, o as EventRegistry, p as IEmailDeliverabilityStore, r as IMessageBus, s as Handlers, t as UnitOfWorkAlreadyActiveError, u as QueryRegistry, v as IUnitOfWork, w as IEventCollector, x as ITransaction, y as UnitOfWork } from "./index-DwWxIdQP.js";
|
|
3
|
+
export { type CommandHandler, type CommandRegistry, type DomainCommand, type DomainEvent, type DomainMessage, type DomainQuery, type EmailAccepted, type EmailAttachment, type EmailBounceRecord, type EventHandler, type EventRegistry, type Handlers, type IEmailDeliverabilityStore, type IEmailService, type IEventCollector, type IMessageBus, type IMessageStore, type IOutboxRelay, type ITransaction, type IUnitOfWork, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type MimeType, type OutboundEmail, type QueryHandler, type QueryRegistry, type RecordEmailEventCommand, type RecordEmailEventDeps, type RecordEmailEventRepos, RecordingEmailService, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps, recordEmailEvent };
|
package/dist/application.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as MessageAlreadyProcessedError, n as UnitOfWork, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-
|
|
1
|
+
import { a as recordEmailEvent, i as MessageAlreadyProcessedError, n as UnitOfWork, o as RecordingEmailService, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-CIxo_BpK.js";
|
|
2
2
|
|
|
3
|
-
export { MessageAlreadyProcessedError, MessageBus, UnitOfWork, UnitOfWorkAlreadyActiveError };
|
|
3
|
+
export { MessageAlreadyProcessedError, MessageBus, RecordingEmailService, UnitOfWork, UnitOfWorkAlreadyActiveError, recordEmailEvent };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ut as AppError } from "../index-Bl0K1RQW.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 {
|
|
1
|
+
import { Ut as AppError } from "../index-Bl0K1RQW.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. */
|
package/dist/better-auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as createLogger } from "./core-
|
|
1
|
+
import { a as createLogger } from "./core-DG_cC8YT.js";
|
|
2
2
|
import { v7 } from "uuid";
|
|
3
3
|
import { isAPIError } from "better-auth/api";
|
|
4
4
|
import { anonymous, bearer, captcha, deviceAuthorization, emailOTP, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ut as AppError } from "../index-Bl0K1RQW.js";
|
|
2
2
|
import { DurableObject } from "cloudflare:workers";
|
|
3
3
|
//#region src/infrastructure/cloudflare/auth-secondary-storage.d.ts
|
|
4
4
|
/** The supplied TTL cannot produce a valid expiry timestamp. Retrying the same input cannot fix it. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { U as DomainCommand, Ut as AppError, W as DomainEvent, Wt as FailureClassification } from "../index-Bl0K1RQW.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/infrastructure/cloudflare/queue/queue-message.d.ts
|
|
4
4
|
/** A message a queue carries: a command from outside the request, or an event from the outbox. */
|
package/dist/cloudflare/queue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as MessageAlreadyProcessedError } from "../application-
|
|
1
|
+
import { Q as AppError, a as createLogger, b as shouldRetryFailure, c as exponentialEqualJitterMs, d as classifyFailure, f as retryAfterSecondsOf, l as spreadJitterMs, u as classifyBoundaryError } from "../core-DG_cC8YT.js";
|
|
2
|
+
import { i as MessageAlreadyProcessedError } from "../application-CIxo_BpK.js";
|
|
3
3
|
import { Result } from "better-result";
|
|
4
4
|
|
|
5
5
|
//#region src/infrastructure/cloudflare/queue/queue-limits.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { q as MessageId } from "../index-
|
|
1
|
+
import { q as MessageId } from "../index-Bl0K1RQW.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/infrastructure/cloudflare/scheduled/scheduled-handler.d.ts
|
|
4
4
|
/** The work one schedule runs. Nothing returns to the platform, so each run reports its own outcome. */
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { W as DomainEvent, et as D1ConstraintKind, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-
|
|
2
|
-
import {
|
|
3
|
-
import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-
|
|
1
|
+
import { W as DomainEvent, et as D1ConstraintKind, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-Bl0K1RQW.js";
|
|
2
|
+
import { C as IMessageStore, x as ITransaction } from "./index-DwWxIdQP.js";
|
|
3
|
+
import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-D5ytJIa-.js";
|
|
4
4
|
import { BatchItem } from "drizzle-orm/batch";
|
|
5
5
|
import { AnyD1Database, DrizzleD1Database } from "drizzle-orm/d1";
|
|
6
6
|
//#region src/infrastructure/cloudflare/d1-database-types.d.ts
|
package/dist/cloudflare.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { E as DatabaseError,
|
|
2
|
-
import { i as MessageAlreadyProcessedError } from "./application-
|
|
3
|
-
import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-
|
|
1
|
+
import { E as DatabaseError, Q as AppError } from "./core-DG_cC8YT.js";
|
|
2
|
+
import { i as MessageAlreadyProcessedError } from "./application-CIxo_BpK.js";
|
|
3
|
+
import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-BPE1In2f.js";
|
|
4
4
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
5
5
|
import { eq, sql } from "drizzle-orm";
|
|
6
6
|
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
@@ -17,6 +17,114 @@ var AppError = class extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/core/email-address.ts
|
|
22
|
+
/** A parsed email address for message payloads and webhook recipients. Ported from Typist's `EmailSchema`. */
|
|
23
|
+
const EmailAddressSchema = z.email().brand();
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/core/email-idempotency-key.ts
|
|
27
|
+
const MAX_KEY_LENGTH = 256;
|
|
28
|
+
/** A key outside 1 to 256 characters. Keys come from stable ids, so this is a defect, not a retry. */
|
|
29
|
+
var InvalidEmailIdempotencyKeyError = class extends AppError {
|
|
30
|
+
length;
|
|
31
|
+
_tag = "InvalidEmailIdempotencyKeyError";
|
|
32
|
+
classification = "terminal";
|
|
33
|
+
constructor(length) {
|
|
34
|
+
super(`Email idempotency key must have 1 to ${MAX_KEY_LENGTH} characters`);
|
|
35
|
+
this.length = length;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* `<scope>/<id>` for one logical send. Resend deduplicates it for 24 hours.
|
|
40
|
+
* Derive `id` from a stable identity: a message id, a job id, a user id with a template name.
|
|
41
|
+
* The same key must map to an identical payload; a changed body is rejected as terminal.
|
|
42
|
+
* A random `id` means every request is a deliberate new send, as a sign-in code resend is.
|
|
43
|
+
* Source: https://resend.com/docs/dashboard/emails/idempotency-keys
|
|
44
|
+
*/
|
|
45
|
+
function emailIdempotencyKey(scope, id) {
|
|
46
|
+
const key = `${scope}/${id}`;
|
|
47
|
+
if (scope.length === 0 || id.length === 0 || key.length > MAX_KEY_LENGTH) throw new InvalidEmailIdempotencyKeyError(key.length);
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/core/email-service.error.ts
|
|
53
|
+
const CLASSIFICATION_BY_REASON = {
|
|
54
|
+
rate_limited: "transient",
|
|
55
|
+
idempotency_conflict: "transient",
|
|
56
|
+
provider_error: "transient",
|
|
57
|
+
quota_exceeded: "terminal",
|
|
58
|
+
invalid_request: "terminal",
|
|
59
|
+
configuration: "terminal"
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The provider rejected a send. The boundary that awaited the send logs once; the adapter never does.
|
|
63
|
+
* `retryAfter` is the server's delay in seconds when it sent one, so `retryAfterSecondsOf` reads it.
|
|
64
|
+
*/
|
|
65
|
+
var EmailServiceError = class extends AppError {
|
|
66
|
+
_tag = "EmailServiceError";
|
|
67
|
+
classification;
|
|
68
|
+
reason;
|
|
69
|
+
provider;
|
|
70
|
+
providerCode;
|
|
71
|
+
statusCode;
|
|
72
|
+
retryAfter;
|
|
73
|
+
constructor(args) {
|
|
74
|
+
super(`Email send failed: ${args.provider} ${args.providerCode}`, { cause: args.cause });
|
|
75
|
+
this.reason = args.reason;
|
|
76
|
+
this.classification = CLASSIFICATION_BY_REASON[args.reason];
|
|
77
|
+
this.provider = args.provider;
|
|
78
|
+
this.providerCode = args.providerCode;
|
|
79
|
+
this.statusCode = args.statusCode;
|
|
80
|
+
this.retryAfter = args.retryAfter;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/core/email-deliverability.ts
|
|
86
|
+
/** The bounce classes Resend reports, ported from Typist's comm-eligibility.ts. */
|
|
87
|
+
const BounceTypeSchema = z.enum([
|
|
88
|
+
"Permanent",
|
|
89
|
+
"Transient",
|
|
90
|
+
"Undetermined"
|
|
91
|
+
]);
|
|
92
|
+
/** Parse a provider's bounce type. An unknown value is `Undetermined`, which suppresses. */
|
|
93
|
+
function parseBounceType(value) {
|
|
94
|
+
const parsed = BounceTypeSchema.safeParse(value);
|
|
95
|
+
return parsed.success ? parsed.data : "Undetermined";
|
|
96
|
+
}
|
|
97
|
+
const BOUNCE_SEVERITY = {
|
|
98
|
+
Transient: 1,
|
|
99
|
+
Undetermined: 2,
|
|
100
|
+
Permanent: 3
|
|
101
|
+
};
|
|
102
|
+
/** Keep the higher severity. Equal severity permits replacement; callers own timestamp ordering. */
|
|
103
|
+
function shouldUpdateBounce(incoming, current) {
|
|
104
|
+
if (current === null) return true;
|
|
105
|
+
return BOUNCE_SEVERITY[incoming] >= BOUNCE_SEVERITY[current];
|
|
106
|
+
}
|
|
107
|
+
/** Typist's send policy: no complaint, and either no bounce or a transient one. Delivery is not guaranteed. */
|
|
108
|
+
function isEmailDeliverable(bounceType, complained) {
|
|
109
|
+
if (complained) return false;
|
|
110
|
+
return bounceType === null || bounceType === "Transient";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/core/record-email-event.ts
|
|
115
|
+
/**
|
|
116
|
+
* The payload of the command an application registers to record a bounce or a complaint.
|
|
117
|
+
* Register it under a name of the application's choice in `defineMessages`; `recordEmailEvent` handles it.
|
|
118
|
+
*/
|
|
119
|
+
const RecordEmailEventPayloadSchema = z.object({
|
|
120
|
+
recipients: z.array(EmailAddressSchema).min(1),
|
|
121
|
+
event: z.discriminatedUnion("type", [z.object({
|
|
122
|
+
type: z.literal("bounced"),
|
|
123
|
+
bounceType: BounceTypeSchema,
|
|
124
|
+
bounceSubType: z.string().nullable()
|
|
125
|
+
}), z.object({ type: z.literal("complained") })])
|
|
126
|
+
});
|
|
127
|
+
|
|
20
128
|
//#endregion
|
|
21
129
|
//#region src/core/authentication.error.ts
|
|
22
130
|
/** A protected operation requires a session that the caller did not provide. */
|
|
@@ -795,5 +903,5 @@ function createLogger(module, config) {
|
|
|
795
903
|
}
|
|
796
904
|
|
|
797
905
|
//#endregion
|
|
798
|
-
export { ValidationProblemDetailsSchema as A, StaleSessionError as B, defineId as C, defineErrors as D, DatabaseError as E, UpgradeRequiredError as F, AuthenticationError as H, ValidationError as I, InternalServerError as L, MalformedRequestError as M, RequestTimeoutError as N, sharedErrors as O, ResourceNotFoundError as P, ServiceUnavailableError as R, defineMessages as S, unreachable as T,
|
|
799
|
-
//# sourceMappingURL=core-
|
|
906
|
+
export { ValidationProblemDetailsSchema as A, StaleSessionError as B, defineId as C, defineErrors as D, DatabaseError as E, UpgradeRequiredError as F, isEmailDeliverable as G, AuthenticationError as H, ValidationError as I, EmailServiceError as J, parseBounceType as K, InternalServerError as L, MalformedRequestError as M, RequestTimeoutError as N, sharedErrors as O, ResourceNotFoundError as P, AppError as Q, ServiceUnavailableError as R, defineMessages as S, unreachable as T, RecordEmailEventPayloadSchema as U, RateLimitError as V, BounceTypeSchema as W, emailIdempotencyKey as X, InvalidEmailIdempotencyKeyError as Y, EmailAddressSchema as Z, isTransientCloudflareRuntimeError as _, createLogger as a, shouldRetryFailure as b, exponentialEqualJitterMs as c, classifyFailure as d, retryAfterSecondsOf as f, isCloudflareRetryableError as g, isCloudflareOverloadedError as h, consoleSink as i, ValidationProblemErrorSchema as j, ProblemDetailsSchema as k, spreadJitterMs as l, isCloudflareObjectResetError as m, Logger as n, setLogSink as o, CLOUDFLARE_TRANSIENT_ERROR_PATTERNS as p, shouldUpdateBounce as q, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, classifyBoundaryError as u, isAbortError as v, notImplemented as w, Entity as x, isTransientTransportError as y, NotAuthorizedError as z };
|
|
907
|
+
//# sourceMappingURL=core-DG_cC8YT.js.map
|