better-ship 0.23.2 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/application-C_xN_1pa.js.map +1 -1
  2. package/dist/application.d.ts +2 -2
  3. package/dist/cloudflare/queue.d.ts +2 -2
  4. package/dist/cloudflare/queue.d.ts.map +1 -1
  5. package/dist/cloudflare/queue.js +20 -3
  6. package/dist/cloudflare/queue.js.map +1 -1
  7. package/dist/cloudflare/tracing.d.ts +24 -0
  8. package/dist/cloudflare/tracing.d.ts.map +1 -0
  9. package/dist/cloudflare/tracing.js +21 -0
  10. package/dist/cloudflare/tracing.js.map +1 -0
  11. package/dist/cloudflare.d.ts +2 -2
  12. package/dist/{index-tNZHSCpJ.d.ts → index-lS_g1olK.d.ts} +17 -2
  13. package/dist/index-lS_g1olK.d.ts.map +1 -0
  14. package/dist/{outbox-repository-options-_B8l8l6l.d.ts → outbox-repository-options-15Sv9WnG.d.ts} +2 -2
  15. package/dist/{outbox-repository-options-_B8l8l6l.d.ts.map → outbox-repository-options-15Sv9WnG.d.ts.map} +1 -1
  16. package/dist/postgres.d.ts +2 -2
  17. package/dist/posthog/metrics.d.ts +1 -1
  18. package/dist/posthog/server.d.ts +1 -1
  19. package/dist/resend.d.ts +6 -1
  20. package/dist/resend.d.ts.map +1 -1
  21. package/dist/resend.js +35 -0
  22. package/dist/resend.js.map +1 -1
  23. package/dist/sentry/server.d.ts +1 -1
  24. package/dist/tanstack.d.ts +10 -1
  25. package/dist/tanstack.d.ts.map +1 -1
  26. package/dist/tanstack.js +11 -0
  27. package/dist/tanstack.js.map +1 -1
  28. package/dist/testing/outbox-repository.d.ts +1 -1
  29. package/package.json +2 -1
  30. package/src/application/email-service.ts +17 -0
  31. package/src/application/index.ts +2 -0
  32. package/src/infrastructure/cloudflare/queue/queue-consumer.ts +23 -3
  33. package/src/infrastructure/cloudflare/tracing.ts +36 -0
  34. package/src/infrastructure/resend/resend-email-service.ts +39 -0
  35. package/src/infrastructure/tanstack/auth.middleware.ts +20 -0
  36. package/src/infrastructure/tanstack/index.ts +1 -0
  37. package/dist/index-tNZHSCpJ.d.ts.map +0 -1
package/dist/resend.js CHANGED
@@ -48,6 +48,16 @@ function retryAfterSeconds(value) {
48
48
  const seconds = Number(value);
49
49
  return Number.isSafeInteger(seconds) ? seconds : null;
50
50
  }
51
+ function receivingError(error, headers) {
52
+ return new EmailServiceError({
53
+ reason: reasonFromResend(error),
54
+ provider: "resend",
55
+ providerCode: error.name,
56
+ statusCode: error.statusCode,
57
+ retryAfter: retryAfterSeconds(headers?.["retry-after"]),
58
+ cause: error
59
+ });
60
+ }
51
61
  function toAttachment(attachment) {
52
62
  const result = {
53
63
  filename: attachment.filename,
@@ -113,6 +123,31 @@ var ResendEmailService = class ResendEmailService {
113
123
  });
114
124
  return { headers: result.data.headers ?? {} };
115
125
  }
126
+ /**
127
+ * Retrieve the sender, plain text body, and attachments by the received email's provider ID.
128
+ * Each attachment carries a signed download link. Provider failures throw EmailServiceError.
129
+ */
130
+ async getReceivedContent(emailId) {
131
+ const client = await this.getClient();
132
+ const email = await client.emails.receiving.get(emailId);
133
+ if (email.error !== null) throw receivingError(email.error, email.headers);
134
+ const files = await client.emails.receiving.attachments.list({
135
+ emailId,
136
+ limit: 100
137
+ });
138
+ if (files.error !== null) throw receivingError(files.error, files.headers);
139
+ return {
140
+ from: email.data.from,
141
+ text: email.data.text,
142
+ attachments: files.data.data.map((file) => ({
143
+ filename: file.filename ?? null,
144
+ contentType: file.content_type,
145
+ size: file.size,
146
+ downloadUrl: file.download_url,
147
+ expiresAt: file.expires_at
148
+ }))
149
+ };
150
+ }
116
151
  async getClient() {
117
152
  if (this.client === void 0) {
118
153
  const { Resend } = await import("resend");
@@ -1 +1 @@
1
- {"version":3,"file":"resend.js","names":[],"sources":["../src/infrastructure/resend/resend-email-service.ts","../src/infrastructure/resend/email-event.ts","../src/infrastructure/resend/resend-webhook.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nimport type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'\n\nimport type {\n EmailAccepted,\n EmailAttachment,\n IEmailService,\n OutboundEmail,\n ReceivedEmail,\n} from '@/application'\nimport { EmailServiceError, type EmailSendFailureReason } from '@/core'\n\n// SDK additions must receive an explicit classification before the package builds.\nconst SDK_REASONS = {\n invalid_idempotency_key: 'invalid_request',\n validation_error: 'invalid_request',\n missing_api_key: 'configuration',\n restricted_api_key: 'configuration',\n invalid_api_key: 'configuration',\n not_found: 'invalid_request',\n method_not_allowed: 'invalid_request',\n invalid_idempotent_request: 'invalid_request',\n concurrent_idempotent_requests: 'idempotency_conflict',\n invalid_attachment: 'invalid_request',\n invalid_from_address: 'invalid_request',\n invalid_access: 'configuration',\n invalid_parameter: 'invalid_request',\n invalid_region: 'invalid_request',\n missing_required_field: 'invalid_request',\n monthly_quota_exceeded: 'quota_exceeded',\n daily_quota_exceeded: 'quota_exceeded',\n rate_limit_exceeded: 'rate_limited',\n security_error: 'configuration',\n application_error: 'provider_error',\n internal_server_error: 'provider_error',\n} satisfies Record<ErrorResponse['name'], EmailSendFailureReason>\n\n// Resend documents these codes before its SDK includes them in ErrorResponse.\nconst DOCUMENTED_REASONS = new Map<string, EmailSendFailureReason>([\n ['email_above_quota', 'quota_exceeded'],\n ['invalid_permission', 'configuration'],\n ['suspended_api_key', 'configuration'],\n ['resource_locked', 'idempotency_conflict'],\n ['service_unavailable', 'provider_error'],\n ['missing_required_parameter', 'invalid_request'],\n])\n\nfunction reasonFromResend(error: ErrorResponse): EmailSendFailureReason {\n if (Object.hasOwn(SDK_REASONS, error.name)) return SDK_REASONS[error.name]\n const reason = DOCUMENTED_REASONS.get(error.name)\n if (reason !== undefined) return reason\n if (error.statusCode === 429) return 'rate_limited'\n if (error.statusCode !== null && error.statusCode >= 400 && error.statusCode < 500)\n return 'invalid_request'\n return 'provider_error'\n}\n\nfunction retryAfterSeconds(value: string | undefined): number | null {\n if (value === undefined || !/^\\d+$/.test(value)) return null\n const seconds = Number(value)\n return Number.isSafeInteger(seconds) ? seconds : null\n}\n\nfunction toAttachment(attachment: EmailAttachment): Attachment {\n const result: Attachment = {\n filename: attachment.filename,\n content: ArrayBuffer.isView(attachment.content)\n ? Buffer.from(attachment.content)\n : attachment.content,\n }\n if (attachment.contentType !== undefined) result.contentType = attachment.contentType\n return result\n}\n\n/**\n * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.\n * The Resend SDK itself logs API errors outside production.\n */\nexport class ResendEmailService implements IEmailService {\n /** Load the SDK on the first email operation. Constructing the service does not load it. */\n static create(apiKey: string, fromAddress: string): ResendEmailService {\n return new ResendEmailService(apiKey, fromAddress)\n }\n\n private client: Resend | undefined\n\n /** Store configuration. Load the SDK and create its client on the first email operation. */\n constructor(\n private readonly apiKey: string,\n private readonly fromAddress: string,\n ) {}\n\n /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */\n async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {\n const client = await this.getClient()\n const payload: CreateEmailOptions = {\n from: email.from ?? this.fromAddress,\n to: [email.to].flat(),\n subject: email.subject,\n html: email.html,\n }\n if (email.text !== undefined) payload.text = email.text\n if (email.replyTo !== undefined) payload.replyTo = email.replyTo\n if (email.tags !== undefined)\n payload.tags = Object.entries(email.tags).map(([name, value]) => ({ name, value }))\n if (email.attachments !== undefined) payload.attachments = email.attachments.map(toAttachment)\n const result = await client.emails.send(\n payload,\n email.idempotencyKey === undefined ? undefined : { idempotencyKey: email.idempotencyKey },\n )\n if (result.error !== null) {\n throw new EmailServiceError({\n reason: reasonFromResend(result.error),\n provider: 'resend',\n providerCode: result.error.name,\n statusCode: result.error.statusCode,\n retryAfter: retryAfterSeconds(result.headers?.['retry-after']),\n cause: result.error,\n })\n }\n return { providerId: result.data.id }\n }\n\n /** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */\n async getReceivedEmail(emailId: string): Promise<ReceivedEmail> {\n const client = await this.getClient()\n const result = await client.emails.receiving.get(emailId)\n if (result.error !== null) {\n throw new EmailServiceError({\n reason: reasonFromResend(result.error),\n provider: 'resend',\n providerCode: result.error.name,\n statusCode: result.error.statusCode,\n retryAfter: retryAfterSeconds(result.headers?.['retry-after']),\n cause: result.error,\n })\n }\n return { headers: result.data.headers ?? {} }\n }\n\n private async getClient(): Promise<Resend> {\n if (this.client === undefined) {\n const { Resend } = await import('resend')\n // Recheck after the import so concurrent calls share one client.\n this.client ??= new Resend(this.apiKey)\n }\n return this.client\n }\n}\n","import type { WebhookEventPayload } from 'resend'\nimport { z } from 'zod'\n\nimport { EmailAddressSchema, parseBounceType, type BounceType } from '@/core'\n\nconst SIMPLE_EMAIL_TYPES = [\n 'email.sent',\n 'email.scheduled',\n 'email.delivered',\n 'email.delivery_delayed',\n 'email.complained',\n 'email.opened',\n 'email.received',\n 'email.suppressed',\n] as const\n\n/** Resend email event names supported by this module. */\nexport type EmailEventType =\n | (typeof SIMPLE_EMAIL_TYPES)[number]\n | 'email.bounced'\n | 'email.failed'\n | 'email.clicked'\n\nconst EMAIL_TYPES = {\n 'email.sent': true,\n 'email.scheduled': true,\n 'email.delivered': true,\n 'email.delivery_delayed': true,\n 'email.complained': true,\n 'email.opened': true,\n 'email.received': true,\n 'email.suppressed': true,\n 'email.bounced': true,\n 'email.failed': true,\n 'email.clicked': true,\n} satisfies Record<Extract<WebhookEventPayload['type'], `email.${string}`>, true>\n\nconst BaseDataSchema = z.object({\n email_id: z.string().min(1),\n message_id: z.string().min(1),\n to: z.array(EmailAddressSchema).min(1),\n subject: z.string(),\n tags: z.record(z.string(), z.string()).default({}),\n})\n\nconst EmailPayloadSchema = z.discriminatedUnion('type', [\n z.object({ type: z.enum(SIMPLE_EMAIL_TYPES), data: BaseDataSchema }),\n z.object({\n type: z.literal('email.bounced'),\n data: BaseDataSchema.extend({\n bounce: z.object({\n type: z.string().optional(),\n subType: z.string().nullable().optional(),\n message: z.string(),\n }),\n }),\n }),\n z.object({\n type: z.literal('email.failed'),\n data: BaseDataSchema.extend({ failed: z.object({ reason: z.string() }) }),\n }),\n z.object({\n type: z.literal('email.clicked'),\n data: BaseDataSchema.extend({ click: z.object({ link: z.string() }) }),\n }),\n])\n\ntype EmailEventBase = {\n readonly id: string\n readonly emailId: string\n readonly messageId: string\n readonly recipients: readonly string[]\n readonly subject: string\n readonly tags: Readonly<Record<string, string>>\n readonly occurredAt: string\n}\n\n/** A verified event. `id` is the Svix identity; use `resend:event:${id}` as the command id. */\nexport type EmailEvent =\n | (EmailEventBase & {\n readonly type: 'email.bounced'\n readonly bounce: {\n readonly type: BounceType\n readonly subType: string | null\n readonly message: string\n }\n })\n | (EmailEventBase & { readonly type: 'email.failed'; readonly reason: string })\n | (EmailEventBase & { readonly type: 'email.clicked'; readonly link: string })\n | (EmailEventBase & { readonly type: (typeof SIMPLE_EMAIL_TYPES)[number] })\n\nconst EventHeaderSchema = z.object({\n type: z.string(),\n created_at: z.iso.datetime({ offset: true }),\n})\n\n/** Validate signed provider data before projecting it into the public event contract. */\nexport function parseEmailEvent(payload: WebhookEventPayload, id: string): EmailEvent | null {\n const header = EventHeaderSchema.parse(payload)\n if (!Object.hasOwn(EMAIL_TYPES, header.type)) return null\n const event = EmailPayloadSchema.parse(payload)\n const base: EmailEventBase = {\n id,\n emailId: event.data.email_id,\n messageId: event.data.message_id,\n recipients: event.data.to,\n subject: event.data.subject,\n tags: event.data.tags,\n occurredAt: header.created_at,\n }\n switch (event.type) {\n case 'email.bounced':\n return {\n ...base,\n type: event.type,\n bounce: {\n type: parseBounceType(event.data.bounce.type),\n subType: event.data.bounce.subType ?? null,\n message: event.data.bounce.message,\n },\n }\n case 'email.failed':\n return { ...base, type: event.type, reason: event.data.failed.reason }\n case 'email.clicked':\n return { ...base, type: event.type, link: event.data.click.link }\n default:\n return { ...base, type: event.type }\n }\n}\n","import type { Resend } from 'resend'\nimport { z } from 'zod'\n\nimport { WebhookVerificationError, type WebhookVerifier } from '../http/webhook-verification.ts'\nimport { parseEmailEvent, type EmailEvent } from './email-event.ts'\n\n// Separate standardwebhooks versions have different prototypes; its error name remains the same.\nconst SignatureErrorSchema = z\n .instanceof(Error)\n .refine((error) => error.name === 'WebhookVerificationError')\n\n/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */\nexport class ResendWebhook implements WebhookVerifier<EmailEvent> {\n /** Load the SDK on the first verification. Only the webhook secret is required. */\n static create(webhookSecret: string): ResendWebhook {\n return new ResendWebhook(webhookSecret)\n }\n\n private client: Resend['webhooks'] | undefined\n\n /** Store the webhook secret. Load the SDK on the first verification. */\n constructor(private readonly webhookSecret: string) {}\n\n /** Verify the raw body and Svix headers. Invalid requests throw typed errors; other failures propagate. */\n async verify(body: string, headers: Headers): Promise<EmailEvent | null> {\n const id = headers.get('svix-id')\n const timestamp = headers.get('svix-timestamp')\n const signature = headers.get('svix-signature')\n if (!id || !timestamp || !signature) throw new WebhookVerificationError('missing_headers')\n const client = await this.getClient()\n let payload: ReturnType<Resend['webhooks']['verify']>\n try {\n payload = client.verify({\n payload: body,\n headers: { id, timestamp, signature },\n webhookSecret: this.webhookSecret,\n })\n } catch (cause) {\n if (cause instanceof SyntaxError) throw new WebhookVerificationError('invalid_payload', cause)\n if (!SignatureErrorSchema.safeParse(cause).success) throw cause\n throw new WebhookVerificationError('invalid_signature', cause)\n }\n try {\n return parseEmailEvent(payload, id)\n } catch (cause) {\n if (cause instanceof z.ZodError) throw new WebhookVerificationError('invalid_payload', cause)\n throw cause\n }\n }\n\n private async getClient(): Promise<Resend['webhooks']> {\n if (this.client === undefined) {\n const { Resend } = await import('resend')\n // The SDK requires a nonempty key even though its local verifier never uses that key.\n this.client ??= new Resend('re_webhook_verification_only').webhooks\n }\n return this.client\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resend.js","names":[],"sources":["../src/infrastructure/resend/resend-email-service.ts","../src/infrastructure/resend/email-event.ts","../src/infrastructure/resend/resend-webhook.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nimport type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'\n\nimport type {\n EmailAccepted,\n EmailAttachment,\n IEmailService,\n OutboundEmail,\n ReceivedContent,\n ReceivedEmail,\n} from '@/application'\nimport { EmailServiceError, type EmailSendFailureReason } from '@/core'\n\n// SDK additions must receive an explicit classification before the package builds.\nconst SDK_REASONS = {\n invalid_idempotency_key: 'invalid_request',\n validation_error: 'invalid_request',\n missing_api_key: 'configuration',\n restricted_api_key: 'configuration',\n invalid_api_key: 'configuration',\n not_found: 'invalid_request',\n method_not_allowed: 'invalid_request',\n invalid_idempotent_request: 'invalid_request',\n concurrent_idempotent_requests: 'idempotency_conflict',\n invalid_attachment: 'invalid_request',\n invalid_from_address: 'invalid_request',\n invalid_access: 'configuration',\n invalid_parameter: 'invalid_request',\n invalid_region: 'invalid_request',\n missing_required_field: 'invalid_request',\n monthly_quota_exceeded: 'quota_exceeded',\n daily_quota_exceeded: 'quota_exceeded',\n rate_limit_exceeded: 'rate_limited',\n security_error: 'configuration',\n application_error: 'provider_error',\n internal_server_error: 'provider_error',\n} satisfies Record<ErrorResponse['name'], EmailSendFailureReason>\n\n// Resend documents these codes before its SDK includes them in ErrorResponse.\nconst DOCUMENTED_REASONS = new Map<string, EmailSendFailureReason>([\n ['email_above_quota', 'quota_exceeded'],\n ['invalid_permission', 'configuration'],\n ['suspended_api_key', 'configuration'],\n ['resource_locked', 'idempotency_conflict'],\n ['service_unavailable', 'provider_error'],\n ['missing_required_parameter', 'invalid_request'],\n])\n\nfunction reasonFromResend(error: ErrorResponse): EmailSendFailureReason {\n if (Object.hasOwn(SDK_REASONS, error.name)) return SDK_REASONS[error.name]\n const reason = DOCUMENTED_REASONS.get(error.name)\n if (reason !== undefined) return reason\n if (error.statusCode === 429) return 'rate_limited'\n if (error.statusCode !== null && error.statusCode >= 400 && error.statusCode < 500)\n return 'invalid_request'\n return 'provider_error'\n}\n\nfunction retryAfterSeconds(value: string | undefined): number | null {\n if (value === undefined || !/^\\d+$/.test(value)) return null\n const seconds = Number(value)\n return Number.isSafeInteger(seconds) ? seconds : null\n}\n\nfunction receivingError(\n error: ErrorResponse,\n headers: Record<string, string> | null,\n): EmailServiceError {\n return new EmailServiceError({\n reason: reasonFromResend(error),\n provider: 'resend',\n providerCode: error.name,\n statusCode: error.statusCode,\n retryAfter: retryAfterSeconds(headers?.['retry-after']),\n cause: error,\n })\n}\n\nfunction toAttachment(attachment: EmailAttachment): Attachment {\n const result: Attachment = {\n filename: attachment.filename,\n content: ArrayBuffer.isView(attachment.content)\n ? Buffer.from(attachment.content)\n : attachment.content,\n }\n if (attachment.contentType !== undefined) result.contentType = attachment.contentType\n return result\n}\n\n/**\n * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.\n * The Resend SDK itself logs API errors outside production.\n */\nexport class ResendEmailService implements IEmailService {\n /** Load the SDK on the first email operation. Constructing the service does not load it. */\n static create(apiKey: string, fromAddress: string): ResendEmailService {\n return new ResendEmailService(apiKey, fromAddress)\n }\n\n private client: Resend | undefined\n\n /** Store configuration. Load the SDK and create its client on the first email operation. */\n constructor(\n private readonly apiKey: string,\n private readonly fromAddress: string,\n ) {}\n\n /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */\n async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {\n const client = await this.getClient()\n const payload: CreateEmailOptions = {\n from: email.from ?? this.fromAddress,\n to: [email.to].flat(),\n subject: email.subject,\n html: email.html,\n }\n if (email.text !== undefined) payload.text = email.text\n if (email.replyTo !== undefined) payload.replyTo = email.replyTo\n if (email.tags !== undefined)\n payload.tags = Object.entries(email.tags).map(([name, value]) => ({ name, value }))\n if (email.attachments !== undefined) payload.attachments = email.attachments.map(toAttachment)\n const result = await client.emails.send(\n payload,\n email.idempotencyKey === undefined ? undefined : { idempotencyKey: email.idempotencyKey },\n )\n if (result.error !== null) {\n throw new EmailServiceError({\n reason: reasonFromResend(result.error),\n provider: 'resend',\n providerCode: result.error.name,\n statusCode: result.error.statusCode,\n retryAfter: retryAfterSeconds(result.headers?.['retry-after']),\n cause: result.error,\n })\n }\n return { providerId: result.data.id }\n }\n\n /** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */\n async getReceivedEmail(emailId: string): Promise<ReceivedEmail> {\n const client = await this.getClient()\n const result = await client.emails.receiving.get(emailId)\n if (result.error !== null) {\n throw new EmailServiceError({\n reason: reasonFromResend(result.error),\n provider: 'resend',\n providerCode: result.error.name,\n statusCode: result.error.statusCode,\n retryAfter: retryAfterSeconds(result.headers?.['retry-after']),\n cause: result.error,\n })\n }\n return { headers: result.data.headers ?? {} }\n }\n\n /**\n * Retrieve the sender, plain text body, and attachments by the received email's provider ID.\n * Each attachment carries a signed download link. Provider failures throw EmailServiceError.\n */\n async getReceivedContent(emailId: string): Promise<ReceivedContent> {\n const client = await this.getClient()\n const email = await client.emails.receiving.get(emailId)\n if (email.error !== null) throw receivingError(email.error, email.headers)\n // One page of 100, the API maximum, holds the attachments of any received email.\n const files = await client.emails.receiving.attachments.list({ emailId, limit: 100 })\n if (files.error !== null) throw receivingError(files.error, files.headers)\n return {\n from: email.data.from,\n text: email.data.text,\n attachments: files.data.data.map((file) => ({\n filename: file.filename ?? null,\n contentType: file.content_type,\n size: file.size,\n downloadUrl: file.download_url,\n expiresAt: file.expires_at,\n })),\n }\n }\n\n private async getClient(): Promise<Resend> {\n if (this.client === undefined) {\n const { Resend } = await import('resend')\n // Recheck after the import so concurrent calls share one client.\n this.client ??= new Resend(this.apiKey)\n }\n return this.client\n }\n}\n","import type { WebhookEventPayload } from 'resend'\nimport { z } from 'zod'\n\nimport { EmailAddressSchema, parseBounceType, type BounceType } from '@/core'\n\nconst SIMPLE_EMAIL_TYPES = [\n 'email.sent',\n 'email.scheduled',\n 'email.delivered',\n 'email.delivery_delayed',\n 'email.complained',\n 'email.opened',\n 'email.received',\n 'email.suppressed',\n] as const\n\n/** Resend email event names supported by this module. */\nexport type EmailEventType =\n | (typeof SIMPLE_EMAIL_TYPES)[number]\n | 'email.bounced'\n | 'email.failed'\n | 'email.clicked'\n\nconst EMAIL_TYPES = {\n 'email.sent': true,\n 'email.scheduled': true,\n 'email.delivered': true,\n 'email.delivery_delayed': true,\n 'email.complained': true,\n 'email.opened': true,\n 'email.received': true,\n 'email.suppressed': true,\n 'email.bounced': true,\n 'email.failed': true,\n 'email.clicked': true,\n} satisfies Record<Extract<WebhookEventPayload['type'], `email.${string}`>, true>\n\nconst BaseDataSchema = z.object({\n email_id: z.string().min(1),\n message_id: z.string().min(1),\n to: z.array(EmailAddressSchema).min(1),\n subject: z.string(),\n tags: z.record(z.string(), z.string()).default({}),\n})\n\nconst EmailPayloadSchema = z.discriminatedUnion('type', [\n z.object({ type: z.enum(SIMPLE_EMAIL_TYPES), data: BaseDataSchema }),\n z.object({\n type: z.literal('email.bounced'),\n data: BaseDataSchema.extend({\n bounce: z.object({\n type: z.string().optional(),\n subType: z.string().nullable().optional(),\n message: z.string(),\n }),\n }),\n }),\n z.object({\n type: z.literal('email.failed'),\n data: BaseDataSchema.extend({ failed: z.object({ reason: z.string() }) }),\n }),\n z.object({\n type: z.literal('email.clicked'),\n data: BaseDataSchema.extend({ click: z.object({ link: z.string() }) }),\n }),\n])\n\ntype EmailEventBase = {\n readonly id: string\n readonly emailId: string\n readonly messageId: string\n readonly recipients: readonly string[]\n readonly subject: string\n readonly tags: Readonly<Record<string, string>>\n readonly occurredAt: string\n}\n\n/** A verified event. `id` is the Svix identity; use `resend:event:${id}` as the command id. */\nexport type EmailEvent =\n | (EmailEventBase & {\n readonly type: 'email.bounced'\n readonly bounce: {\n readonly type: BounceType\n readonly subType: string | null\n readonly message: string\n }\n })\n | (EmailEventBase & { readonly type: 'email.failed'; readonly reason: string })\n | (EmailEventBase & { readonly type: 'email.clicked'; readonly link: string })\n | (EmailEventBase & { readonly type: (typeof SIMPLE_EMAIL_TYPES)[number] })\n\nconst EventHeaderSchema = z.object({\n type: z.string(),\n created_at: z.iso.datetime({ offset: true }),\n})\n\n/** Validate signed provider data before projecting it into the public event contract. */\nexport function parseEmailEvent(payload: WebhookEventPayload, id: string): EmailEvent | null {\n const header = EventHeaderSchema.parse(payload)\n if (!Object.hasOwn(EMAIL_TYPES, header.type)) return null\n const event = EmailPayloadSchema.parse(payload)\n const base: EmailEventBase = {\n id,\n emailId: event.data.email_id,\n messageId: event.data.message_id,\n recipients: event.data.to,\n subject: event.data.subject,\n tags: event.data.tags,\n occurredAt: header.created_at,\n }\n switch (event.type) {\n case 'email.bounced':\n return {\n ...base,\n type: event.type,\n bounce: {\n type: parseBounceType(event.data.bounce.type),\n subType: event.data.bounce.subType ?? null,\n message: event.data.bounce.message,\n },\n }\n case 'email.failed':\n return { ...base, type: event.type, reason: event.data.failed.reason }\n case 'email.clicked':\n return { ...base, type: event.type, link: event.data.click.link }\n default:\n return { ...base, type: event.type }\n }\n}\n","import type { Resend } from 'resend'\nimport { z } from 'zod'\n\nimport { WebhookVerificationError, type WebhookVerifier } from '../http/webhook-verification.ts'\nimport { parseEmailEvent, type EmailEvent } from './email-event.ts'\n\n// Separate standardwebhooks versions have different prototypes; its error name remains the same.\nconst SignatureErrorSchema = z\n .instanceof(Error)\n .refine((error) => error.name === 'WebhookVerificationError')\n\n/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */\nexport class ResendWebhook implements WebhookVerifier<EmailEvent> {\n /** Load the SDK on the first verification. Only the webhook secret is required. */\n static create(webhookSecret: string): ResendWebhook {\n return new ResendWebhook(webhookSecret)\n }\n\n private client: Resend['webhooks'] | undefined\n\n /** Store the webhook secret. Load the SDK on the first verification. */\n constructor(private readonly webhookSecret: string) {}\n\n /** Verify the raw body and Svix headers. Invalid requests throw typed errors; other failures propagate. */\n async verify(body: string, headers: Headers): Promise<EmailEvent | null> {\n const id = headers.get('svix-id')\n const timestamp = headers.get('svix-timestamp')\n const signature = headers.get('svix-signature')\n if (!id || !timestamp || !signature) throw new WebhookVerificationError('missing_headers')\n const client = await this.getClient()\n let payload: ReturnType<Resend['webhooks']['verify']>\n try {\n payload = client.verify({\n payload: body,\n headers: { id, timestamp, signature },\n webhookSecret: this.webhookSecret,\n })\n } catch (cause) {\n if (cause instanceof SyntaxError) throw new WebhookVerificationError('invalid_payload', cause)\n if (!SignatureErrorSchema.safeParse(cause).success) throw cause\n throw new WebhookVerificationError('invalid_signature', cause)\n }\n try {\n return parseEmailEvent(payload, id)\n } catch (cause) {\n if (cause instanceof z.ZodError) throw new WebhookVerificationError('invalid_payload', cause)\n throw cause\n }\n }\n\n private async getClient(): Promise<Resend['webhooks']> {\n if (this.client === undefined) {\n const { Resend } = await import('resend')\n // The SDK requires a nonempty key even though its local verifier never uses that key.\n this.client ??= new Resend('re_webhook_verification_only').webhooks\n }\n return this.client\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { u as LoggerErrorHook } from "../index-DUFS0SJ_.js";
2
- import { G as ObservabilityUser, U as ObservabilityScope, W as ObservabilityService } from "../index-tNZHSCpJ.js";
2
+ import { G as ObservabilityScope, K as ObservabilityService, q as ObservabilityUser } from "../index-lS_g1olK.js";
3
3
  import * as Sentry from "@sentry/cloudflare";
4
4
  //#region src/infrastructure/sentry/server.d.ts
5
5
  /** Application-owned deployment settings with the full Cloudflare SDK option contract. */
@@ -34,6 +34,12 @@ type AuthContext<Session extends AuthSession> = RequiredAuthContext<Session> | {
34
34
  readonly user: undefined;
35
35
  readonly session: undefined;
36
36
  };
37
+ /** A resolved session and the request context that produced it. */
38
+ type AuthenticatedContext<Session extends AuthSession> = {
39
+ readonly user: Session['user'];
40
+ readonly session: Session['session'];
41
+ readonly requestContext: RequestContext;
42
+ };
37
43
  /** Select the current request's Better Auth instance from TanStack's registered context. */
38
44
  type AuthMiddlewareOptions<Session extends AuthSession> = {
39
45
  readonly getAuth: (context: RequestContext) => {
@@ -43,10 +49,13 @@ type AuthMiddlewareOptions<Session extends AuthSession> = {
43
49
  }) => Promise<Session | null>;
44
50
  };
45
51
  };
52
+ /** Runs after a session resolves and before the handler. A guest or a failed lookup does not call it. */
53
+ readonly onAuthenticated?: (context: AuthenticatedContext<Session>) => void;
46
54
  };
47
55
  /**
48
56
  * Preserve Porte's function and request middleware contracts with application-owned services.
49
57
  * Optional lookup failures log once and continue as guests. Required lookup failures propagate.
58
+ * `onAuthenticated` runs after a session resolves and before the rest of the chain.
50
59
  */
51
60
  export declare function createAuthMiddleware<Session extends AuthSession>(options: AuthMiddlewareOptions<Session>): {
52
61
  extractAuth: import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, AuthContext<Session>, undefined, undefined, undefined>;
@@ -88,5 +97,5 @@ export declare function createErrorMiddleware<Entries>(options: ErrorMiddlewareO
88
97
  toHttpErrorResponse: (cause: unknown) => Response;
89
98
  };
90
99
  //#endregion
91
- export type { AuthContext, AuthCookieSource, AuthMiddlewareOptions, AuthSession, ErrorMiddlewareOptions, RequiredAuthContext };
100
+ export type { AuthContext, AuthCookieSource, AuthMiddlewareOptions, AuthSession, AuthenticatedContext, ErrorMiddlewareOptions, RequiredAuthContext };
92
101
  //# sourceMappingURL=tanstack.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tanstack.d.ts","names":[],"sources":["../src/infrastructure/tanstack/request-context.ts","../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/error.middleware.ts"],"mappings":";;;;;;;;;;KAQY,iBAAiB;EAAmB;IAAU,sBAAsB;;IAC5E;;;;KCCQ;WACD;aAAiB;;WACjB;aAAoB;;;;KAInB,oBAAoB,gBAAgB;WACrC,MAAM;WACN,SAAS;;;KAIR,YAAY,gBAAgB,eACpC,oBAAoB;WAET;WACA;;;KAIH,sBAAsB,gBAAgB;WACvC,UAAU,SAAS;aACjB;eACE,aAAa;QAAW,SAAS;YAAc,QAAQ;;;;;;;;wBAetD,qBAAqB,gBAAgB,aACnD,SAAS,sBAAsB;;;;;;;;KCrCrB;WACD,UAAU;aAAmB,aAAa;;;;KAIzC,uBAAuB;WACxB,QAAQ,WAAW;;WAEnB;;WAEA,UAAU,SAAS,mBAAmB;;;;;;wBAOjC,sBAAsB,SAAS,SAAS,uBAAuB"}
1
+ {"version":3,"file":"tanstack.d.ts","names":[],"sources":["../src/infrastructure/tanstack/request-context.ts","../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/error.middleware.ts"],"mappings":";;;;;;;;;;KAQY,iBAAiB;EAAmB;IAAU,sBAAsB;;IAC5E;;;;KCCQ;WACD;aAAiB;;WACjB;aAAoB;;;;KAInB,oBAAoB,gBAAgB;WACrC,MAAM;WACN,SAAS;;;KAIR,YAAY,gBAAgB,eACpC,oBAAoB;WAET;WACA;;;KAIH,qBAAqB,gBAAgB;WACtC,MAAM;WACN,SAAS;WACT,gBAAgB;;;KAIf,sBAAsB,gBAAgB;WACvC,UAAU,SAAS;aACjB;eACE,aAAa;QAAW,SAAS;YAAc,QAAQ;;;;WAI3D,mBAAmB,SAAS,qBAAqB;;;;;;;wBAc5C,qBAAqB,gBAAgB,aACnD,SAAS,sBAAsB;;;;;;;;KC/CrB;WACD,UAAU;aAAmB,aAAa;;;;KAIzC,uBAAuB;WACxB,QAAQ,WAAW;;WAEnB;;WAEA,UAAU,SAAS,mBAAmB;;;;;;wBAOjC,sBAAsB,SAAS,SAAS,uBAAuB"}
package/dist/tanstack.js CHANGED
@@ -14,8 +14,16 @@ const requestHeaders = async () => {
14
14
  /**
15
15
  * Preserve Porte's function and request middleware contracts with application-owned services.
16
16
  * Optional lookup failures log once and continue as guests. Required lookup failures propagate.
17
+ * `onAuthenticated` runs after a session resolves and before the rest of the chain.
17
18
  */
18
19
  function createAuthMiddleware(options) {
20
+ function notifyAuthenticated(requestContext, resolved) {
21
+ options.onAuthenticated?.({
22
+ user: resolved.user,
23
+ session: resolved.session,
24
+ requestContext
25
+ });
26
+ }
19
27
  const extractAuth = createMiddleware({ type: "function" }).server(async ({ next, context }) => {
20
28
  let resolved;
21
29
  try {
@@ -29,6 +37,7 @@ function createAuthMiddleware(options) {
29
37
  user: void 0,
30
38
  session: void 0
31
39
  } });
40
+ notifyAuthenticated(context, resolved);
32
41
  return next({ context: {
33
42
  user: resolved.user,
34
43
  session: resolved.session
@@ -46,6 +55,7 @@ function createAuthMiddleware(options) {
46
55
  extractAuth,
47
56
  requireAuth: createMiddleware({ type: "function" }).server(async ({ next, context }) => {
48
57
  const resolved = await resolveRequiredSession(context, await requestHeaders());
58
+ notifyAuthenticated(context, resolved);
49
59
  return next({ context: {
50
60
  user: resolved.user,
51
61
  session: resolved.session
@@ -53,6 +63,7 @@ function createAuthMiddleware(options) {
53
63
  }),
54
64
  requireAuthRequest: createMiddleware({ type: "request" }).server(async ({ request, next, context }) => {
55
65
  const resolved = await resolveRequiredSession(context, request.headers);
66
+ notifyAuthenticated(context, resolved);
56
67
  return next({ context: {
57
68
  user: resolved.user,
58
69
  session: resolved.session
@@ -1 +1 @@
1
- {"version":3,"file":"tanstack.js","names":["logger"],"sources":["../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/resolve-failure.ts","../src/infrastructure/tanstack/http-error-response.ts","../src/infrastructure/tanstack/session-cookies.ts","../src/infrastructure/tanstack/to-error-payload.ts","../src/infrastructure/tanstack/error.middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\nimport { getSessionCookie } from 'better-auth/cookies'\n\nimport { AuthenticationError, StaleSessionError, createLogger } from '@/core'\n\nimport type { RequestContext } from './request-context.ts'\n\nconst logger = createLogger('auth-middleware')\n\n/** The fields middleware reads; generic callers retain every additional session and user field. */\nexport type AuthSession = {\n readonly user: { readonly id: string }\n readonly session: { readonly id: string }\n}\n\n/** Authenticated context retains the application's complete user and session types. */\nexport type RequiredAuthContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n}\n\n/** Optional authentication supplies both fields together or neither field. */\nexport type AuthContext<Session extends AuthSession> =\n | RequiredAuthContext<Session>\n | {\n readonly user: undefined\n readonly session: undefined\n }\n\n/** Select the current request's Better Auth instance from TanStack's registered context. */\nexport type AuthMiddlewareOptions<Session extends AuthSession> = {\n readonly getAuth: (context: RequestContext) => {\n readonly api: {\n readonly getSession: (options: { headers: Headers }) => Promise<Session | null>\n }\n }\n}\n\n// The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily inside `.server()`.\nconst requestHeaders = async () => {\n const { getRequestHeaders } = await import('@tanstack/react-start/server')\n return getRequestHeaders()\n}\n\n/**\n * Preserve Porte's function and request middleware contracts with application-owned services.\n * Optional lookup failures log once and continue as guests. Required lookup failures propagate.\n */\nexport function createAuthMiddleware<Session extends AuthSession>(\n options: AuthMiddlewareOptions<Session>,\n) {\n const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(\n async ({ next, context }) => {\n let resolved: Session | null\n try {\n const headers = await requestHeaders()\n resolved = await options.getAuth(context).api.getSession({ headers })\n } catch (error) {\n logger.error('get_session_failed', { error })\n resolved = null\n }\n if (resolved === null)\n return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })\n return next<AuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n\n async function resolveRequiredSession(\n context: RequestContext,\n headers: Headers,\n ): Promise<Session> {\n const resolved = await options.getAuth(context).api.getSession({ headers })\n if (resolved === null) {\n if (getSessionCookie(headers)) throw new StaleSessionError()\n throw new AuthenticationError()\n }\n return resolved\n }\n\n const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {\n const resolved = await resolveRequiredSession(context, await requestHeaders())\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n })\n const requireAuthRequest = createMiddleware({ type: 'request' }).server(\n async ({ request, next, context }) => {\n const resolved = await resolveRequiredSession(context, request.headers)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n return { extractAuth, requireAuth, requireAuthRequest }\n}\n","import { AppError, createLogger, type ErrorTable, type ResolvedError } from '@/core'\nimport type { ErrorPayloadOf } from '@/core'\n\nconst logger = createLogger('error-boundary')\n\n/**\n * Resolve one failure through the table or fall back to the generic replacement, and log it once.\n * Both transport converters call this, so one rule decides the log level: mapped is a warning, unmapped is an error with the cause.\n */\nexport function resolveFailure<Entries>(\n table: ErrorTable<Entries>,\n cause: unknown,\n): ResolvedError<ErrorPayloadOf<ErrorTable<Entries>>> {\n const resolved = table.resolve(cause)\n if (resolved !== null) {\n logger.warn('handled_error', {\n tag: resolved.payload._tag,\n status: resolved.status,\n })\n return resolved\n }\n const classification = cause instanceof AppError ? cause.classification : 'unknown'\n logger.error('unexpected_error', { error: cause, classification })\n return table.fallback(cause)\n}\n","import { getReasonPhrase } from 'http-status-codes'\nimport { z } from 'zod'\n\nimport type { ErrorTable, ProblemDetails } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// A known delay becomes the header; a null delay sends no header.\nconst RetryAfterSchema = z.object({ retryAfter: z.number() })\n\n/**\n * Build the API route converter for one error table.\n * The body is RFC 9457 problem details; public fields become extension members.\n */\nexport function createHttpErrorResponseConverter<Entries>(table: ErrorTable<Entries>) {\n return function toHttpErrorResponse(cause: unknown): Response {\n const { status, payload } = resolveFailure(table, cause)\n const { _tag, message, ...fields } = payload\n const body: ProblemDetails = {\n type: 'about:blank',\n title: getReasonPhrase(status),\n detail: message,\n ...fields,\n }\n const headers = new Headers({ 'Content-Type': 'application/problem+json' })\n const retry = RetryAfterSchema.safeParse(payload)\n if (retry.success) headers.set('Retry-After', String(retry.data.retryAfter))\n return Response.json(body, { status, headers })\n }\n}\n","import type { deleteCookie } from '@tanstack/react-start/server'\nimport type { BetterAuthCookies } from 'better-auth'\nimport { parseCookies } from 'better-auth/cookies'\n\ntype AuthCookie = BetterAuthCookies['sessionToken']\ntype DeleteCookieOptions = NonNullable<Parameters<typeof deleteCookie>[1]>\n\n/**\n * Delete the session, session cache, account cache, and remember-me cookies of one instance.\n * Chunks present on the request go too. Names and attributes come from the instance, so custom prefixes and domain cookies clear.\n */\nexport async function clearStaleSessionCookies(cookies: BetterAuthCookies): Promise<void> {\n // The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily here.\n const server = await import('@tanstack/react-start/server')\n const present = [...parseCookies(server.getRequestHeaders().get('cookie') ?? '').keys()]\n const definitions = [\n cookies.sessionToken,\n cookies.sessionData,\n cookies.accountData,\n cookies.dontRememberToken,\n ]\n for (const cookie of definitions) {\n // Better Auth names a chunk `${name}.${index}`, so a missed chunk would resurrect the value.\n const chunks = present.filter((name) => name.startsWith(`${cookie.name}.`))\n for (const name of new Set([cookie.name, ...chunks]))\n server.deleteCookie(name, deletionOf(cookie.attributes))\n }\n}\n\nfunction deletionOf(attributes: AuthCookie['attributes']): DeleteCookieOptions {\n // Better Auth sets `/` and the secure flag at runtime; the types leave both optional.\n const options: DeleteCookieOptions = {\n path: attributes.path ?? '/',\n secure: attributes.secure ?? false,\n }\n if (attributes.domain !== undefined) options.domain = attributes.domain\n return options\n}\n","import { z } from 'zod'\n\nimport { ValidationError, type ErrorPayloadOf, type ErrorTable } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// TanStack serializes a validator's issues into `Error.message` as JSON.\nconst SerializedIssuesSchema = z\n .array(z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])) }))\n .min(1)\n\nfunction reconstructValidationError(cause: unknown): ValidationError | null {\n if (!(cause instanceof Error)) return null\n let message: unknown\n try {\n message = JSON.parse(cause.message)\n } catch {\n return null\n }\n const parsed = SerializedIssuesSchema.safeParse(message)\n return parsed.success ? ValidationError.fromIssues(parsed.data) : null\n}\n\n/**\n * Build the server-function converter for one error table.\n * The result is a plain object, because only own data survives serialization.\n */\nexport function createErrorPayloadConverter<Entries>(table: ErrorTable<Entries>) {\n return function toErrorPayload(cause: unknown): ErrorPayloadOf<ErrorTable<Entries>> {\n return resolveFailure(table, reconstructValidationError(cause) ?? cause).payload\n }\n}\n","import { isNotFound, isRedirect, redirect } from '@tanstack/react-router'\nimport { createMiddleware } from '@tanstack/react-start'\nimport type { BetterAuthCookies } from 'better-auth'\n\nimport { StaleSessionError, type ErrorTable } from '@/core'\n\nimport { createHttpErrorResponseConverter } from './http-error-response.ts'\nimport type { RequestContext } from './request-context.ts'\nimport { clearStaleSessionCookies } from './session-cookies.ts'\nimport { createErrorPayloadConverter } from './to-error-payload.ts'\n\n/** The part of a Better Auth instance the boundary reads: its computed cookie definitions. */\nexport type AuthCookieSource = {\n readonly $context: Promise<{ readonly authCookies: BetterAuthCookies }>\n}\n\n/** The application's error table, its stale-session redirect, and its Better Auth instance. */\nexport type ErrorMiddlewareOptions<Entries> = {\n readonly errors: ErrorTable<Entries>\n /** The path a server function redirects to for a stale session, with any search string. */\n readonly signIn: string\n /** Select the request's Better Auth instance; its cookie definitions say what to clear. */\n readonly getAuth: (context: RequestContext) => AuthCookieSource\n}\n\n/**\n * Build both transport boundaries from one error table.\n * Register `functionErrorMiddleware` globally. Put `routeErrorMiddleware` first in an API route's `server.middleware`.\n */\nexport function createErrorMiddleware<Entries>(options: ErrorMiddlewareOptions<Entries>) {\n const toErrorPayload = createErrorPayloadConverter(options.errors)\n const toHttpErrorResponse = createHttpErrorResponseConverter(options.errors)\n\n async function clearSession(context: RequestContext): Promise<void> {\n const { authCookies } = await options.getAuth(context).$context\n await clearStaleSessionCookies(authCookies)\n }\n\n async function handleFunctionError(cause: unknown, context: RequestContext): Promise<never> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) {\n await clearSession(context)\n // oxlint-disable-next-line typescript/only-throw-error -- Router redirects by throwing this.\n throw redirect({ href: options.signIn })\n }\n // oxlint-disable-next-line typescript/only-throw-error -- The client rejects with this and reads its tag.\n throw toErrorPayload(cause)\n }\n\n // A route answers 401 instead of a redirect because fetch would follow it into the HTML shell.\n async function handleRouteError(cause: unknown, context: RequestContext): Promise<Response> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) await clearSession(context)\n return toHttpErrorResponse(cause)\n }\n\n const functionErrorMiddleware = createMiddleware({ type: 'function' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleFunctionError(cause, context)\n }\n },\n )\n\n const routeErrorMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleRouteError(cause, context)\n }\n },\n )\n\n return { functionErrorMiddleware, routeErrorMiddleware, toErrorPayload, toHttpErrorResponse }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tanstack.js","names":["logger"],"sources":["../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/resolve-failure.ts","../src/infrastructure/tanstack/http-error-response.ts","../src/infrastructure/tanstack/session-cookies.ts","../src/infrastructure/tanstack/to-error-payload.ts","../src/infrastructure/tanstack/error.middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\nimport { getSessionCookie } from 'better-auth/cookies'\n\nimport { AuthenticationError, StaleSessionError, createLogger } from '@/core'\n\nimport type { RequestContext } from './request-context.ts'\n\nconst logger = createLogger('auth-middleware')\n\n/** The fields middleware reads; generic callers retain every additional session and user field. */\nexport type AuthSession = {\n readonly user: { readonly id: string }\n readonly session: { readonly id: string }\n}\n\n/** Authenticated context retains the application's complete user and session types. */\nexport type RequiredAuthContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n}\n\n/** Optional authentication supplies both fields together or neither field. */\nexport type AuthContext<Session extends AuthSession> =\n | RequiredAuthContext<Session>\n | {\n readonly user: undefined\n readonly session: undefined\n }\n\n/** A resolved session and the request context that produced it. */\nexport type AuthenticatedContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n readonly requestContext: RequestContext\n}\n\n/** Select the current request's Better Auth instance from TanStack's registered context. */\nexport type AuthMiddlewareOptions<Session extends AuthSession> = {\n readonly getAuth: (context: RequestContext) => {\n readonly api: {\n readonly getSession: (options: { headers: Headers }) => Promise<Session | null>\n }\n }\n /** Runs after a session resolves and before the handler. A guest or a failed lookup does not call it. */\n readonly onAuthenticated?: (context: AuthenticatedContext<Session>) => void\n}\n\n// The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily inside `.server()`.\nconst requestHeaders = async () => {\n const { getRequestHeaders } = await import('@tanstack/react-start/server')\n return getRequestHeaders()\n}\n\n/**\n * Preserve Porte's function and request middleware contracts with application-owned services.\n * Optional lookup failures log once and continue as guests. Required lookup failures propagate.\n * `onAuthenticated` runs after a session resolves and before the rest of the chain.\n */\nexport function createAuthMiddleware<Session extends AuthSession>(\n options: AuthMiddlewareOptions<Session>,\n) {\n function notifyAuthenticated(requestContext: RequestContext, resolved: Session): void {\n options.onAuthenticated?.({\n user: resolved.user,\n session: resolved.session,\n requestContext,\n })\n }\n const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(\n async ({ next, context }) => {\n let resolved: Session | null\n try {\n const headers = await requestHeaders()\n resolved = await options.getAuth(context).api.getSession({ headers })\n } catch (error) {\n logger.error('get_session_failed', { error })\n resolved = null\n }\n if (resolved === null)\n return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })\n notifyAuthenticated(context, resolved)\n return next<AuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n\n async function resolveRequiredSession(\n context: RequestContext,\n headers: Headers,\n ): Promise<Session> {\n const resolved = await options.getAuth(context).api.getSession({ headers })\n if (resolved === null) {\n if (getSessionCookie(headers)) throw new StaleSessionError()\n throw new AuthenticationError()\n }\n return resolved\n }\n\n const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {\n const resolved = await resolveRequiredSession(context, await requestHeaders())\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n })\n const requireAuthRequest = createMiddleware({ type: 'request' }).server(\n async ({ request, next, context }) => {\n const resolved = await resolveRequiredSession(context, request.headers)\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n return { extractAuth, requireAuth, requireAuthRequest }\n}\n","import { AppError, createLogger, type ErrorTable, type ResolvedError } from '@/core'\nimport type { ErrorPayloadOf } from '@/core'\n\nconst logger = createLogger('error-boundary')\n\n/**\n * Resolve one failure through the table or fall back to the generic replacement, and log it once.\n * Both transport converters call this, so one rule decides the log level: mapped is a warning, unmapped is an error with the cause.\n */\nexport function resolveFailure<Entries>(\n table: ErrorTable<Entries>,\n cause: unknown,\n): ResolvedError<ErrorPayloadOf<ErrorTable<Entries>>> {\n const resolved = table.resolve(cause)\n if (resolved !== null) {\n logger.warn('handled_error', {\n tag: resolved.payload._tag,\n status: resolved.status,\n })\n return resolved\n }\n const classification = cause instanceof AppError ? cause.classification : 'unknown'\n logger.error('unexpected_error', { error: cause, classification })\n return table.fallback(cause)\n}\n","import { getReasonPhrase } from 'http-status-codes'\nimport { z } from 'zod'\n\nimport type { ErrorTable, ProblemDetails } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// A known delay becomes the header; a null delay sends no header.\nconst RetryAfterSchema = z.object({ retryAfter: z.number() })\n\n/**\n * Build the API route converter for one error table.\n * The body is RFC 9457 problem details; public fields become extension members.\n */\nexport function createHttpErrorResponseConverter<Entries>(table: ErrorTable<Entries>) {\n return function toHttpErrorResponse(cause: unknown): Response {\n const { status, payload } = resolveFailure(table, cause)\n const { _tag, message, ...fields } = payload\n const body: ProblemDetails = {\n type: 'about:blank',\n title: getReasonPhrase(status),\n detail: message,\n ...fields,\n }\n const headers = new Headers({ 'Content-Type': 'application/problem+json' })\n const retry = RetryAfterSchema.safeParse(payload)\n if (retry.success) headers.set('Retry-After', String(retry.data.retryAfter))\n return Response.json(body, { status, headers })\n }\n}\n","import type { deleteCookie } from '@tanstack/react-start/server'\nimport type { BetterAuthCookies } from 'better-auth'\nimport { parseCookies } from 'better-auth/cookies'\n\ntype AuthCookie = BetterAuthCookies['sessionToken']\ntype DeleteCookieOptions = NonNullable<Parameters<typeof deleteCookie>[1]>\n\n/**\n * Delete the session, session cache, account cache, and remember-me cookies of one instance.\n * Chunks present on the request go too. Names and attributes come from the instance, so custom prefixes and domain cookies clear.\n */\nexport async function clearStaleSessionCookies(cookies: BetterAuthCookies): Promise<void> {\n // The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily here.\n const server = await import('@tanstack/react-start/server')\n const present = [...parseCookies(server.getRequestHeaders().get('cookie') ?? '').keys()]\n const definitions = [\n cookies.sessionToken,\n cookies.sessionData,\n cookies.accountData,\n cookies.dontRememberToken,\n ]\n for (const cookie of definitions) {\n // Better Auth names a chunk `${name}.${index}`, so a missed chunk would resurrect the value.\n const chunks = present.filter((name) => name.startsWith(`${cookie.name}.`))\n for (const name of new Set([cookie.name, ...chunks]))\n server.deleteCookie(name, deletionOf(cookie.attributes))\n }\n}\n\nfunction deletionOf(attributes: AuthCookie['attributes']): DeleteCookieOptions {\n // Better Auth sets `/` and the secure flag at runtime; the types leave both optional.\n const options: DeleteCookieOptions = {\n path: attributes.path ?? '/',\n secure: attributes.secure ?? false,\n }\n if (attributes.domain !== undefined) options.domain = attributes.domain\n return options\n}\n","import { z } from 'zod'\n\nimport { ValidationError, type ErrorPayloadOf, type ErrorTable } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// TanStack serializes a validator's issues into `Error.message` as JSON.\nconst SerializedIssuesSchema = z\n .array(z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])) }))\n .min(1)\n\nfunction reconstructValidationError(cause: unknown): ValidationError | null {\n if (!(cause instanceof Error)) return null\n let message: unknown\n try {\n message = JSON.parse(cause.message)\n } catch {\n return null\n }\n const parsed = SerializedIssuesSchema.safeParse(message)\n return parsed.success ? ValidationError.fromIssues(parsed.data) : null\n}\n\n/**\n * Build the server-function converter for one error table.\n * The result is a plain object, because only own data survives serialization.\n */\nexport function createErrorPayloadConverter<Entries>(table: ErrorTable<Entries>) {\n return function toErrorPayload(cause: unknown): ErrorPayloadOf<ErrorTable<Entries>> {\n return resolveFailure(table, reconstructValidationError(cause) ?? cause).payload\n }\n}\n","import { isNotFound, isRedirect, redirect } from '@tanstack/react-router'\nimport { createMiddleware } from '@tanstack/react-start'\nimport type { BetterAuthCookies } from 'better-auth'\n\nimport { StaleSessionError, type ErrorTable } from '@/core'\n\nimport { createHttpErrorResponseConverter } from './http-error-response.ts'\nimport type { RequestContext } from './request-context.ts'\nimport { clearStaleSessionCookies } from './session-cookies.ts'\nimport { createErrorPayloadConverter } from './to-error-payload.ts'\n\n/** The part of a Better Auth instance the boundary reads: its computed cookie definitions. */\nexport type AuthCookieSource = {\n readonly $context: Promise<{ readonly authCookies: BetterAuthCookies }>\n}\n\n/** The application's error table, its stale-session redirect, and its Better Auth instance. */\nexport type ErrorMiddlewareOptions<Entries> = {\n readonly errors: ErrorTable<Entries>\n /** The path a server function redirects to for a stale session, with any search string. */\n readonly signIn: string\n /** Select the request's Better Auth instance; its cookie definitions say what to clear. */\n readonly getAuth: (context: RequestContext) => AuthCookieSource\n}\n\n/**\n * Build both transport boundaries from one error table.\n * Register `functionErrorMiddleware` globally. Put `routeErrorMiddleware` first in an API route's `server.middleware`.\n */\nexport function createErrorMiddleware<Entries>(options: ErrorMiddlewareOptions<Entries>) {\n const toErrorPayload = createErrorPayloadConverter(options.errors)\n const toHttpErrorResponse = createHttpErrorResponseConverter(options.errors)\n\n async function clearSession(context: RequestContext): Promise<void> {\n const { authCookies } = await options.getAuth(context).$context\n await clearStaleSessionCookies(authCookies)\n }\n\n async function handleFunctionError(cause: unknown, context: RequestContext): Promise<never> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) {\n await clearSession(context)\n // oxlint-disable-next-line typescript/only-throw-error -- Router redirects by throwing this.\n throw redirect({ href: options.signIn })\n }\n // oxlint-disable-next-line typescript/only-throw-error -- The client rejects with this and reads its tag.\n throw toErrorPayload(cause)\n }\n\n // A route answers 401 instead of a redirect because fetch would follow it into the HTML shell.\n async function handleRouteError(cause: unknown, context: RequestContext): Promise<Response> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) await clearSession(context)\n return toHttpErrorResponse(cause)\n }\n\n const functionErrorMiddleware = createMiddleware({ type: 'function' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleFunctionError(cause, context)\n }\n },\n )\n\n const routeErrorMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleRouteError(cause, context)\n }\n },\n )\n\n return { functionErrorMiddleware, routeErrorMiddleware, toErrorPayload, toHttpErrorResponse }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { W as DomainEvent } from "../index-DUFS0SJ_.js";
2
- import { A as IOutboxRepository } from "../index-tNZHSCpJ.js";
2
+ import { A as IOutboxRepository } from "../index-lS_g1olK.js";
3
3
  //#region src/infrastructure/testing/outbox-repository-contract.d.ts
4
4
  /** Stored fields observed by the adapter contract, independent of a database library. */
5
5
  export type OutboxContractRow = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.23.2",
4
+ "version": "0.25.0",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -33,6 +33,7 @@
33
33
  "./cloudflare/r2-client": "./dist/cloudflare/r2-client.js",
34
34
  "./cloudflare/r2-errors": "./dist/cloudflare/r2-errors.js",
35
35
  "./cloudflare/scheduled": "./dist/cloudflare/scheduled.js",
36
+ "./cloudflare/tracing": "./dist/cloudflare/tracing.js",
36
37
  "./core": "./dist/core.js",
37
38
  "./http": "./dist/http.js",
38
39
  "./postgres": "./dist/postgres.js",
@@ -35,6 +35,23 @@ export type OutboundEmail = {
35
35
  /** Received email headers, without the body or attachments. */
36
36
  export type ReceivedEmail = { readonly headers: Readonly<Record<string, string>> }
37
37
 
38
+ /** One attachment of a received email. `downloadUrl` is a signed link that stops working at `expiresAt`. */
39
+ export type ReceivedAttachment = {
40
+ readonly filename: string | null
41
+ readonly contentType: string
42
+ readonly size: number
43
+ readonly downloadUrl: string
44
+ readonly expiresAt: string
45
+ }
46
+
47
+ /** A received email's sender, plain text body, and attachments, for an application that reads mail it receives. */
48
+ export type ReceivedContent = {
49
+ /** The sender as the email states it, for example "Display Name <address@domain.com>". */
50
+ readonly from: string
51
+ readonly text: string | null
52
+ readonly attachments: readonly ReceivedAttachment[]
53
+ }
54
+
38
55
  /** The provider accepted the email and assigned its id. */
39
56
  export type EmailAccepted = { readonly providerId: string }
40
57
 
@@ -20,6 +20,8 @@ export {
20
20
  type MimeType,
21
21
  type EmailAccepted,
22
22
  type ReceivedEmail,
23
+ type ReceivedContent,
24
+ type ReceivedAttachment,
23
25
  } from './email-service.ts'
24
26
  export {
25
27
  recordEmailEvent,
@@ -26,7 +26,7 @@ export type DispatchBus<TMessage extends QueueMessage> = {
26
26
  /**
27
27
  * Parses and dispatches each queue message, then selects acknowledgement or retry.
28
28
  * Cloudflare owns the retry limit and sends exhausted messages to the configured dead letter.
29
- * The consumer logs each failure once; handlers do not.
29
+ * The consumer logs each failure once, inside the dispatch wrapper; handlers do not.
30
30
  */
31
31
  export class QueueConsumer<TMessage extends QueueMessage> {
32
32
  /**
@@ -59,6 +59,8 @@ export class QueueConsumer<TMessage extends QueueMessage> {
59
59
 
60
60
  private async processMessage(message: Message): Promise<void> {
61
61
  let parsed: TMessage | null = null
62
+ let handlerFailed = false
63
+ let settled = false
62
64
 
63
65
  try {
64
66
  const body = await this.parseMessage(message)
@@ -74,10 +76,28 @@ export class QueueConsumer<TMessage extends QueueMessage> {
74
76
  parsed = body
75
77
 
76
78
  await this.wrapDispatch(body, message.attempts, async () => {
77
- await this.bus.handle(body)
79
+ try {
80
+ await this.bus.handle(body)
81
+ } catch (cause) {
82
+ handlerFailed = true
83
+ this.handleFailure(message, body, cause)
84
+ settled = true
85
+ // The wrapper still sees the rejection, and this method does not settle it again.
86
+ throw cause
87
+ }
78
88
  })
89
+ } catch (cause) {
90
+ if (settled) return
91
+ // A settlement inside run threw: reject the batch, as a settlement failure does elsewhere.
92
+ if (handlerFailed) throw cause
93
+ this.handleFailure(message, parsed, cause)
94
+ return
95
+ }
96
+ // A wrapper can resolve after run rejects; that message is already settled.
97
+ if (parsed === null || handlerFailed) return
98
+ try {
79
99
  message.ack()
80
- logger.debug('queue_message_processed', { name: body.name })
100
+ logger.debug('queue_message_processed', { name: parsed.name })
81
101
  } catch (cause) {
82
102
  this.handleFailure(message, parsed, cause)
83
103
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Workers tracing for Workers Issues.
3
+ *
4
+ * Ported from Suparelay `apps/web/src/server/infrastructure/tracing/span.ts`.
5
+ * Pass `tracing` in. This file does not import `cloudflare:workers`.
6
+ * A client bundle can import a server module that uses these helpers.
7
+ *
8
+ * Issues shows the active span's attributes beside a failure. Put identifiers on the span.
9
+ * Outside an invocation there is no active span, and a missing span records nothing.
10
+ */
11
+
12
+ /** Attribute values a Workers span accepts. `undefined` leaves that attribute unset. */
13
+ export type SpanAttributes = Record<string, string | number | boolean | undefined>
14
+
15
+ /** Add attributes to the active span. That span is the invocation root unless `withSpan` encloses the call. */
16
+ export function annotateSpan(tracing: Tracing, attributes: SpanAttributes): void {
17
+ tracing.getActiveSpan()?.setAttributes(attributes)
18
+ }
19
+
20
+ /**
21
+ * Run `operation` in a child span.
22
+ * Use it when one invocation runs several units of work, such as each message in a queue batch.
23
+ * This helper does not record an exception. A failure reaches Issues as the uncaught error or `logger.error`.
24
+ * An expected throw, such as a duplicate message, is not an exception record.
25
+ */
26
+ export function withSpan<T>(
27
+ tracing: Tracing,
28
+ name: string,
29
+ attributes: SpanAttributes,
30
+ operation: () => Promise<T>,
31
+ ): Promise<T> {
32
+ return tracing.enterSpan(name, (span) => {
33
+ span.setAttributes(attributes)
34
+ return operation()
35
+ })
36
+ }
@@ -7,6 +7,7 @@ import type {
7
7
  EmailAttachment,
8
8
  IEmailService,
9
9
  OutboundEmail,
10
+ ReceivedContent,
10
11
  ReceivedEmail,
11
12
  } from '@/application'
12
13
  import { EmailServiceError, type EmailSendFailureReason } from '@/core'
@@ -62,6 +63,20 @@ function retryAfterSeconds(value: string | undefined): number | null {
62
63
  return Number.isSafeInteger(seconds) ? seconds : null
63
64
  }
64
65
 
66
+ function receivingError(
67
+ error: ErrorResponse,
68
+ headers: Record<string, string> | null,
69
+ ): EmailServiceError {
70
+ return new EmailServiceError({
71
+ reason: reasonFromResend(error),
72
+ provider: 'resend',
73
+ providerCode: error.name,
74
+ statusCode: error.statusCode,
75
+ retryAfter: retryAfterSeconds(headers?.['retry-after']),
76
+ cause: error,
77
+ })
78
+ }
79
+
65
80
  function toAttachment(attachment: EmailAttachment): Attachment {
66
81
  const result: Attachment = {
67
82
  filename: attachment.filename,
@@ -139,6 +154,30 @@ export class ResendEmailService implements IEmailService {
139
154
  return { headers: result.data.headers ?? {} }
140
155
  }
141
156
 
157
+ /**
158
+ * Retrieve the sender, plain text body, and attachments by the received email's provider ID.
159
+ * Each attachment carries a signed download link. Provider failures throw EmailServiceError.
160
+ */
161
+ async getReceivedContent(emailId: string): Promise<ReceivedContent> {
162
+ const client = await this.getClient()
163
+ const email = await client.emails.receiving.get(emailId)
164
+ if (email.error !== null) throw receivingError(email.error, email.headers)
165
+ // One page of 100, the API maximum, holds the attachments of any received email.
166
+ const files = await client.emails.receiving.attachments.list({ emailId, limit: 100 })
167
+ if (files.error !== null) throw receivingError(files.error, files.headers)
168
+ return {
169
+ from: email.data.from,
170
+ text: email.data.text,
171
+ attachments: files.data.data.map((file) => ({
172
+ filename: file.filename ?? null,
173
+ contentType: file.content_type,
174
+ size: file.size,
175
+ downloadUrl: file.download_url,
176
+ expiresAt: file.expires_at,
177
+ })),
178
+ }
179
+ }
180
+
142
181
  private async getClient(): Promise<Resend> {
143
182
  if (this.client === undefined) {
144
183
  const { Resend } = await import('resend')
@@ -27,6 +27,13 @@ export type AuthContext<Session extends AuthSession> =
27
27
  readonly session: undefined
28
28
  }
29
29
 
30
+ /** A resolved session and the request context that produced it. */
31
+ export type AuthenticatedContext<Session extends AuthSession> = {
32
+ readonly user: Session['user']
33
+ readonly session: Session['session']
34
+ readonly requestContext: RequestContext
35
+ }
36
+
30
37
  /** Select the current request's Better Auth instance from TanStack's registered context. */
31
38
  export type AuthMiddlewareOptions<Session extends AuthSession> = {
32
39
  readonly getAuth: (context: RequestContext) => {
@@ -34,6 +41,8 @@ export type AuthMiddlewareOptions<Session extends AuthSession> = {
34
41
  readonly getSession: (options: { headers: Headers }) => Promise<Session | null>
35
42
  }
36
43
  }
44
+ /** Runs after a session resolves and before the handler. A guest or a failed lookup does not call it. */
45
+ readonly onAuthenticated?: (context: AuthenticatedContext<Session>) => void
37
46
  }
38
47
 
39
48
  // The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily inside `.server()`.
@@ -45,10 +54,18 @@ const requestHeaders = async () => {
45
54
  /**
46
55
  * Preserve Porte's function and request middleware contracts with application-owned services.
47
56
  * Optional lookup failures log once and continue as guests. Required lookup failures propagate.
57
+ * `onAuthenticated` runs after a session resolves and before the rest of the chain.
48
58
  */
49
59
  export function createAuthMiddleware<Session extends AuthSession>(
50
60
  options: AuthMiddlewareOptions<Session>,
51
61
  ) {
62
+ function notifyAuthenticated(requestContext: RequestContext, resolved: Session): void {
63
+ options.onAuthenticated?.({
64
+ user: resolved.user,
65
+ session: resolved.session,
66
+ requestContext,
67
+ })
68
+ }
52
69
  const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(
53
70
  async ({ next, context }) => {
54
71
  let resolved: Session | null
@@ -61,6 +78,7 @@ export function createAuthMiddleware<Session extends AuthSession>(
61
78
  }
62
79
  if (resolved === null)
63
80
  return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })
81
+ notifyAuthenticated(context, resolved)
64
82
  return next<AuthContext<Session>>({
65
83
  context: { user: resolved.user, session: resolved.session },
66
84
  })
@@ -81,6 +99,7 @@ export function createAuthMiddleware<Session extends AuthSession>(
81
99
 
82
100
  const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {
83
101
  const resolved = await resolveRequiredSession(context, await requestHeaders())
102
+ notifyAuthenticated(context, resolved)
84
103
  return next<RequiredAuthContext<Session>>({
85
104
  context: { user: resolved.user, session: resolved.session },
86
105
  })
@@ -88,6 +107,7 @@ export function createAuthMiddleware<Session extends AuthSession>(
88
107
  const requireAuthRequest = createMiddleware({ type: 'request' }).server(
89
108
  async ({ request, next, context }) => {
90
109
  const resolved = await resolveRequiredSession(context, request.headers)
110
+ notifyAuthenticated(context, resolved)
91
111
  return next<RequiredAuthContext<Session>>({
92
112
  context: { user: resolved.user, session: resolved.session },
93
113
  })
@@ -2,6 +2,7 @@ export {
2
2
  createAuthMiddleware,
3
3
  type AuthSession,
4
4
  type AuthContext,
5
+ type AuthenticatedContext,
5
6
  type RequiredAuthContext,
6
7
  type AuthMiddlewareOptions,
7
8
  } from './auth.middleware.ts'
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-tNZHSCpJ.d.ts","names":[],"sources":["../src/application/handlers.ts","../src/application/analytics-service.ts","../src/application/metrics-service.ts","../src/application/observability-service.ts","../src/application/email-service.ts","../src/application/event-collector.ts","../src/application/message-store.ts","../src/application/outbox-repository.ts","../src/application/outbox-relay.ts","../src/application/transaction.ts","../src/application/unit-of-work.ts","../src/application/record-email-event.ts","../src/application/registry.ts","../src/application/message-bus.ts","../src/application/outbox-retention.ts","../src/application/message-already-processed.error.ts","../src/application/unit-of-work.errors.ts"],"mappings":"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"}