better-ship 0.6.0 → 0.8.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-DjsQtpgw.js → application-DYfcAy-Q.js} +2 -2
- package/dist/{application-DjsQtpgw.js.map → application-DYfcAy-Q.js.map} +1 -1
- package/dist/application.d.ts +2 -2
- package/dist/application.js +1 -1
- 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.d.ts +4 -2
- package/dist/better-auth.d.ts.map +1 -1
- package/dist/better-auth.js +22 -6
- package/dist/better-auth.js.map +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 +175 -0
- package/dist/cloudflare/queue.d.ts.map +1 -0
- package/dist/cloudflare/queue.js +419 -0
- package/dist/cloudflare/queue.js.map +1 -0
- package/dist/cloudflare.d.ts +3 -3
- package/dist/cloudflare.js +3 -3
- package/dist/{core-Cx5SeyKV.js → core-DC_4pOrI.js} +92 -2
- package/dist/core-DC_4pOrI.js.map +1 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/{index-BStnggQO.d.ts → index-6XMPJu7u.d.ts} +49 -2
- package/dist/index-6XMPJu7u.d.ts.map +1 -0
- package/dist/{index-DWIRrJMP.d.ts → index-_Cihfq1J.d.ts} +2 -2
- package/dist/{index-DWIRrJMP.d.ts.map → index-_Cihfq1J.d.ts.map} +1 -1
- package/dist/postgres.d.ts +3 -3
- package/dist/postgres.js +3 -3
- package/dist/tanstack.d.ts +1 -1
- package/dist/tanstack.js +1 -1
- package/dist/{unit-of-work-context-DToDpAkv.js → unit-of-work-context-BFCmU92Z.js} +2 -2
- package/dist/{unit-of-work-context-DToDpAkv.js.map → unit-of-work-context-BFCmU92Z.js.map} +1 -1
- package/dist/{unit-of-work-context-CPz9rdSS.d.ts → unit-of-work-context-BWTJfYPW.d.ts} +3 -3
- package/dist/{unit-of-work-context-CPz9rdSS.d.ts.map → unit-of-work-context-BWTJfYPW.d.ts.map} +1 -1
- package/package.json +2 -1
- package/src/core/backoff.ts +29 -0
- package/src/core/classify-failure.ts +60 -0
- package/src/core/cloudflare-classification.ts +45 -0
- package/src/core/index.ts +14 -0
- package/src/infrastructure/better-auth/options.ts +29 -8
- package/src/infrastructure/cloudflare/queue/index.ts +25 -0
- package/src/infrastructure/cloudflare/queue/queue-client.ts +177 -0
- package/src/infrastructure/cloudflare/queue/queue-consumer.ts +146 -0
- package/src/infrastructure/cloudflare/queue/queue-limits.ts +54 -0
- package/src/infrastructure/cloudflare/queue/queue-message.ts +4 -0
- package/src/infrastructure/cloudflare/queue/queue.errors.ts +105 -0
- package/src/infrastructure/cloudflare/queue/redelivery-policy.ts +64 -0
- package/dist/core-Cx5SeyKV.js.map +0 -1
- package/dist/index-BStnggQO.d.ts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as DatabaseError, T as unreachable, U as AppError, a as createLogger } from "./core-DC_4pOrI.js";
|
|
2
2
|
import { panic } from "better-result";
|
|
3
3
|
|
|
4
4
|
//#region src/application/message-already-processed.error.ts
|
|
@@ -128,4 +128,4 @@ var UnitOfWorkAlreadyActiveError = class extends AppError {
|
|
|
128
128
|
|
|
129
129
|
//#endregion
|
|
130
130
|
export { MessageAlreadyProcessedError as i, UnitOfWork as n, MessageBus as r, UnitOfWorkAlreadyActiveError as t };
|
|
131
|
-
//# sourceMappingURL=application-
|
|
131
|
+
//# sourceMappingURL=application-DYfcAy-Q.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application-DjsQtpgw.js","names":[],"sources":["../src/application/message-already-processed.error.ts","../src/application/message-bus.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import { AppError, DatabaseError, type MessageId } from '@/core'\n\n/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */\nexport class MessageAlreadyProcessedError extends AppError {\n readonly _tag = 'MessageAlreadyProcessedError'\n readonly classification = 'terminal'\n\n constructor(\n readonly messageId: MessageId,\n options?: { readonly cause: unknown },\n ) {\n super(`message ${messageId} already processed`, options)\n }\n\n /** Recognize only the processed-message primary key; preserve other failures unchanged at the call site. */\n static from(cause: unknown, messageId: MessageId): MessageAlreadyProcessedError | undefined {\n if (cause instanceof MessageAlreadyProcessedError) return cause\n if (!(cause instanceof DatabaseError)) return undefined\n const details = cause.details\n const duplicate =\n details.database === 'd1'\n ? details.kind === 'unique' &&\n details.table === 'processed_messages' &&\n details.column === 'message_id'\n : details.code === '23505' &&\n details.table_name === 'processed_messages' &&\n details.constraint_name === 'processed_messages_pkey'\n if (!duplicate) return undefined\n return new MessageAlreadyProcessedError(messageId, { cause })\n }\n}\n","import { panic } from 'better-result'\n\nimport {\n createLogger,\n unreachable,\n type DomainCommand,\n type DomainEvent,\n type DomainMessage,\n type DomainQuery,\n} from '@/core'\n\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { Handlers, MessageResult, RegistryMessage } from './registry.ts'\n\nconst logger = createLogger('message-bus')\n\n/** The port an entrypoint dispatches through. `AppDeps` names this as `IMessageBus<typeof registry>`. */\nexport interface IMessageBus<TRegistry extends Handlers<never>> {\n handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>>\n}\n\n/**\n * Routes one message to its slot. The bus opens no transaction and keeps no outbox:\n * a handler that writes opens the unit of work itself.\n */\nexport class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMessageBus<TRegistry> {\n constructor(\n private readonly deps: Deps,\n private readonly registry: TRegistry,\n ) {}\n\n /** Resolves to the handler's own value for a command or query, and to void for an event. */\n async handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>> {\n // SAFETY: the slot for this name is what `MessageResult` names, and dispatch returns its value.\n return (await this.dispatch(message)) as MessageResult<TMessage, TRegistry>\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- one table holds every slot's value; `handle` names it.\n private dispatch(message: DomainMessage): Promise<unknown> {\n if (message.type === 'command') return this.handleCommand(message)\n if (message.type === 'query') return this.handleQuery(message)\n if (message.type === 'event') return this.handleEvent(message)\n // A message parsed at a boundary cannot reach here; one that does must not resolve silently.\n return unreachable(message)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleCommand(command: DomainCommand): Promise<unknown> {\n // `satisfies` proved the registry complete, so a missing slot is a broken registry object.\n const handler = this.registry.commands[command.name]\n if (handler === undefined) return panic(`No handler for command ${command.name}`)\n logger.debug('handling_command', { name: command.name, id: command.id })\n return handler(command, this.deps)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleQuery(query: DomainQuery): Promise<unknown> {\n const handler = this.registry.queries[query.name]\n if (handler === undefined) return panic(`No handler for query ${query.name}`)\n return handler(query, this.deps)\n }\n\n /**\n * Every subscriber runs to completion. One failure is rethrown as is. Several are thrown as\n * one `AggregateError`. A `MessageAlreadyProcessedError` means that subscriber already committed on\n * an earlier delivery, so it counts as delivered.\n */\n private async handleEvent(event: DomainEvent): Promise<void> {\n const subscriptions = this.registry.events[event.name] ?? []\n const outcomes = await Promise.allSettled(\n subscriptions.map((subscription) => subscription(event, this.deps)),\n )\n const failures: unknown[] = []\n for (const [subscriber, outcome] of outcomes.entries()) {\n if (outcome.status === 'fulfilled') continue\n if (outcome.reason instanceof MessageAlreadyProcessedError) {\n // The bus swallows this error, so this line is the only record that the skip happened.\n logger.info('duplicate_subscriber_ignored', { event: event.name, id: event.id, subscriber })\n continue\n }\n failures.push(outcome.reason)\n }\n if (failures.length === 1) throw failures[0]\n if (failures.length > 1) {\n throw new AggregateError(failures, `${event.name}: ${failures.length} subscribers failed`)\n }\n }\n}\n","import type { MessageId } from '@/core'\n\nimport type { IEventCollector } from './event-collector.ts'\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { IMessageStore } from './message-store.ts'\nimport type { IOutboxRelay } from './outbox-relay.ts'\nimport type { ITransaction } from './transaction.ts'\n\n/**\n * Commits the processed-message record, repository writes, and emitted events together before resolving.\n * An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.\n * Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.\n */\nexport interface IUnitOfWork<Repos> {\n run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>\n}\n\nexport type UnitOfWorkDeps<Tx, Repos> = {\n readonly transaction: ITransaction<Tx>\n /** The processed-messages and outbox repository for this transaction. */\n readonly messageStore: (tx: Tx) => IMessageStore\n /** Repositories participate in the current transaction. */\n readonly createRepositories: (tx: Tx) => Repos\n readonly eventCollector: IEventCollector\n readonly relay: IOutboxRelay\n}\n\n/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */\nexport class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {\n constructor(private readonly deps: UnitOfWorkDeps<Tx, Repos>) {}\n\n async run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T> {\n const { transaction, messageStore, createRepositories, eventCollector, relay } = this.deps\n let written = 0\n\n let value: T\n try {\n value = await transaction.transaction(async (tx) => {\n const store = messageStore(tx)\n await store.claim(messageId)\n const repos = createRepositories(tx)\n const result = await work(repos)\n const pending = eventCollector.collect()\n await store.persistEvents(pending)\n written = pending.length\n return result\n })\n } catch (error) {\n throw MessageAlreadyProcessedError.from(error, messageId) ?? error\n }\n\n if (written > 0) relay.wake()\n return value\n }\n}\n","import { AppError } from '@/core'\n\n/** A boundary was opened inside another. One message, one atomic scope. */\nexport class UnitOfWorkAlreadyActiveError extends AppError {\n readonly _tag = 'UnitOfWorkAlreadyActiveError'\n readonly classification = 'terminal'\n\n constructor() {\n super('a unit of work is already active')\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"application-DYfcAy-Q.js","names":[],"sources":["../src/application/message-already-processed.error.ts","../src/application/message-bus.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import { AppError, DatabaseError, type MessageId } from '@/core'\n\n/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */\nexport class MessageAlreadyProcessedError extends AppError {\n readonly _tag = 'MessageAlreadyProcessedError'\n readonly classification = 'terminal'\n\n constructor(\n readonly messageId: MessageId,\n options?: { readonly cause: unknown },\n ) {\n super(`message ${messageId} already processed`, options)\n }\n\n /** Recognize only the processed-message primary key; preserve other failures unchanged at the call site. */\n static from(cause: unknown, messageId: MessageId): MessageAlreadyProcessedError | undefined {\n if (cause instanceof MessageAlreadyProcessedError) return cause\n if (!(cause instanceof DatabaseError)) return undefined\n const details = cause.details\n const duplicate =\n details.database === 'd1'\n ? details.kind === 'unique' &&\n details.table === 'processed_messages' &&\n details.column === 'message_id'\n : details.code === '23505' &&\n details.table_name === 'processed_messages' &&\n details.constraint_name === 'processed_messages_pkey'\n if (!duplicate) return undefined\n return new MessageAlreadyProcessedError(messageId, { cause })\n }\n}\n","import { panic } from 'better-result'\n\nimport {\n createLogger,\n unreachable,\n type DomainCommand,\n type DomainEvent,\n type DomainMessage,\n type DomainQuery,\n} from '@/core'\n\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { Handlers, MessageResult, RegistryMessage } from './registry.ts'\n\nconst logger = createLogger('message-bus')\n\n/** The port an entrypoint dispatches through. `AppDeps` names this as `IMessageBus<typeof registry>`. */\nexport interface IMessageBus<TRegistry extends Handlers<never>> {\n handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>>\n}\n\n/**\n * Routes one message to its slot. The bus opens no transaction and keeps no outbox:\n * a handler that writes opens the unit of work itself.\n */\nexport class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMessageBus<TRegistry> {\n constructor(\n private readonly deps: Deps,\n private readonly registry: TRegistry,\n ) {}\n\n /** Resolves to the handler's own value for a command or query, and to void for an event. */\n async handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>> {\n // SAFETY: the slot for this name is what `MessageResult` names, and dispatch returns its value.\n return (await this.dispatch(message)) as MessageResult<TMessage, TRegistry>\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- one table holds every slot's value; `handle` names it.\n private dispatch(message: DomainMessage): Promise<unknown> {\n if (message.type === 'command') return this.handleCommand(message)\n if (message.type === 'query') return this.handleQuery(message)\n if (message.type === 'event') return this.handleEvent(message)\n // A message parsed at a boundary cannot reach here; one that does must not resolve silently.\n return unreachable(message)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleCommand(command: DomainCommand): Promise<unknown> {\n // `satisfies` proved the registry complete, so a missing slot is a broken registry object.\n const handler = this.registry.commands[command.name]\n if (handler === undefined) return panic(`No handler for command ${command.name}`)\n logger.debug('handling_command', { name: command.name, id: command.id })\n return handler(command, this.deps)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleQuery(query: DomainQuery): Promise<unknown> {\n const handler = this.registry.queries[query.name]\n if (handler === undefined) return panic(`No handler for query ${query.name}`)\n return handler(query, this.deps)\n }\n\n /**\n * Every subscriber runs to completion. One failure is rethrown as is. Several are thrown as\n * one `AggregateError`. A `MessageAlreadyProcessedError` means that subscriber already committed on\n * an earlier delivery, so it counts as delivered.\n */\n private async handleEvent(event: DomainEvent): Promise<void> {\n const subscriptions = this.registry.events[event.name] ?? []\n const outcomes = await Promise.allSettled(\n subscriptions.map((subscription) => subscription(event, this.deps)),\n )\n const failures: unknown[] = []\n for (const [subscriber, outcome] of outcomes.entries()) {\n if (outcome.status === 'fulfilled') continue\n if (outcome.reason instanceof MessageAlreadyProcessedError) {\n // The bus swallows this error, so this line is the only record that the skip happened.\n logger.info('duplicate_subscriber_ignored', { event: event.name, id: event.id, subscriber })\n continue\n }\n failures.push(outcome.reason)\n }\n if (failures.length === 1) throw failures[0]\n if (failures.length > 1) {\n throw new AggregateError(failures, `${event.name}: ${failures.length} subscribers failed`)\n }\n }\n}\n","import type { MessageId } from '@/core'\n\nimport type { IEventCollector } from './event-collector.ts'\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { IMessageStore } from './message-store.ts'\nimport type { IOutboxRelay } from './outbox-relay.ts'\nimport type { ITransaction } from './transaction.ts'\n\n/**\n * Commits the processed-message record, repository writes, and emitted events together before resolving.\n * An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.\n * Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.\n */\nexport interface IUnitOfWork<Repos> {\n run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>\n}\n\nexport type UnitOfWorkDeps<Tx, Repos> = {\n readonly transaction: ITransaction<Tx>\n /** The processed-messages and outbox repository for this transaction. */\n readonly messageStore: (tx: Tx) => IMessageStore\n /** Repositories participate in the current transaction. */\n readonly createRepositories: (tx: Tx) => Repos\n readonly eventCollector: IEventCollector\n readonly relay: IOutboxRelay\n}\n\n/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */\nexport class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {\n constructor(private readonly deps: UnitOfWorkDeps<Tx, Repos>) {}\n\n async run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T> {\n const { transaction, messageStore, createRepositories, eventCollector, relay } = this.deps\n let written = 0\n\n let value: T\n try {\n value = await transaction.transaction(async (tx) => {\n const store = messageStore(tx)\n await store.claim(messageId)\n const repos = createRepositories(tx)\n const result = await work(repos)\n const pending = eventCollector.collect()\n await store.persistEvents(pending)\n written = pending.length\n return result\n })\n } catch (error) {\n throw MessageAlreadyProcessedError.from(error, messageId) ?? error\n }\n\n if (written > 0) relay.wake()\n return value\n }\n}\n","import { AppError } from '@/core'\n\n/** A boundary was opened inside another. One message, one atomic scope. */\nexport class UnitOfWorkAlreadyActiveError extends AppError {\n readonly _tag = 'UnitOfWorkAlreadyActiveError'\n readonly classification = 'terminal'\n\n constructor() {\n super('a unit of work is already active')\n }\n}\n"],"mappings":"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"}
|
package/dist/application.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as RegistryMessage, a as UnitOfWorkDeps, b as EventHandler, c as IOutboxRelay, d as CommandRegistry, f as EventRegistry, g as QueryRegistry, h as MessageResult, i as UnitOfWork, l as IMessageBus, m as MessageRegistry, n as MessageAlreadyProcessedError, o as IEventCollector, p as Handlers, r as IUnitOfWork, s as ITransaction, t as UnitOfWorkAlreadyActiveError, u as MessageBus, v as IMessageStore, x as QueryHandler, y as CommandHandler } from "./index-
|
|
1
|
+
import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, q as MessageId } from "./index-6XMPJu7u.js";
|
|
2
|
+
import { _ as RegistryMessage, a as UnitOfWorkDeps, b as EventHandler, c as IOutboxRelay, d as CommandRegistry, f as EventRegistry, g as QueryRegistry, h as MessageResult, i as UnitOfWork, l as IMessageBus, m as MessageRegistry, n as MessageAlreadyProcessedError, o as IEventCollector, p as Handlers, r as IUnitOfWork, s as ITransaction, t as UnitOfWorkAlreadyActiveError, u as MessageBus, v as IMessageStore, x as QueryHandler, y as CommandHandler } from "./index-_Cihfq1J.js";
|
|
3
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 };
|
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 { i as MessageAlreadyProcessedError, n as UnitOfWork, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-DYfcAy-Q.js";
|
|
2
2
|
|
|
3
3
|
export { MessageAlreadyProcessedError, MessageBus, UnitOfWork, UnitOfWorkAlreadyActiveError };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kt as AppError } from "../index-6XMPJu7u.js";
|
|
2
2
|
//#region src/infrastructure/better-auth/apple-client-secret.d.ts
|
|
3
3
|
/** Apple sends its OAuth callback from this origin. Include it in trustedOrigins when enabling Apple. */
|
|
4
4
|
export declare const APPLE_ORIGIN = "https://appleid.apple.com";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kt as AppError } from "../index-6XMPJu7u.js";
|
|
2
2
|
import { BetterAuthRateLimitStorage } from "better-auth";
|
|
3
3
|
//#region src/infrastructure/better-auth/rate-limit.d.ts
|
|
4
4
|
/** Each rule must match its binding's deployed limit and period. Cloudflare accepts only 10 or 60 seconds. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { U as AppError, a as createLogger } from "../core-DC_4pOrI.js";
|
|
2
2
|
|
|
3
3
|
//#region src/infrastructure/better-auth/rate-limit.ts
|
|
4
4
|
/** A binding failure with no known retry classification. The cause preserves the original failure. */
|
package/dist/better-auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CloudflareTurnstileOptions, LastLoginMethodOptions, anonymous, bearer, captcha, deviceAuthorization, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
1
|
+
import { CloudflareTurnstileOptions, EmailOTPOptions, LastLoginMethodOptions, anonymous, bearer, captcha, deviceAuthorization, emailOTP, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
2
2
|
import { BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitStorage, DBAdapterInstance } from "better-auth";
|
|
3
3
|
//#region src/infrastructure/better-auth/options.d.ts
|
|
4
4
|
/**
|
|
@@ -35,6 +35,7 @@ export declare const authDefaults: {
|
|
|
35
35
|
/** Configure built-in plugins with native options. Runtime auth requires captcha; lastLoginMethod is enabled unless explicitly disabled. */
|
|
36
36
|
export type AuthPluginOptions = {
|
|
37
37
|
readonly magicLink?: Parameters<typeof magicLink>[0];
|
|
38
|
+
readonly emailOTP?: EmailOTPOptions;
|
|
38
39
|
readonly anonymous?: NonNullable<Parameters<typeof anonymous>[0]>;
|
|
39
40
|
readonly captcha?: Omit<CloudflareTurnstileOptions, 'provider'>;
|
|
40
41
|
readonly lastLoginMethod?: false | NonNullable<Parameters<typeof lastLoginMethod>[0]>;
|
|
@@ -61,6 +62,7 @@ export type AuthConfig = Omit<BetterAuthOptions, 'database' | 'plugins' | 'rateL
|
|
|
61
62
|
});
|
|
62
63
|
type PluginTypes = {
|
|
63
64
|
magicLink: ReturnType<typeof magicLink>;
|
|
65
|
+
emailOTP: ReturnType<typeof emailOTP>;
|
|
64
66
|
anonymous: ReturnType<typeof anonymous>;
|
|
65
67
|
captcha: ReturnType<typeof captcha>;
|
|
66
68
|
lastLoginMethod: ReturnType<typeof lastLoginMethod<{}>>;
|
|
@@ -72,7 +74,7 @@ type LastLoginPlugin<Options extends AuthPluginOptions> = Options extends {
|
|
|
72
74
|
readonly lastLoginMethod: infer Settings extends LastLoginMethodOptions;
|
|
73
75
|
} ? ReturnType<typeof lastLoginMethod<Settings>> : PluginTypes['lastLoginMethod'];
|
|
74
76
|
type SelectedPlugin<Options extends AuthPluginOptions, Key extends keyof PluginTypes> = Options extends Record<Key, false | undefined> ? [] : Options extends Record<Key, NonNullable<AuthPluginOptions[Key]>> ? [Key extends 'lastLoginMethod' ? LastLoginPlugin<Options> : PluginTypes[Key]] : Key extends 'lastLoginMethod' ? [PluginTypes[Key]] : [];
|
|
75
|
-
type SelectedPlugins<Options extends AuthPluginOptions> = [...SelectedPlugin<Options, 'captcha'>, ...SelectedPlugin<Options, 'lastLoginMethod'>, ...SelectedPlugin<Options, 'openAPI'>, ...SelectedPlugin<Options, 'magicLink'>, ...SelectedPlugin<Options, 'anonymous'>, ...SelectedPlugin<Options, 'deviceAuthorization'>, ...SelectedPlugin<Options, 'bearer'>];
|
|
77
|
+
type SelectedPlugins<Options extends AuthPluginOptions> = [...SelectedPlugin<Options, 'captcha'>, ...SelectedPlugin<Options, 'lastLoginMethod'>, ...SelectedPlugin<Options, 'openAPI'>, ...SelectedPlugin<Options, 'magicLink'>, ...SelectedPlugin<Options, 'emailOTP'>, ...SelectedPlugin<Options, 'anonymous'>, ...SelectedPlugin<Options, 'deviceAuthorization'>, ...SelectedPlugin<Options, 'bearer'>];
|
|
76
78
|
type ConfiguredPlugins<Config extends AuthConfig> = SelectedPlugins<Config['integrations']>;
|
|
77
79
|
type MergeOptions<Defaults, Overrides> = Omit<Defaults, keyof Overrides> & Overrides;
|
|
78
80
|
type SessionOptions<Config extends AuthConfig> = MergeOptions<typeof authDefaults.session, NonNullable<Config['session']>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"better-auth.d.ts","names":[],"sources":["../src/infrastructure/better-auth/options.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"better-auth.d.ts","names":[],"sources":["../src/infrastructure/better-auth/options.ts"],"mappings":";;;;;;qBAuCa;;;;;;IAQE;IAAY;;;;;;;;;;;;;;;;;MASV;;;;;YAKL;WACD,YAAY,kBAAkB;WAC9B,WAAW;WACX,YAAY,YAAY,kBAAkB;WAC1C,UAAU,KAAK;WACf,0BAA0B,YAAY,kBAAkB;WACxD,UAAU,YAAY,kBAAkB;WACxC,SAAS,YAAY,kBAAkB;WACvC,sBAAsB,YAAY,kBAAkB;;;;;;YAOnD,aAAa,KAAK;WACnB,YAAY,KACnB,YAAY;;WAKC;WACA,kBAAkB;WAClB,cAAc;aACZ,SAAS,KAAK;;;WAIhB;WACA;WACA,cAAc;;KAI1B;EACH,WAAW,kBAAkB;EAC7B,UAAU,kBAAkB;EAC5B,WAAW,kBAAkB;EAC7B,SAAS,kBAAkB;EAC3B,iBAAiB,kBAAkB;EACnC,SAAS,kBAAkB;EAC3B,QAAQ,kBAAkB;EAC1B,qBAAqB,kBAAkB;;KAGpC,gBAAgB,gBAAgB,qBAAqB;WAC/C,uBAAuB,iBAAiB;IAE/C,kBAAkB,gBAAgB,aAClC;KAEC,eAAe,gBAAgB,mBAAmB,kBAAkB,eACvE,gBAAgB,OAAO,+BAEnB,gBAAgB,OAAO,KAAK,YAAY,kBAAkB,UACvD,gCAAgC,gBAAgB,WAAW,YAAY,QACxE,iCACG,YAAY;KAGlB,gBAAgB,gBAAgB,yBAChC,eAAe,wBACf,eAAe,gCACf,eAAe,wBACf,eAAe,0BACf,eAAe,yBACf,eAAe,0BACf,eAAe,oCACf,eAAe;KAGf,kBAAkB,eAAe,cAAc,gBAAgB;KAE/D,aAAa,UAAU,aAAa,KAAK,gBAAgB,aAAa;KAEtE,eAAe,eAAe,cAAc,oBACxC,aAAa,SACpB,YAAY;KAGT,kBAAkB,eAAe,YAAY,gBAAgB,sBAAsB,KACtF;EAGA,UAAU;EACV,YAAY,oBAAoB,aAAa,YAAY,YAAY;EACrE,WAAW,YAAY;EACvB,SAAS,KAAK,eAAe;IAC3B,aAAa,oBACJ,aAAa,QAAQ,aAC5B,YAAY,YAAY;;EAG5B,UAAU,oBAAoB,aAAa,UAAU,YAAY;IAC/D,UAAU,oBACD,aAAa,SAAS,UAC7B,YAAY,YAAY;IAE1B,WAAW,oBACF,aAAa,SAAS,WAC7B,YAAY,YAAY;;EAG5B,aAAa,kBAAkB,YAAY;;;YAIjC,kCACJ,eAAe,kBACf,gBAAgB,yBAEtB,UAAU,mBACV,QAAQ,QACR,oBAAoB,YACjB,kBAAkB,QAAQ;;;;;qBA0ClB,yBAAyB"}
|
package/dist/better-auth.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { a as createLogger } from "./core-
|
|
1
|
+
import { a as createLogger } from "./core-DC_4pOrI.js";
|
|
2
2
|
import { v7 } from "uuid";
|
|
3
3
|
import { isAPIError } from "better-auth/api";
|
|
4
|
-
import { anonymous, bearer, captcha, deviceAuthorization, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
4
|
+
import { anonymous, bearer, captcha, deviceAuthorization, emailOTP, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
5
5
|
|
|
6
6
|
//#region src/infrastructure/better-auth/options.ts
|
|
7
7
|
const logger = createLogger("auth");
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_CAPTCHA_ENDPOINTS = [
|
|
9
9
|
"/sign-up/email",
|
|
10
10
|
"/sign-in/email",
|
|
11
|
-
"/request-password-reset"
|
|
12
|
-
|
|
11
|
+
"/request-password-reset"
|
|
12
|
+
];
|
|
13
|
+
const MAGIC_LINK_CAPTCHA_ENDPOINT = "/sign-in/magic-link";
|
|
14
|
+
const EMAIL_OTP_CAPTCHA_ENDPOINTS = [
|
|
15
|
+
"/email-otp/send-verification-otp",
|
|
16
|
+
"/email-otp/request-password-reset",
|
|
17
|
+
"/forget-password/email-otp"
|
|
13
18
|
];
|
|
14
19
|
/**
|
|
15
20
|
* Shared policy from Porte and Typist. The factory merges application overrides into these defaults.
|
|
@@ -47,7 +52,12 @@ function buildPlugins(options) {
|
|
|
47
52
|
const plugins = [];
|
|
48
53
|
if (options.captcha !== void 0) {
|
|
49
54
|
const captchaOptions = { ...options.captcha };
|
|
50
|
-
if (
|
|
55
|
+
if (captchaOptions.endpoints === void 0) {
|
|
56
|
+
const endpoints = [...DEFAULT_CAPTCHA_ENDPOINTS];
|
|
57
|
+
if (options.magicLink !== void 0) endpoints.push(MAGIC_LINK_CAPTCHA_ENDPOINT);
|
|
58
|
+
if (options.emailOTP !== void 0) endpoints.push(...EMAIL_OTP_CAPTCHA_ENDPOINTS);
|
|
59
|
+
if (endpoints.length > DEFAULT_CAPTCHA_ENDPOINTS.length) captchaOptions.endpoints = endpoints;
|
|
60
|
+
}
|
|
51
61
|
plugins.push(captcha({
|
|
52
62
|
...captchaOptions,
|
|
53
63
|
provider: "cloudflare-turnstile"
|
|
@@ -59,6 +69,12 @@ function buildPlugins(options) {
|
|
|
59
69
|
expiresIn: 300,
|
|
60
70
|
...options.magicLink
|
|
61
71
|
}));
|
|
72
|
+
if (options.emailOTP !== void 0) plugins.push(emailOTP({
|
|
73
|
+
expiresIn: 300,
|
|
74
|
+
storeOTP: "encrypted",
|
|
75
|
+
resendStrategy: "reuse",
|
|
76
|
+
...options.emailOTP
|
|
77
|
+
}));
|
|
62
78
|
if (options.anonymous !== void 0) plugins.push(anonymous(options.anonymous));
|
|
63
79
|
if (options.deviceAuthorization !== void 0) plugins.push(deviceAuthorization(options.deviceAuthorization));
|
|
64
80
|
if (options.bearer !== void 0) plugins.push(bearer(options.bearer));
|
package/dist/better-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"better-auth.js","names":["uuidv7"],"sources":["../src/infrastructure/better-auth/options.ts"],"sourcesContent":["import type {\n BetterAuthOptions,\n BetterAuthPlugin,\n BetterAuthRateLimitStorage,\n DBAdapterInstance,\n} from 'better-auth'\nimport { isAPIError } from 'better-auth/api'\nimport {\n anonymous,\n bearer,\n captcha,\n deviceAuthorization,\n lastLoginMethod,\n magicLink,\n openAPI,\n type CloudflareTurnstileOptions,\n type LastLoginMethodOptions,\n} from 'better-auth/plugins'\nimport { v7 as uuidv7 } from 'uuid'\n\nimport { createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n\n// Better Auth does not export its default captcha paths; keep them when adding magic-link requests.\nconst MAGIC_LINK_CAPTCHA_ENDPOINTS = [\n '/sign-up/email',\n '/sign-in/email',\n '/request-password-reset',\n '/sign-in/magic-link',\n]\n\n/**\n * Shared policy from Porte and Typist. The factory merges application overrides into these defaults.\n */\nexport const authDefaults = {\n onAPIError: {\n throw: false,\n onError(error) {\n if (isAPIError(error) && error.statusCode < 500) return\n logger.error('auth_operation_failed', { error })\n },\n },\n rateLimit: { window: 60, max: 100 },\n session: {\n expiresIn: 60 * 60 * 24 * 7,\n updateAge: 60 * 60 * 24,\n freshAge: 60 * 60 * 24,\n cookieCache: { enabled: true, maxAge: 300 },\n },\n advanced: {\n database: { generateId: () => uuidv7(), joins: true },\n ipAddress: { ipAddressHeaders: ['cf-connecting-ip', 'x-forwarded-for'] },\n },\n} satisfies BetterAuthOptions\n\n/** Configure built-in plugins with native options. Runtime auth requires captcha; lastLoginMethod is enabled unless explicitly disabled. */\nexport type AuthPluginOptions = {\n readonly magicLink?: Parameters<typeof magicLink>[0]\n readonly anonymous?: NonNullable<Parameters<typeof anonymous>[0]>\n readonly captcha?: Omit<CloudflareTurnstileOptions, 'provider'>\n readonly lastLoginMethod?: false | NonNullable<Parameters<typeof lastLoginMethod>[0]>\n readonly openAPI?: NonNullable<Parameters<typeof openAPI>[0]>\n readonly bearer?: NonNullable<Parameters<typeof bearer>[0]>\n readonly deviceAuthorization?: NonNullable<Parameters<typeof deviceAuthorization>[0]>\n}\n\n/**\n * Runtime auth requires rate limiting and Turnstile credentials. Schema generation omits both.\n * Both modes select plugins through `integrations`, so schema generation must repeat the runtime selection.\n */\nexport type AuthConfig = Omit<BetterAuthOptions, 'database' | 'plugins' | 'rateLimit'> & {\n readonly rateLimit?: Omit<\n NonNullable<BetterAuthOptions['rateLimit']>,\n 'enabled' | 'storage' | 'customStorage'\n >\n} & (\n | {\n readonly mode?: 'runtime'\n readonly rateLimitStorage: BetterAuthRateLimitStorage\n readonly integrations: AuthPluginOptions & {\n readonly captcha: Omit<CloudflareTurnstileOptions, 'provider'>\n }\n }\n | {\n readonly mode: 'schema'\n readonly rateLimitStorage?: never\n readonly integrations: AuthPluginOptions\n }\n )\n\ntype PluginTypes = {\n magicLink: ReturnType<typeof magicLink>\n anonymous: ReturnType<typeof anonymous>\n captcha: ReturnType<typeof captcha>\n lastLoginMethod: ReturnType<typeof lastLoginMethod<{}>>\n openAPI: ReturnType<typeof openAPI>\n bearer: ReturnType<typeof bearer>\n deviceAuthorization: ReturnType<typeof deviceAuthorization>\n}\n\ntype LastLoginPlugin<Options extends AuthPluginOptions> = Options extends {\n readonly lastLoginMethod: infer Settings extends LastLoginMethodOptions\n}\n ? ReturnType<typeof lastLoginMethod<Settings>>\n : PluginTypes['lastLoginMethod']\n\ntype SelectedPlugin<Options extends AuthPluginOptions, Key extends keyof PluginTypes> =\n Options extends Record<Key, false | undefined>\n ? []\n : Options extends Record<Key, NonNullable<AuthPluginOptions[Key]>>\n ? [Key extends 'lastLoginMethod' ? LastLoginPlugin<Options> : PluginTypes[Key]]\n : Key extends 'lastLoginMethod'\n ? [PluginTypes[Key]]\n : []\n\ntype SelectedPlugins<Options extends AuthPluginOptions> = [\n ...SelectedPlugin<Options, 'captcha'>,\n ...SelectedPlugin<Options, 'lastLoginMethod'>,\n ...SelectedPlugin<Options, 'openAPI'>,\n ...SelectedPlugin<Options, 'magicLink'>,\n ...SelectedPlugin<Options, 'anonymous'>,\n ...SelectedPlugin<Options, 'deviceAuthorization'>,\n ...SelectedPlugin<Options, 'bearer'>,\n]\n\ntype ConfiguredPlugins<Config extends AuthConfig> = SelectedPlugins<Config['integrations']>\n\ntype MergeOptions<Defaults, Overrides> = Omit<Defaults, keyof Overrides> & Overrides\n\ntype SessionOptions<Config extends AuthConfig> = MergeOptions<\n typeof authDefaults.session,\n NonNullable<Config['session']>\n>\n\ntype ConfiguredOptions<Config extends AuthConfig, Plugins extends BetterAuthPlugin[]> = Omit<\n Config,\n 'mode' | 'integrations' | 'rateLimitStorage' | 'rateLimit' | 'session' | 'advanced' | 'onAPIError'\n> & {\n database: DBAdapterInstance\n onAPIError: MergeOptions<typeof authDefaults.onAPIError, NonNullable<Config['onAPIError']>>\n rateLimit: NonNullable<BetterAuthOptions['rateLimit']>\n session: Omit<SessionOptions<Config>, 'cookieCache'> & {\n cookieCache: MergeOptions<\n typeof authDefaults.session.cookieCache,\n NonNullable<NonNullable<Config['session']>['cookieCache']>\n >\n }\n advanced: MergeOptions<typeof authDefaults.advanced, NonNullable<Config['advanced']>> & {\n database: MergeOptions<\n typeof authDefaults.advanced.database,\n NonNullable<NonNullable<Config['advanced']>['database']>\n >\n ipAddress: MergeOptions<\n typeof authDefaults.advanced.ipAddress,\n NonNullable<NonNullable<Config['advanced']>['ipAddress']>\n >\n }\n plugins: [...ConfiguredPlugins<Config>, ...Plugins]\n}\n\n/** A required database adapter and application config produce options with inferred plugin and session types. */\nexport type BetterAuthOptionsFactory = <\n const Config extends AuthConfig,\n const Plugins extends BetterAuthPlugin[] = [],\n>(\n database: DBAdapterInstance,\n config: Config,\n additionalPlugins?: Plugins,\n) => ConfiguredOptions<Config, Plugins>\n\nfunction buildPlugins<const Options extends AuthPluginOptions>(\n options: Options,\n): SelectedPlugins<Options> {\n const plugins: BetterAuthPlugin[] = []\n if (options.captcha !== undefined) {\n const captchaOptions = { ...options.captcha }\n if (options.magicLink !== undefined && captchaOptions.endpoints === undefined)\n captchaOptions.endpoints = [...MAGIC_LINK_CAPTCHA_ENDPOINTS]\n plugins.push(captcha({ ...captchaOptions, provider: 'cloudflare-turnstile' }))\n }\n if (options.lastLoginMethod !== false) plugins.push(lastLoginMethod(options.lastLoginMethod))\n if (options.openAPI !== undefined) plugins.push(openAPI(options.openAPI))\n if (options.magicLink !== undefined)\n plugins.push(magicLink({ expiresIn: 300, ...options.magicLink }))\n if (options.anonymous !== undefined) plugins.push(anonymous(options.anonymous))\n if (options.deviceAuthorization !== undefined)\n plugins.push(deviceAuthorization(options.deviceAuthorization))\n if (options.bearer !== undefined) plugins.push(bearer(options.bearer))\n // SAFETY: Each branch adds exactly the plugin selected by the corresponding configuration key.\n return plugins as SelectedPlugins<Options>\n}\n\n/**\n * Build native Better Auth options with shared defaults and configured plugins; both arguments are required.\n * Pass Stripe and other plugins in the third argument. Put tanstackStartCookies() last.\n */\nexport const createBetterAuthOptions: BetterAuthOptionsFactory = (\n database,\n config,\n additionalPlugins,\n) => {\n const { mode, integrations, rateLimitStorage, ...options } = config\n const result: BetterAuthOptions = {\n ...options,\n database,\n onAPIError: { ...authDefaults.onAPIError, ...options.onAPIError },\n rateLimit:\n mode === 'schema'\n ? { enabled: false }\n : {\n ...authDefaults.rateLimit,\n ...options.rateLimit,\n enabled: true,\n customStorage: rateLimitStorage,\n },\n session: {\n ...authDefaults.session,\n ...options.session,\n cookieCache: { ...authDefaults.session.cookieCache, ...options.session?.cookieCache },\n },\n advanced: {\n ...authDefaults.advanced,\n ...options.advanced,\n database: { ...authDefaults.advanced.database, ...options.advanced?.database },\n ipAddress: { ...authDefaults.advanced.ipAddress, ...options.advanced?.ipAddress },\n },\n plugins: [...buildPlugins(integrations), ...(additionalPlugins ?? [])],\n }\n // SAFETY: The factory preserves configured field schemas and plugins while merging only the documented default fields.\n return result as ConfiguredOptions<typeof config, Exclude<typeof additionalPlugins, undefined>>\n}\n"],"mappings":";;;;;;AAsBA,MAAM,SAAS,aAAa,MAAM;AAGlC,MAAM,+BAA+B;CACnC;CACA;CACA;CACA;AACF;;;;AAKA,MAAa,eAAe;CAC1B,YAAY;EACV,OAAO;EACP,QAAQ,OAAO;GACb,IAAI,WAAW,KAAK,KAAK,MAAM,aAAa,KAAK;GACjD,OAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC;EACjD;CACF;CACA,WAAW;EAAE,QAAQ;EAAI,KAAK;CAAI;CAClC,SAAS;EACP,WAAW;EACX,WAAW;EACX,UAAU;EACV,aAAa;GAAE,SAAS;GAAM,QAAQ;EAAI;CAC5C;CACA,UAAU;EACR,UAAU;GAAE,kBAAkBA,GAAO;GAAG,OAAO;EAAK;EACpD,WAAW,EAAE,kBAAkB,CAAC,oBAAoB,iBAAiB,EAAE;CACzE;AACF;AAqHA,SAAS,aACP,SAC0B;CAC1B,MAAM,UAA8B,CAAC;CACrC,IAAI,QAAQ,YAAY,QAAW;EACjC,MAAM,iBAAiB,EAAE,GAAG,QAAQ,QAAQ;EAC5C,IAAI,QAAQ,cAAc,UAAa,eAAe,cAAc,QAClE,eAAe,YAAY,CAAC,GAAG,4BAA4B;EAC7D,QAAQ,KAAK,QAAQ;GAAE,GAAG;GAAgB,UAAU;EAAuB,CAAC,CAAC;CAC/E;CACA,IAAI,QAAQ,oBAAoB,OAAO,QAAQ,KAAK,gBAAgB,QAAQ,eAAe,CAAC;CAC5F,IAAI,QAAQ,YAAY,QAAW,QAAQ,KAAK,QAAQ,QAAQ,OAAO,CAAC;CACxE,IAAI,QAAQ,cAAc,QACxB,QAAQ,KAAK,UAAU;EAAE,WAAW;EAAK,GAAG,QAAQ;CAAU,CAAC,CAAC;CAClE,IAAI,QAAQ,cAAc,QAAW,QAAQ,KAAK,UAAU,QAAQ,SAAS,CAAC;CAC9E,IAAI,QAAQ,wBAAwB,QAClC,QAAQ,KAAK,oBAAoB,QAAQ,mBAAmB,CAAC;CAC/D,IAAI,QAAQ,WAAW,QAAW,QAAQ,KAAK,OAAO,QAAQ,MAAM,CAAC;CAErE,OAAO;AACT;;;;;AAMA,MAAa,2BACX,UACA,QACA,sBACG;CACH,MAAM,EAAE,MAAM,cAAc,kBAAkB,GAAG,YAAY;CA4B7D,OAAO;EA1BL,GAAG;EACH;EACA,YAAY;GAAE,GAAG,aAAa;GAAY,GAAG,QAAQ;EAAW;EAChE,WACE,SAAS,WACL,EAAE,SAAS,MAAM,IACjB;GACE,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,SAAS;GACT,eAAe;EACjB;EACN,SAAS;GACP,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,aAAa;IAAE,GAAG,aAAa,QAAQ;IAAa,GAAG,QAAQ,SAAS;GAAY;EACtF;EACA,UAAU;GACR,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,UAAU;IAAE,GAAG,aAAa,SAAS;IAAU,GAAG,QAAQ,UAAU;GAAS;GAC7E,WAAW;IAAE,GAAG,aAAa,SAAS;IAAW,GAAG,QAAQ,UAAU;GAAU;EAClF;EACA,SAAS,CAAC,GAAG,aAAa,YAAY,GAAG,GAAI,qBAAqB,CAAC,CAAE;CAG3D;AACd"}
|
|
1
|
+
{"version":3,"file":"better-auth.js","names":["uuidv7"],"sources":["../src/infrastructure/better-auth/options.ts"],"sourcesContent":["import type {\n BetterAuthOptions,\n BetterAuthPlugin,\n BetterAuthRateLimitStorage,\n DBAdapterInstance,\n} from 'better-auth'\nimport { isAPIError } from 'better-auth/api'\nimport {\n anonymous,\n bearer,\n captcha,\n deviceAuthorization,\n emailOTP,\n lastLoginMethod,\n magicLink,\n openAPI,\n type CloudflareTurnstileOptions,\n type EmailOTPOptions,\n type LastLoginMethodOptions,\n} from 'better-auth/plugins'\nimport { v7 as uuidv7 } from 'uuid'\n\nimport { createLogger } from '@/core'\n\nconst logger = createLogger('auth')\n\n// Better Auth does not export its default captcha paths; keep them when adding email delivery endpoints.\nconst DEFAULT_CAPTCHA_ENDPOINTS = ['/sign-up/email', '/sign-in/email', '/request-password-reset']\nconst MAGIC_LINK_CAPTCHA_ENDPOINT = '/sign-in/magic-link'\n// Every OTP endpoint that sends an email, including the deprecated password-reset alias.\nconst EMAIL_OTP_CAPTCHA_ENDPOINTS = [\n '/email-otp/send-verification-otp',\n '/email-otp/request-password-reset',\n '/forget-password/email-otp',\n]\n\n/**\n * Shared policy from Porte and Typist. The factory merges application overrides into these defaults.\n */\nexport const authDefaults = {\n onAPIError: {\n throw: false,\n onError(error) {\n if (isAPIError(error) && error.statusCode < 500) return\n logger.error('auth_operation_failed', { error })\n },\n },\n rateLimit: { window: 60, max: 100 },\n session: {\n expiresIn: 60 * 60 * 24 * 7,\n updateAge: 60 * 60 * 24,\n freshAge: 60 * 60 * 24,\n cookieCache: { enabled: true, maxAge: 300 },\n },\n advanced: {\n database: { generateId: () => uuidv7(), joins: true },\n ipAddress: { ipAddressHeaders: ['cf-connecting-ip', 'x-forwarded-for'] },\n },\n} satisfies BetterAuthOptions\n\n/** Configure built-in plugins with native options. Runtime auth requires captcha; lastLoginMethod is enabled unless explicitly disabled. */\nexport type AuthPluginOptions = {\n readonly magicLink?: Parameters<typeof magicLink>[0]\n readonly emailOTP?: EmailOTPOptions\n readonly anonymous?: NonNullable<Parameters<typeof anonymous>[0]>\n readonly captcha?: Omit<CloudflareTurnstileOptions, 'provider'>\n readonly lastLoginMethod?: false | NonNullable<Parameters<typeof lastLoginMethod>[0]>\n readonly openAPI?: NonNullable<Parameters<typeof openAPI>[0]>\n readonly bearer?: NonNullable<Parameters<typeof bearer>[0]>\n readonly deviceAuthorization?: NonNullable<Parameters<typeof deviceAuthorization>[0]>\n}\n\n/**\n * Runtime auth requires rate limiting and Turnstile credentials. Schema generation omits both.\n * Both modes select plugins through `integrations`, so schema generation must repeat the runtime selection.\n */\nexport type AuthConfig = Omit<BetterAuthOptions, 'database' | 'plugins' | 'rateLimit'> & {\n readonly rateLimit?: Omit<\n NonNullable<BetterAuthOptions['rateLimit']>,\n 'enabled' | 'storage' | 'customStorage'\n >\n} & (\n | {\n readonly mode?: 'runtime'\n readonly rateLimitStorage: BetterAuthRateLimitStorage\n readonly integrations: AuthPluginOptions & {\n readonly captcha: Omit<CloudflareTurnstileOptions, 'provider'>\n }\n }\n | {\n readonly mode: 'schema'\n readonly rateLimitStorage?: never\n readonly integrations: AuthPluginOptions\n }\n )\n\ntype PluginTypes = {\n magicLink: ReturnType<typeof magicLink>\n emailOTP: ReturnType<typeof emailOTP>\n anonymous: ReturnType<typeof anonymous>\n captcha: ReturnType<typeof captcha>\n lastLoginMethod: ReturnType<typeof lastLoginMethod<{}>>\n openAPI: ReturnType<typeof openAPI>\n bearer: ReturnType<typeof bearer>\n deviceAuthorization: ReturnType<typeof deviceAuthorization>\n}\n\ntype LastLoginPlugin<Options extends AuthPluginOptions> = Options extends {\n readonly lastLoginMethod: infer Settings extends LastLoginMethodOptions\n}\n ? ReturnType<typeof lastLoginMethod<Settings>>\n : PluginTypes['lastLoginMethod']\n\ntype SelectedPlugin<Options extends AuthPluginOptions, Key extends keyof PluginTypes> =\n Options extends Record<Key, false | undefined>\n ? []\n : Options extends Record<Key, NonNullable<AuthPluginOptions[Key]>>\n ? [Key extends 'lastLoginMethod' ? LastLoginPlugin<Options> : PluginTypes[Key]]\n : Key extends 'lastLoginMethod'\n ? [PluginTypes[Key]]\n : []\n\ntype SelectedPlugins<Options extends AuthPluginOptions> = [\n ...SelectedPlugin<Options, 'captcha'>,\n ...SelectedPlugin<Options, 'lastLoginMethod'>,\n ...SelectedPlugin<Options, 'openAPI'>,\n ...SelectedPlugin<Options, 'magicLink'>,\n ...SelectedPlugin<Options, 'emailOTP'>,\n ...SelectedPlugin<Options, 'anonymous'>,\n ...SelectedPlugin<Options, 'deviceAuthorization'>,\n ...SelectedPlugin<Options, 'bearer'>,\n]\n\ntype ConfiguredPlugins<Config extends AuthConfig> = SelectedPlugins<Config['integrations']>\n\ntype MergeOptions<Defaults, Overrides> = Omit<Defaults, keyof Overrides> & Overrides\n\ntype SessionOptions<Config extends AuthConfig> = MergeOptions<\n typeof authDefaults.session,\n NonNullable<Config['session']>\n>\n\ntype ConfiguredOptions<Config extends AuthConfig, Plugins extends BetterAuthPlugin[]> = Omit<\n Config,\n 'mode' | 'integrations' | 'rateLimitStorage' | 'rateLimit' | 'session' | 'advanced' | 'onAPIError'\n> & {\n database: DBAdapterInstance\n onAPIError: MergeOptions<typeof authDefaults.onAPIError, NonNullable<Config['onAPIError']>>\n rateLimit: NonNullable<BetterAuthOptions['rateLimit']>\n session: Omit<SessionOptions<Config>, 'cookieCache'> & {\n cookieCache: MergeOptions<\n typeof authDefaults.session.cookieCache,\n NonNullable<NonNullable<Config['session']>['cookieCache']>\n >\n }\n advanced: MergeOptions<typeof authDefaults.advanced, NonNullable<Config['advanced']>> & {\n database: MergeOptions<\n typeof authDefaults.advanced.database,\n NonNullable<NonNullable<Config['advanced']>['database']>\n >\n ipAddress: MergeOptions<\n typeof authDefaults.advanced.ipAddress,\n NonNullable<NonNullable<Config['advanced']>['ipAddress']>\n >\n }\n plugins: [...ConfiguredPlugins<Config>, ...Plugins]\n}\n\n/** A required database adapter and application config produce options with inferred plugin and session types. */\nexport type BetterAuthOptionsFactory = <\n const Config extends AuthConfig,\n const Plugins extends BetterAuthPlugin[] = [],\n>(\n database: DBAdapterInstance,\n config: Config,\n additionalPlugins?: Plugins,\n) => ConfiguredOptions<Config, Plugins>\n\nfunction buildPlugins<const Options extends AuthPluginOptions>(\n options: Options,\n): SelectedPlugins<Options> {\n const plugins: BetterAuthPlugin[] = []\n if (options.captcha !== undefined) {\n const captchaOptions = { ...options.captcha }\n if (captchaOptions.endpoints === undefined) {\n const endpoints = [...DEFAULT_CAPTCHA_ENDPOINTS]\n if (options.magicLink !== undefined) endpoints.push(MAGIC_LINK_CAPTCHA_ENDPOINT)\n if (options.emailOTP !== undefined) endpoints.push(...EMAIL_OTP_CAPTCHA_ENDPOINTS)\n if (endpoints.length > DEFAULT_CAPTCHA_ENDPOINTS.length) captchaOptions.endpoints = endpoints\n }\n plugins.push(captcha({ ...captchaOptions, provider: 'cloudflare-turnstile' }))\n }\n if (options.lastLoginMethod !== false) plugins.push(lastLoginMethod(options.lastLoginMethod))\n if (options.openAPI !== undefined) plugins.push(openAPI(options.openAPI))\n if (options.magicLink !== undefined)\n plugins.push(magicLink({ expiresIn: 300, ...options.magicLink }))\n // Encrypted storage keeps resend reuse possible; an unsalted hash of a six-digit code protects nothing.\n if (options.emailOTP !== undefined)\n plugins.push(\n emailOTP({\n expiresIn: 300,\n storeOTP: 'encrypted',\n resendStrategy: 'reuse',\n ...options.emailOTP,\n }),\n )\n if (options.anonymous !== undefined) plugins.push(anonymous(options.anonymous))\n if (options.deviceAuthorization !== undefined)\n plugins.push(deviceAuthorization(options.deviceAuthorization))\n if (options.bearer !== undefined) plugins.push(bearer(options.bearer))\n // SAFETY: Each branch adds exactly the plugin selected by the corresponding configuration key.\n return plugins as SelectedPlugins<Options>\n}\n\n/**\n * Build native Better Auth options with shared defaults and configured plugins; both arguments are required.\n * Pass Stripe and other plugins in the third argument. Put tanstackStartCookies() last.\n */\nexport const createBetterAuthOptions: BetterAuthOptionsFactory = (\n database,\n config,\n additionalPlugins,\n) => {\n const { mode, integrations, rateLimitStorage, ...options } = config\n const result: BetterAuthOptions = {\n ...options,\n database,\n onAPIError: { ...authDefaults.onAPIError, ...options.onAPIError },\n rateLimit:\n mode === 'schema'\n ? { enabled: false }\n : {\n ...authDefaults.rateLimit,\n ...options.rateLimit,\n enabled: true,\n customStorage: rateLimitStorage,\n },\n session: {\n ...authDefaults.session,\n ...options.session,\n cookieCache: { ...authDefaults.session.cookieCache, ...options.session?.cookieCache },\n },\n advanced: {\n ...authDefaults.advanced,\n ...options.advanced,\n database: { ...authDefaults.advanced.database, ...options.advanced?.database },\n ipAddress: { ...authDefaults.advanced.ipAddress, ...options.advanced?.ipAddress },\n },\n plugins: [...buildPlugins(integrations), ...(additionalPlugins ?? [])],\n }\n // SAFETY: The factory preserves configured field schemas and plugins while merging only the documented default fields.\n return result as ConfiguredOptions<typeof config, Exclude<typeof additionalPlugins, undefined>>\n}\n"],"mappings":";;;;;;AAwBA,MAAM,SAAS,aAAa,MAAM;AAGlC,MAAM,4BAA4B;CAAC;CAAkB;CAAkB;AAAyB;AAChG,MAAM,8BAA8B;AAEpC,MAAM,8BAA8B;CAClC;CACA;CACA;AACF;;;;AAKA,MAAa,eAAe;CAC1B,YAAY;EACV,OAAO;EACP,QAAQ,OAAO;GACb,IAAI,WAAW,KAAK,KAAK,MAAM,aAAa,KAAK;GACjD,OAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC;EACjD;CACF;CACA,WAAW;EAAE,QAAQ;EAAI,KAAK;CAAI;CAClC,SAAS;EACP,WAAW;EACX,WAAW;EACX,UAAU;EACV,aAAa;GAAE,SAAS;GAAM,QAAQ;EAAI;CAC5C;CACA,UAAU;EACR,UAAU;GAAE,kBAAkBA,GAAO;GAAG,OAAO;EAAK;EACpD,WAAW,EAAE,kBAAkB,CAAC,oBAAoB,iBAAiB,EAAE;CACzE;AACF;AAwHA,SAAS,aACP,SAC0B;CAC1B,MAAM,UAA8B,CAAC;CACrC,IAAI,QAAQ,YAAY,QAAW;EACjC,MAAM,iBAAiB,EAAE,GAAG,QAAQ,QAAQ;EAC5C,IAAI,eAAe,cAAc,QAAW;GAC1C,MAAM,YAAY,CAAC,GAAG,yBAAyB;GAC/C,IAAI,QAAQ,cAAc,QAAW,UAAU,KAAK,2BAA2B;GAC/E,IAAI,QAAQ,aAAa,QAAW,UAAU,KAAK,GAAG,2BAA2B;GACjF,IAAI,UAAU,SAAS,0BAA0B,QAAQ,eAAe,YAAY;EACtF;EACA,QAAQ,KAAK,QAAQ;GAAE,GAAG;GAAgB,UAAU;EAAuB,CAAC,CAAC;CAC/E;CACA,IAAI,QAAQ,oBAAoB,OAAO,QAAQ,KAAK,gBAAgB,QAAQ,eAAe,CAAC;CAC5F,IAAI,QAAQ,YAAY,QAAW,QAAQ,KAAK,QAAQ,QAAQ,OAAO,CAAC;CACxE,IAAI,QAAQ,cAAc,QACxB,QAAQ,KAAK,UAAU;EAAE,WAAW;EAAK,GAAG,QAAQ;CAAU,CAAC,CAAC;CAElE,IAAI,QAAQ,aAAa,QACvB,QAAQ,KACN,SAAS;EACP,WAAW;EACX,UAAU;EACV,gBAAgB;EAChB,GAAG,QAAQ;CACb,CAAC,CACH;CACF,IAAI,QAAQ,cAAc,QAAW,QAAQ,KAAK,UAAU,QAAQ,SAAS,CAAC;CAC9E,IAAI,QAAQ,wBAAwB,QAClC,QAAQ,KAAK,oBAAoB,QAAQ,mBAAmB,CAAC;CAC/D,IAAI,QAAQ,WAAW,QAAW,QAAQ,KAAK,OAAO,QAAQ,MAAM,CAAC;CAErE,OAAO;AACT;;;;;AAMA,MAAa,2BACX,UACA,QACA,sBACG;CACH,MAAM,EAAE,MAAM,cAAc,kBAAkB,GAAG,YAAY;CA4B7D,OAAO;EA1BL,GAAG;EACH;EACA,YAAY;GAAE,GAAG,aAAa;GAAY,GAAG,QAAQ;EAAW;EAChE,WACE,SAAS,WACL,EAAE,SAAS,MAAM,IACjB;GACE,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,SAAS;GACT,eAAe;EACjB;EACN,SAAS;GACP,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,aAAa;IAAE,GAAG,aAAa,QAAQ;IAAa,GAAG,QAAQ,SAAS;GAAY;EACtF;EACA,UAAU;GACR,GAAG,aAAa;GAChB,GAAG,QAAQ;GACX,UAAU;IAAE,GAAG,aAAa,SAAS;IAAU,GAAG,QAAQ,UAAU;GAAS;GAC7E,WAAW;IAAE,GAAG,aAAa,SAAS;IAAW,GAAG,QAAQ,UAAU;GAAU;EAClF;EACA,SAAS,CAAC,GAAG,aAAa,YAAY,GAAG,GAAI,qBAAqB,CAAC,CAAE;CAG3D;AACd"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kt as AppError } from "../index-6XMPJu7u.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. */
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { At as FailureClassification, U as DomainCommand, W as DomainEvent, kt as AppError } from "../index-6XMPJu7u.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/infrastructure/cloudflare/queue/queue-message.d.ts
|
|
4
|
+
/** A message a queue carries: a command from outside the request, or an event from the outbox. */
|
|
5
|
+
type QueueMessage = DomainCommand | DomainEvent;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/infrastructure/cloudflare/queue/queue-client.d.ts
|
|
8
|
+
/** Cloudflare's delivery delay, in seconds. */
|
|
9
|
+
type QueueSendOptions = {
|
|
10
|
+
readonly delaySeconds: number;
|
|
11
|
+
};
|
|
12
|
+
/** The two calls the client makes on a queue binding. */
|
|
13
|
+
type QueueBinding<Body> = Pick<Queue<Body>, 'send' | 'sendBatch'>;
|
|
14
|
+
/**
|
|
15
|
+
* The lanes and the dead letter of one application. A lane is a queue with its own consumer
|
|
16
|
+
* settings; `laneOf` is the product's routing rule, written once at the composition root.
|
|
17
|
+
*/
|
|
18
|
+
type QueueClientOptions<TMessage extends QueueMessage, Lane extends string> = {
|
|
19
|
+
readonly queues: Readonly<Record<Lane, QueueBinding<TMessage>>>;
|
|
20
|
+
/** Where the consumer parks poison bodies. Cloudflare also routes exhausted retries here. */
|
|
21
|
+
readonly deadLetter: Pick<Queue, 'send'>;
|
|
22
|
+
readonly laneOf: (message: TMessage) => Lane;
|
|
23
|
+
};
|
|
24
|
+
/** The port an entrypoint, a relay, or a consumer sends through. */
|
|
25
|
+
interface IQueueClient<TMessage extends QueueMessage> {
|
|
26
|
+
send(message: TMessage, options?: QueueSendOptions): Promise<void>;
|
|
27
|
+
sendBatch(messages: ReadonlyArray<TMessage>): Promise<void>;
|
|
28
|
+
/** Sends every message within the limits and returns the oversized ones. */
|
|
29
|
+
sendBatchPartial(messages: ReadonlyArray<TMessage>): Promise<ReadonlyArray<TMessage>>;
|
|
30
|
+
/** Parks a delivered message's body as it arrived. */
|
|
31
|
+
sendToDlq(message: Message): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sends messages to the application's queues. Checks the size limits before a send, retries a
|
|
35
|
+
* transient binding failure a bounded number of times, and converts the final failure to `QueueSendError`.
|
|
36
|
+
*/
|
|
37
|
+
export declare class QueueClient<TMessage extends QueueMessage, Lane extends string> implements IQueueClient<TMessage> {
|
|
38
|
+
private readonly options;
|
|
39
|
+
constructor(options: QueueClientOptions<TMessage, Lane>);
|
|
40
|
+
/** @throws OversizedMessageError before the send. @throws QueueSendError after the retries. */
|
|
41
|
+
send(message: TMessage, options?: QueueSendOptions): Promise<void>;
|
|
42
|
+
/** Rejects the whole batch before any send when one message is oversized. */
|
|
43
|
+
sendBatch(messages: ReadonlyArray<TMessage>): Promise<void>;
|
|
44
|
+
sendBatchPartial(messages: ReadonlyArray<TMessage>): Promise<ReadonlyArray<TMessage>>;
|
|
45
|
+
sendToDlq(message: Message): Promise<void>;
|
|
46
|
+
/** Each lane's chunks retry on their own, so one failed chunk does not resend the others. */
|
|
47
|
+
private sendSizedBatch;
|
|
48
|
+
private queueFor;
|
|
49
|
+
private callQueue;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/infrastructure/cloudflare/queue/queue-consumer.d.ts
|
|
53
|
+
/** Wraps one dispatch: an error-tracking scope, an analytics context. The default runs it as is. */
|
|
54
|
+
type DispatchWrapper<TMessage extends QueueMessage> = (message: TMessage, attempt: number, run: () => Promise<void>) => Promise<void>;
|
|
55
|
+
/** The one call the consumer makes on the bus. */
|
|
56
|
+
type DispatchBus<TMessage extends QueueMessage> = {
|
|
57
|
+
handle(message: TMessage): Promise<unknown>;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Parses and dispatches each queue message, then selects acknowledgement or retry.
|
|
61
|
+
* Cloudflare owns the retry limit and sends exhausted messages to the configured dead letter.
|
|
62
|
+
* The consumer logs each failure once; handlers do not.
|
|
63
|
+
*/
|
|
64
|
+
export declare class QueueConsumer<TMessage extends QueueMessage> {
|
|
65
|
+
private readonly bus;
|
|
66
|
+
private readonly queueClient;
|
|
67
|
+
private readonly schema;
|
|
68
|
+
private readonly wrapDispatch;
|
|
69
|
+
/**
|
|
70
|
+
* @param bus - Where a parsed message goes.
|
|
71
|
+
* @param queueClient - Parks an unparseable body in the dead letter.
|
|
72
|
+
* @param schema - What a body may be: the application's `messages.queueSchema`.
|
|
73
|
+
* @param wrapDispatch - Wraps one dispatch in the application's context. Default: run it as is.
|
|
74
|
+
*/
|
|
75
|
+
constructor(bus: DispatchBus<TMessage>, queueClient: Pick<IQueueClient<TMessage>, 'sendToDlq'>, schema: z.ZodType<TMessage>, wrapDispatch?: DispatchWrapper<TMessage>);
|
|
76
|
+
/** Every message settles on its own. A settlement that throws rejects the batch after its siblings settle. */
|
|
77
|
+
processBatch(batch: MessageBatch): Promise<void>;
|
|
78
|
+
private processMessage;
|
|
79
|
+
/** Returns null on failure; the caller owns the single log point. */
|
|
80
|
+
private parseMessage;
|
|
81
|
+
/** Park an unparseable message in the dead letter, then acknowledge its source delivery. */
|
|
82
|
+
private parkPoison;
|
|
83
|
+
/**
|
|
84
|
+
* Retry declared transient and unknown failures so Cloudflare can enforce the attempt ceiling.
|
|
85
|
+
* Acknowledge only declared terminal failures.
|
|
86
|
+
*/
|
|
87
|
+
private handleFailure;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/infrastructure/cloudflare/queue/queue-limits.d.ts
|
|
91
|
+
/**
|
|
92
|
+
* The Cloudflare Queues producer limits with metadata headroom, checked before each send so a
|
|
93
|
+
* violation never reaches the binding. The dead letter has the same per-message limit.
|
|
94
|
+
* https://developers.cloudflare.com/queues/platform/limits/
|
|
95
|
+
*/
|
|
96
|
+
export declare const QUEUE_LIMITS: {
|
|
97
|
+
readonly messageBytes: 127000;
|
|
98
|
+
readonly batchBytes: 250000;
|
|
99
|
+
readonly batchCount: 100;
|
|
100
|
+
};
|
|
101
|
+
/** The size the binding measures: the JSON encoding of the body, in UTF-8 bytes. */
|
|
102
|
+
export declare function messageByteLength(message: QueueMessage): number;
|
|
103
|
+
/** A message with its measured size. */
|
|
104
|
+
type SizedMessage<TMessage> = {
|
|
105
|
+
readonly message: TMessage;
|
|
106
|
+
readonly bytes: number;
|
|
107
|
+
};
|
|
108
|
+
/** Measure every message once. */
|
|
109
|
+
export declare function sizeMessages<TMessage extends QueueMessage>(messages: ReadonlyArray<TMessage>): SizedMessage<TMessage>[];
|
|
110
|
+
/** Whether one message is over the per-message limit. */
|
|
111
|
+
export declare function isOversized(sized: SizedMessage<QueueMessage>): boolean;
|
|
112
|
+
/** Split a batch at the message-count and byte limits, keeping the order. */
|
|
113
|
+
export declare function chunkByLimits<TMessage>(sized: ReadonlyArray<SizedMessage<TMessage>>): TMessage[][];
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/infrastructure/cloudflare/queue/queue.errors.d.ts
|
|
116
|
+
/** The Queue binding calls the client makes. Each is safe to repeat. */
|
|
117
|
+
type QueueOperation = 'send' | 'send_batch' | 'send_dlq';
|
|
118
|
+
/** Classify a raw Queue binding failure: the code table first, then Cloudflare's signals, then transport. */
|
|
119
|
+
export declare function classifyQueueError(cause: unknown): FailureClassification;
|
|
120
|
+
/** A send through a Queue binding failed. The classification says whether an immediate retry may help. */
|
|
121
|
+
export declare class QueueSendError extends AppError {
|
|
122
|
+
readonly _tag = "QueueSendError";
|
|
123
|
+
readonly classification: FailureClassification;
|
|
124
|
+
readonly operation: QueueOperation;
|
|
125
|
+
constructor(args: {
|
|
126
|
+
readonly operation: QueueOperation;
|
|
127
|
+
readonly cause: unknown;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A message over the per-message cap can never be sent on any queue; the dead letter has the
|
|
132
|
+
* same cap. The client detects it before the send, so it never comes from a platform error.
|
|
133
|
+
*/
|
|
134
|
+
export declare class OversizedMessageError extends AppError {
|
|
135
|
+
readonly _tag = "OversizedMessageError";
|
|
136
|
+
readonly classification = "terminal";
|
|
137
|
+
readonly messageName: string;
|
|
138
|
+
readonly bytes: number;
|
|
139
|
+
readonly limit: number;
|
|
140
|
+
constructor(args: {
|
|
141
|
+
readonly messageName: string;
|
|
142
|
+
readonly bytes: number;
|
|
143
|
+
readonly limit: number;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The dead letter did not accept an unparseable message. Always transient, whatever the send
|
|
148
|
+
* failure was, so the source delivery retries instead of acknowledging and losing the body.
|
|
149
|
+
*/
|
|
150
|
+
export declare class DeadLetterUnavailableError extends AppError {
|
|
151
|
+
readonly _tag = "DeadLetterUnavailableError";
|
|
152
|
+
readonly classification = "transient";
|
|
153
|
+
constructor(cause: unknown);
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/infrastructure/cloudflare/queue/redelivery-policy.d.ts
|
|
157
|
+
/** How the consumer settles one failed message: acknowledge it, or have Cloudflare redeliver it after a delay. */
|
|
158
|
+
type RedeliveryDecision = {
|
|
159
|
+
readonly action: 'ack';
|
|
160
|
+
readonly classification: 'terminal';
|
|
161
|
+
} | {
|
|
162
|
+
readonly action: 'retry';
|
|
163
|
+
readonly classification: Exclude<FailureClassification, 'terminal'>;
|
|
164
|
+
readonly delaySeconds: number;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Decide whether the consumer acknowledges a failed message or asks Cloudflare to redeliver it.
|
|
168
|
+
* A terminal failure acknowledges. Transient and unknown failures redeliver: after the advertised
|
|
169
|
+
* wait plus a spread when the error carries one, else with exponential equal-jitter backoff.
|
|
170
|
+
* An `AggregateError` from event delivery redelivers when any member does, after the longest wait.
|
|
171
|
+
*/
|
|
172
|
+
export declare function decideRedelivery(cause: unknown, attempt: number): RedeliveryDecision;
|
|
173
|
+
//#endregion
|
|
174
|
+
export type { DispatchBus, DispatchWrapper, IQueueClient, QueueBinding, QueueClientOptions, QueueMessage, QueueOperation, QueueSendOptions, RedeliveryDecision, SizedMessage };
|
|
175
|
+
//# sourceMappingURL=queue.d.ts.map
|
|
@@ -0,0 +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"],"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,UAAU,cAAc,YAAY,QAAQ,cAAc;;EAE3E,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,UAAU,cAAc,YAAY,QAAQ,cAAc;EAM3E,UAAU,SAAS,UAAU;;UAQrB;UA4BN;UAIM;;;;;KChJJ,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"}
|