diff --git a/backend/proxy/.dev.vars.example b/backend/proxy/.dev.vars.example index 7eb36c4a..2a7d1a04 100644 --- a/backend/proxy/.dev.vars.example +++ b/backend/proxy/.dev.vars.example @@ -5,3 +5,4 @@ ALLOWED_ORIGIN="http://localhost:4200" GEMINI_MODEL="gemini-2.5-flash-lite" BETTER_AUTH_URL="http://localhost:8787" GOOGLE_CLIENT_ID="" +EMAIL_FROM="" \ No newline at end of file diff --git a/backend/proxy/package.json b/backend/proxy/package.json index 1f2eb0cc..37c9f1ea 100644 --- a/backend/proxy/package.json +++ b/backend/proxy/package.json @@ -51,6 +51,7 @@ "drizzle-orm": "^0.45.1", "hono": "^4.10.6", "hono-openapi": "^1.2.0", + "resend": "^6.9.2", "zod": "^4.3.5" } } diff --git a/backend/proxy/scripts/setup-secrets-store.ts b/backend/proxy/scripts/setup-secrets-store.ts new file mode 100644 index 00000000..a3fc2c24 --- /dev/null +++ b/backend/proxy/scripts/setup-secrets-store.ts @@ -0,0 +1,47 @@ +import { execSync } from 'node:child_process' + +const LOCAL_SECRET_STORE_ID = '4a9ca50edba84431879f34b0b67f9998' + +const LOCAL_SECRETS = [ + { + name: 'DANMAKU_GEMINI_API_KEY_STG', + scope: 'workers', + value: 'gemini-api', + }, + { + name: 'DA_AI_GATEWAY_ID_STG', + scope: 'workers', + value: 'da-ai-gateway-id', + }, + { + name: 'DA_AI_GATEWAY_NAME_STG', + scope: 'workers', + value: 'da-ai-gateway-name', + }, + { + name: 'BETTER_AUTH_SECRET_STG', + scope: 'workers', + value: 'better-auth-secret', + }, + { + name: 'GOOGLE_CLIENT_SECRET_STG', + scope: 'workers', + value: 'google-client-secret', + }, + { + name: 'RESEND_API_KEY_STG', + scope: 'workers', + value: 'resend-api-key', + }, +] + +async function setupSecretsStore() { + for (const secret of LOCAL_SECRETS) { + console.log(`Creating secret ${secret.name}...`) + execSync( + `pnpm wrangler secrets-store secret create ${LOCAL_SECRET_STORE_ID} --name ${secret.name} --scopes ${secret.scope} --value ${secret.value}` + ) + } +} + +void setupSecretsStore() diff --git a/backend/proxy/src/auth/config.ts b/backend/proxy/src/auth/config.ts index fbbe9086..8a35ef98 100644 --- a/backend/proxy/src/auth/config.ts +++ b/backend/proxy/src/auth/config.ts @@ -3,6 +3,7 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle' import { admin, bearer, jwt, openAPI } from 'better-auth/plugins' import { ac, adminRole, moderatorRole, userRole } from '@/auth/permissions' import { getOrCreateDb } from '@/db' +import { getOrCreateEmailService } from '@/email' async function getGoogleProvider(env: Env) { const clientId = env.GOOGLE_CLIENT_ID?.trim() @@ -19,6 +20,17 @@ async function getGoogleProvider(env: Env) { } } +// Updated per-request via setWaitUntil before auth handlers run +let currentWaitUntil: (promise: Promise) => void = () => { + // noop +} + +export function setWaitUntil( + waitUntil: (promise: Promise) => void +): void { + currentWaitUntil = waitUntil +} + async function createAuth(env: Env) { const secret = await env.BETTER_AUTH_SECRET.get() @@ -36,7 +48,31 @@ async function createAuth(env: Env) { basePath: '/auth', secret, trustedOrigins: [env.BETTER_AUTH_TRUSTED_ORIGINS], - emailAndPassword: { enabled: true, minPasswordLength: 6 }, + emailAndPassword: { + enabled: true, + minPasswordLength: 6, + sendResetPassword: async ({ user, url }) => { + const emailService = await getOrCreateEmailService(env) + const promise = emailService.send({ + to: user.email, + subject: 'Reset your password', + text: `Click the link to reset your password: ${url}`, + }) + currentWaitUntil(promise) + }, + }, + emailVerification: { + sendOnSignUp: true, + sendVerificationEmail: async ({ user, url }) => { + const emailService = await getOrCreateEmailService(env) + const promise = emailService.send({ + to: user.email, + subject: 'Verify your email address', + text: `Click the link to verify your email: ${url}`, + }) + currentWaitUntil(promise) + }, + }, socialProviders: googleProvider ? { google: googleProvider } : {}, database: drizzleAdapter(getOrCreateDb(env.DB), { provider: 'sqlite', diff --git a/backend/proxy/src/email/index.ts b/backend/proxy/src/email/index.ts new file mode 100644 index 00000000..863c04ef --- /dev/null +++ b/backend/proxy/src/email/index.ts @@ -0,0 +1,18 @@ +import { ResendEmailService } from './resend' +import type { EmailService } from './types' + +export type { EmailService, SendEmailParams } from './types' + +let emailService: EmailService | null = null + +export async function getOrCreateEmailService(env: Env): Promise { + if (emailService) { + return emailService + } + + const apiKey = await env.RESEND_API_KEY.get() + const from = env.EMAIL_FROM.trim() + + emailService = new ResendEmailService(apiKey, from) + return emailService +} diff --git a/backend/proxy/src/email/resend.ts b/backend/proxy/src/email/resend.ts new file mode 100644 index 00000000..88a76cf6 --- /dev/null +++ b/backend/proxy/src/email/resend.ts @@ -0,0 +1,43 @@ +import { Resend } from 'resend' +import type { EmailService, SendEmailParams } from './types' + +/** + * Resend implementation of the email service abstraction. + * Only this file should import from 'resend' so the rest of the app stays provider-agnostic. + */ +export class ResendEmailService implements EmailService { + private readonly resend: Resend + private readonly from: string + + constructor(apiKey: string, from: string) { + this.resend = new Resend(apiKey) + this.from = from + } + + async send(params: SendEmailParams): Promise { + const to = Array.isArray(params.to) ? params.to : [params.to] + const html = + params.html ?? + (params.text + ? `

${params.text.replaceAll('\n', '
')}

` + : '

') + const text = params.text + + const payload: Parameters[0] = { + from: this.from, + to, + subject: params.subject, + html, + } + if (text) payload.text = text + + console.log('Sending email to', to) + const { error, data } = await this.resend.emails.send(payload) + + if (error) { + throw new Error(`Resend send failed: ${JSON.stringify(error)}`) + } + + console.log('Email sent successfully', data) + } +} diff --git a/backend/proxy/src/email/types.ts b/backend/proxy/src/email/types.ts new file mode 100644 index 00000000..8d08fa1f --- /dev/null +++ b/backend/proxy/src/email/types.ts @@ -0,0 +1,19 @@ +/** + * Abstraction for sending transactional emails. + * Keeps the app decoupled from a specific provider (e.g. Resend). + */ + +export interface SendEmailParams { + /** Recipient address(es) */ + to: string | string[] + /** Email subject */ + subject: string + /** Plain text body (optional if html is set) */ + text?: string + /** HTML body (optional if text is set) */ + html?: string +} + +export interface EmailService { + send(params: SendEmailParams): Promise +} diff --git a/backend/proxy/src/middleware/authContext.ts b/backend/proxy/src/middleware/authContext.ts index 57a396af..5fa49728 100644 --- a/backend/proxy/src/middleware/authContext.ts +++ b/backend/proxy/src/middleware/authContext.ts @@ -1,4 +1,4 @@ -import { getOrCreateAuth } from '@/auth/config' +import { getOrCreateAuth, setWaitUntil } from '@/auth/config' import type { AuthSession } from '@/auth/types' import { factory } from '@/factory' import { getIsTestEnv } from '@/utils/getIsTestEnv' @@ -23,6 +23,7 @@ export const authContext = () => } try { + setWaitUntil(context.executionCtx.waitUntil) const auth = await getOrCreateAuth(context.env) const { user, session } = await resolveAuthSession( context.req.raw.headers, diff --git a/backend/proxy/src/routes/api/auth/router.ts b/backend/proxy/src/routes/api/auth/router.ts index 72459c2e..06b92946 100644 --- a/backend/proxy/src/routes/api/auth/router.ts +++ b/backend/proxy/src/routes/api/auth/router.ts @@ -1,15 +1,17 @@ -import { getOrCreateAuth } from '@/auth/config' +import { getOrCreateAuth, setWaitUntil } from '@/auth/config' import { factory } from '@/factory' export const authRouter = factory.createApp() authRouter.get('/docs', async (context) => { + setWaitUntil(context.executionCtx.waitUntil) const auth = await getOrCreateAuth(context.env) return context.json(await auth.api.generateOpenAPISchema(), { status: 200 }) }) authRouter.on(['GET', 'POST'], '/*', async (context) => { try { + setWaitUntil(context.executionCtx.waitUntil) const auth = await getOrCreateAuth(context.env) return auth.handler(context.req.raw) } catch (error) { diff --git a/backend/proxy/worker-configuration.d.ts b/backend/proxy/worker-configuration.d.ts index fbe1aff5..b2eb783e 100644 --- a/backend/proxy/worker-configuration.d.ts +++ b/backend/proxy/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 91211e0623a626a08ef3dc56a3388fb4) +// Generated by Wrangler by running `wrangler types` (hash: cc001bfc772b383e98e6060476f6ad78) // Runtime types generated with workerd@1.20260205.0 2025-05-05 nodejs_compat declare namespace Cloudflare { interface GlobalProps { @@ -13,13 +13,17 @@ declare namespace Cloudflare { DA_AI_GATEWAY_NAME: SecretsStoreSecret; BETTER_AUTH_SECRET: SecretsStoreSecret; GOOGLE_CLIENT_SECRET: SecretsStoreSecret; + RESEND_API_KEY: SecretsStoreSecret; CF_VERSION_METADATA: WorkerVersionMetadata; - ENVIRONMENT: "staging"; - ALLOWED_ORIGIN: "https://danmaku.weeblify.app,https://danmaku-staging.weeblify.app,http://localhost:4200"; BETTER_AUTH_TRUSTED_ORIGINS: "https://**.weeblify.app"; - GEMINI_MODEL: "gemini-2.5-flash-lite"; - BETTER_AUTH_URL: "https://api.danmaku-staging.weeblify.app"; - GOOGLE_CLIENT_ID: ""; + EMAIL_FROM: "Danmaku Anywhere "; + ENVIRONMENT: string; + DANDANPLAY_API_HOST: string; + DANDANPLAY_APP_ID: string; + ALLOWED_ORIGIN: string; + GEMINI_MODEL: string; + BETTER_AUTH_URL: string; + GOOGLE_CLIENT_ID: string; DDP_SERVICE: Fetcher /* ddp-microservice-staging */; } interface ProductionEnv { @@ -30,16 +34,27 @@ declare namespace Cloudflare { DA_AI_GATEWAY_NAME: SecretsStoreSecret; BETTER_AUTH_SECRET: SecretsStoreSecret; GOOGLE_CLIENT_SECRET: SecretsStoreSecret; + RESEND_API_KEY: SecretsStoreSecret; CF_VERSION_METADATA: WorkerVersionMetadata; - ENVIRONMENT: "production"; - ALLOWED_ORIGIN: "https://danmaku.weeblify.app"; BETTER_AUTH_TRUSTED_ORIGINS: "https://**.weeblify.app"; - GEMINI_MODEL: "gemini-2.5-flash-lite"; - BETTER_AUTH_URL: "https://api.danmaku.weeblify.app"; - GOOGLE_CLIENT_ID: ""; + EMAIL_FROM: "Danmaku Anywhere "; + ENVIRONMENT: string; + DANDANPLAY_API_HOST: string; + DANDANPLAY_APP_ID: string; + ALLOWED_ORIGIN: string; + GEMINI_MODEL: string; + BETTER_AUTH_URL: string; + GOOGLE_CLIENT_ID: string; DDP_SERVICE: Fetcher /* ddp-microservice-prod */; } interface Env { + ENVIRONMENT: string; + DANDANPLAY_API_HOST: string; + DANDANPLAY_APP_ID: string; + ALLOWED_ORIGIN: string; + GEMINI_MODEL: string; + BETTER_AUTH_URL: string; + GOOGLE_CLIENT_ID: string; FILES_BUCKET: R2Bucket; DB: D1Database; DANMAKU_GEMINI_API_KEY: SecretsStoreSecret; @@ -47,13 +62,10 @@ declare namespace Cloudflare { DA_AI_GATEWAY_NAME: SecretsStoreSecret; BETTER_AUTH_SECRET: SecretsStoreSecret; GOOGLE_CLIENT_SECRET: SecretsStoreSecret; + RESEND_API_KEY: SecretsStoreSecret; CF_VERSION_METADATA: WorkerVersionMetadata; - ENVIRONMENT: "staging" | "production"; - ALLOWED_ORIGIN: "https://danmaku.weeblify.app,https://danmaku-staging.weeblify.app,http://localhost:4200" | "https://danmaku.weeblify.app"; BETTER_AUTH_TRUSTED_ORIGINS: "https://**.weeblify.app"; - GEMINI_MODEL: "gemini-2.5-flash-lite"; - BETTER_AUTH_URL: "https://api.danmaku-staging.weeblify.app" | "https://api.danmaku.weeblify.app"; - GOOGLE_CLIENT_ID: ""; + EMAIL_FROM: "Danmaku Anywhere "; DDP_SERVICE: Fetcher /* ddp-microservice-staging */ | Fetcher /* ddp-microservice-prod */; } } @@ -62,7 +74,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types diff --git a/backend/proxy/wrangler.json b/backend/proxy/wrangler.json index e11e1c59..bc76919e 100644 --- a/backend/proxy/wrangler.json +++ b/backend/proxy/wrangler.json @@ -12,7 +12,8 @@ "BETTER_AUTH_TRUSTED_ORIGINS": "https://**.weeblify.app", "GEMINI_MODEL": "gemini-2.5-flash-lite", "BETTER_AUTH_URL": "https://api.danmaku-staging.weeblify.app", - "GOOGLE_CLIENT_ID": "" + "GOOGLE_CLIENT_ID": "", + "EMAIL_FROM": "Danmaku Anywhere " }, "version_metadata": { "binding": "CF_VERSION_METADATA" @@ -42,6 +43,11 @@ "binding": "GOOGLE_CLIENT_SECRET", "store_id": "4a9ca50edba84431879f34b0b67f9998", "secret_name": "GOOGLE_CLIENT_SECRET_STG" + }, + { + "binding": "RESEND_API_KEY", + "store_id": "4a9ca50edba84431879f34b0b67f9998", + "secret_name": "RESEND_API_KEY_STG" } ], "routes": [ @@ -79,7 +85,8 @@ "BETTER_AUTH_TRUSTED_ORIGINS": "https://**.weeblify.app", "GEMINI_MODEL": "gemini-2.5-flash-lite", "BETTER_AUTH_URL": "https://api.danmaku-staging.weeblify.app", - "GOOGLE_CLIENT_ID": "" + "GOOGLE_CLIENT_ID": "", + "EMAIL_FROM": "Danmaku Anywhere " }, "version_metadata": { "binding": "CF_VERSION_METADATA" @@ -109,6 +116,11 @@ "binding": "GOOGLE_CLIENT_SECRET", "store_id": "4a9ca50edba84431879f34b0b67f9998", "secret_name": "GOOGLE_CLIENT_SECRET_STG" + }, + { + "binding": "RESEND_API_KEY", + "store_id": "4a9ca50edba84431879f34b0b67f9998", + "secret_name": "RESEND_API_KEY_STG" } ], "routes": [ @@ -120,7 +132,8 @@ "r2_buckets": [ { "binding": "FILES_BUCKET", - "bucket_name": "danmaku-anywhere-file-staging" + "bucket_name": "danmaku-anywhere-file-staging", + "preview_bucket_name": "danmaku-anywhere-file-staging-preview" } ], "d1_databases": [ @@ -146,7 +159,8 @@ "BETTER_AUTH_TRUSTED_ORIGINS": "https://**.weeblify.app", "GEMINI_MODEL": "gemini-2.5-flash-lite", "BETTER_AUTH_URL": "https://api.danmaku.weeblify.app", - "GOOGLE_CLIENT_ID": "" + "GOOGLE_CLIENT_ID": "", + "EMAIL_FROM": "Danmaku Anywhere " }, "version_metadata": { "binding": "CF_VERSION_METADATA" @@ -176,6 +190,11 @@ "binding": "GOOGLE_CLIENT_SECRET", "store_id": "4a9ca50edba84431879f34b0b67f9998", "secret_name": "GOOGLE_CLIENT_SECRET" + }, + { + "binding": "RESEND_API_KEY", + "store_id": "4a9ca50edba84431879f34b0b67f9998", + "secret_name": "RESEND_API_KEY" } ], "routes": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f671879..5dc6ca49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,6 +203,9 @@ importers: hono-openapi: specifier: ^1.2.0 version: 1.3.0(@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.8))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.8)(openapi-types@12.1.3) + resend: + specifier: ^6.9.2 + version: 6.9.3 zod: specifier: ^4.3.5 version: 4.3.6 @@ -3708,6 +3711,9 @@ packages: '@speed-highlight/core@1.2.14': resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-community/standard-json@0.3.5': resolution: {integrity: sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==} peerDependencies: @@ -5636,6 +5642,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -7180,6 +7189,9 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postal-mime@2.7.3: + resolution: {integrity: sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==} + postcss-media-query-parser@0.2.3: resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} @@ -7461,6 +7473,15 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resend@6.9.3: + resolution: {integrity: sha512-GRXjH9XZBJA+daH7bBVDuTShr22iWCxXA8P7t495G4dM/RC+d+3gHBK/6bz9K6Vpcq11zRQKmD+B+jECwQlyGQ==} + engines: {node: '>=20'} + peerDependencies: + '@react-email/render': '*' + peerDependenciesMeta: + '@react-email/render': + optional: true + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -7746,6 +7767,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -7867,6 +7891,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svix@1.84.1: + resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -8141,6 +8168,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -11294,6 +11325,8 @@ snapshots: '@speed-highlight/core@1.2.14': {} + '@stablelib/base64@1.0.1': {} + '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6)': dependencies: '@standard-schema/spec': 1.1.0 @@ -13356,6 +13389,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -15159,6 +15194,8 @@ snapshots: pluralize@8.0.0: {} + postal-mime@2.7.3: {} + postcss-media-query-parser@0.2.3: {} postcss-safe-parser@7.0.1(postcss@8.5.8): @@ -15478,6 +15515,11 @@ snapshots: reselect@5.1.1: {} + resend@6.9.3: + dependencies: + postal-mime: 2.7.3 + svix: 1.84.1 + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -15877,6 +15919,11 @@ snapshots: stackback@0.0.2: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@1.5.0: optional: true @@ -15991,6 +16038,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svix@1.84.1: + dependencies: + standardwebhooks: 1.0.0 + uuid: 10.0.0 + symbol-tree@3.2.4: {} tagged-tag@1.0.0: {} @@ -16242,6 +16294,8 @@ snapshots: utils-merge@1.0.1: optional: true + uuid@10.0.0: {} + uuid@8.3.2: {} validate-npm-package-name@7.0.2: {}