From 08827bd4b5b60ffe12f8e1a0361e33ca8ce737cf Mon Sep 17 00:00:00 2001 From: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:38:19 +0200 Subject: [PATCH 1/2] feat(api-service): monthly usage digest email fixes NV-6933 (#10042) --- .agents/skills/email-best-practices/SKILL.md | 59 + .../resources/branding.md | 5 + .../resources/compliance.md | 107 ++ .../resources/deliverability.md | 117 ++ .../resources/email-capture.md | 128 ++ .../resources/email-types.md | 173 +++ .../resources/list-management.md | 157 +++ .../resources/marketing-emails.md | 115 ++ .../resources/sending-reliability.md | 155 +++ .../resources/transactional-email-catalog.md | 418 ++++++ .../resources/transactional-emails.md | 92 ++ .../resources/webhooks-events.md | 167 +++ .agents/skills/react-email/SKILL.md | 518 ++++++++ .agents/skills/react-email/TESTS.md | 878 +++++++++++++ .../react-email/references/COMPONENTS.md | 429 ++++++ .agents/skills/react-email/references/I18N.md | 657 ++++++++++ .../skills/react-email/references/PATTERNS.md | 713 ++++++++++ .../skills/react-email/references/SENDING.md | 115 ++ .../skills/react-email/references/STYLING.md | 302 +++++ .cspell.json | 4 +- .source | 2 +- apps/api/src/app.module.ts | 4 +- .../rate-limiting/guards/throttler.guard.ts | 17 +- .../public/images/report-emails/bell.svg | 3 + .../public/images/report-emails/calendar.svg | 3 + .../public/images/report-emails/chat.svg | 3 + .../public/images/report-emails/email.svg | 3 + .../images/report-emails/linkedin-dot.svg | 3 + .../public/images/report-emails/push.svg | 5 + .../public/images/report-emails/sms.svg | 3 + .../images/report-emails/trend-down.svg | 3 + .../public/images/report-emails/trend-up.svg | 3 + .../images/report-emails/winding-arrow.svg | 3 + .../public/images/report-emails/x-dot.svg | 4 + .../images/report-emails/youtube-dot.svg | 4 + enterprise/packages/billing/package.json | 1 + .../src/modules/cron.module.ts | 6 +- .../src/services/analytic-logs/index.ts | 1 + .../trace-log/trace-log.repository.ts | 149 +++ .../trace-log/trace-log.schema.ts | 9 +- .../workflow-run-count.repository.ts | 182 ++- .../workflow-run-count.schema.ts | 28 +- .../workflow-run/workflow-run.repository.ts | 59 - .../execute-bridge-request.usecase.ts | 19 +- .../repositories/member/member.repository.ts | 2 +- .../organization/organization.repository.ts | 2 +- libs/notifications/package.json | 1 + libs/notifications/src/index.ts | 2 + .../src/workflows/usage-report/email.tsx | 1159 +++++++++++++++++ .../src/workflows/usage-report/schemas.ts | 45 + .../usage-report/usage-report.workflow.ts | 37 + .../framework/src/constants/cron.constants.ts | 1 + packages/shared/src/config/job-queue.ts | 1 + packages/shared/src/types/cron.ts | 2 + packages/shared/src/types/feature-flags.ts | 4 + pnpm-lock.yaml | 75 +- 56 files changed, 6994 insertions(+), 163 deletions(-) create mode 100644 .agents/skills/email-best-practices/SKILL.md create mode 100644 .agents/skills/email-best-practices/resources/branding.md create mode 100644 .agents/skills/email-best-practices/resources/compliance.md create mode 100644 .agents/skills/email-best-practices/resources/deliverability.md create mode 100644 .agents/skills/email-best-practices/resources/email-capture.md create mode 100644 .agents/skills/email-best-practices/resources/email-types.md create mode 100644 .agents/skills/email-best-practices/resources/list-management.md create mode 100644 .agents/skills/email-best-practices/resources/marketing-emails.md create mode 100644 .agents/skills/email-best-practices/resources/sending-reliability.md create mode 100644 .agents/skills/email-best-practices/resources/transactional-email-catalog.md create mode 100644 .agents/skills/email-best-practices/resources/transactional-emails.md create mode 100644 .agents/skills/email-best-practices/resources/webhooks-events.md create mode 100644 .agents/skills/react-email/SKILL.md create mode 100644 .agents/skills/react-email/TESTS.md create mode 100644 .agents/skills/react-email/references/COMPONENTS.md create mode 100644 .agents/skills/react-email/references/I18N.md create mode 100644 .agents/skills/react-email/references/PATTERNS.md create mode 100644 .agents/skills/react-email/references/SENDING.md create mode 100644 .agents/skills/react-email/references/STYLING.md create mode 100644 apps/dashboard/public/images/report-emails/bell.svg create mode 100644 apps/dashboard/public/images/report-emails/calendar.svg create mode 100644 apps/dashboard/public/images/report-emails/chat.svg create mode 100644 apps/dashboard/public/images/report-emails/email.svg create mode 100644 apps/dashboard/public/images/report-emails/linkedin-dot.svg create mode 100644 apps/dashboard/public/images/report-emails/push.svg create mode 100644 apps/dashboard/public/images/report-emails/sms.svg create mode 100644 apps/dashboard/public/images/report-emails/trend-down.svg create mode 100644 apps/dashboard/public/images/report-emails/trend-up.svg create mode 100644 apps/dashboard/public/images/report-emails/winding-arrow.svg create mode 100644 apps/dashboard/public/images/report-emails/x-dot.svg create mode 100644 apps/dashboard/public/images/report-emails/youtube-dot.svg create mode 100644 libs/notifications/src/workflows/usage-report/email.tsx create mode 100644 libs/notifications/src/workflows/usage-report/schemas.ts create mode 100644 libs/notifications/src/workflows/usage-report/usage-report.workflow.ts diff --git a/.agents/skills/email-best-practices/SKILL.md b/.agents/skills/email-best-practices/SKILL.md new file mode 100644 index 00000000000..4314e9a0946 --- /dev/null +++ b/.agents/skills/email-best-practices/SKILL.md @@ -0,0 +1,59 @@ +--- +name: email-best-practices +description: Use when building email features, emails going to spam, high bounce rates, setting up SPF/DKIM/DMARC authentication, implementing email capture, ensuring compliance (CAN-SPAM, GDPR, CASL), handling webhooks, retry logic, or deciding transactional vs marketing. +--- + +# Email Best Practices + +Guidance for building deliverable, compliant, user-friendly emails. + +## Architecture Overview + +``` +[User] → [Email Form] → [Validation] → [Double Opt-In] + ↓ + [Consent Recorded] + ↓ +[Suppression Check] ←──────────────[Ready to Send] + ↓ +[Idempotent Send + Retry] ──────→ [Email API] + ↓ + [Webhook Events] + ↓ + ┌────────┬────────┬─────────────┐ + ↓ ↓ ↓ ↓ + Delivered Bounced Complained Opened/Clicked + ↓ ↓ + [Suppression List Updated] + ↓ + [List Hygiene Jobs] +``` + +## Quick Reference + +| Need to... | See | +|------------|-----| +| Set up SPF/DKIM/DMARC, fix spam issues | [Deliverability](./resources/deliverability.md) | +| Build password reset, OTP, confirmations | [Transactional Emails](./resources/transactional-emails.md) | +| Plan which emails your app needs | [Transactional Email Catalog](./resources/transactional-email-catalog.md) | +| Build newsletter signup, validate emails | [Email Capture](./resources/email-capture.md) | +| Send newsletters, promotions | [Marketing Emails](./resources/marketing-emails.md) | +| Ensure CAN-SPAM/GDPR/CASL compliance | [Compliance](./resources/compliance.md) | +| Decide transactional vs marketing | [Email Types](./resources/email-types.md) | +| Handle retries, idempotency, errors | [Sending Reliability](./resources/sending-reliability.md) | +| Process delivery events, set up webhooks | [Webhooks & Events](./resources/webhooks-events.md) | +| Manage bounces, complaints, suppression | [List Management](./resources/list-management.md) | + +## Start Here + +**New app?** +Start with the [Catalog](./resources/transactional-email-catalog.md) to plan which emails your app needs (password reset, verification, etc.), then set up [Deliverability](./resources/deliverability.md) (DNS authentication) before sending your first email. + +**Spam issues?** +Check [Deliverability](./resources/deliverability.md) first—authentication problems are the most common cause. Gmail/Yahoo reject unauthenticated emails. + +**Marketing emails?** +Follow this path: [Email Capture](./resources/email-capture.md) (collect consent) → [Compliance](./resources/compliance.md) (legal requirements) → [Marketing Emails](./resources/marketing-emails.md) (best practices). + +**Production-ready sending?** +Add reliability: [Sending Reliability](./resources/sending-reliability.md) (retry + idempotency) → [Webhooks & Events](./resources/webhooks-events.md) (track delivery) → [List Management](./resources/list-management.md) (handle bounces). diff --git a/.agents/skills/email-best-practices/resources/branding.md b/.agents/skills/email-best-practices/resources/branding.md new file mode 100644 index 00000000000..359057489d9 --- /dev/null +++ b/.agents/skills/email-best-practices/resources/branding.md @@ -0,0 +1,5 @@ +# Email Branding + +## BIMI (Optional) + +Display brand logo in email clients. Requires DMARC `p=quarantine` or `p=reject`. \ No newline at end of file diff --git a/.agents/skills/email-best-practices/resources/compliance.md b/.agents/skills/email-best-practices/resources/compliance.md new file mode 100644 index 00000000000..48527b05cc7 --- /dev/null +++ b/.agents/skills/email-best-practices/resources/compliance.md @@ -0,0 +1,107 @@ +# Email Compliance + +Legal requirements for email by jurisdiction. **Not legal advice—consult an attorney for your specific situation.** + +## Quick Reference + +| Law | Region | Key Requirement | Penalty | +|-----|--------|-----------------|---------| +| CAN-SPAM | US | Opt-out mechanism, physical address | $53k/email | +| GDPR | EU | Explicit opt-in consent | €20M or 4% revenue | +| CASL | Canada | Express consent, opt-out mechanism | $1M (individual) to $10M (organization) CAD | + +## CAN-SPAM (United States) + +**Requirements:** +- Accurate header info (From, To, Reply-To) +- Non-deceptive subject lines +- Physical mailing address in every email +- Clear opt-out mechanism +- Honor opt-out within 10 business days + +**Transactional emails:** Can send without opt-in if related to a transaction and not promotional. + +## GDPR (European Union) + +**Requirements:** +- Explicit opt-in consent (not pre-checked boxes) +- Consent must be freely given, specific, informed +- Easy to withdraw consent (as easy as giving it) +- Right to access data and deletion ("right to be forgotten") +- Process unsubscribe immediately + +**Consent records:** Document who, when, how, and what they consented to. + +**Transactional emails:** Can send based on contract fulfillment or legitimate interest. + +## CASL (Canada) + +**Consent types:** +- **Express consent:** Explicit opt-in (ideal) +- **Implied consent:** Existing business relationship (2 years) or inquiry (6 months) + +**Requirements:** +- Clear sender identification that will be valid for 60 days after send +- Unsubscribe functional for 60 days after send +- Process unsubscribe no later than 10 business days +- Keep consent records 3 years after expiration + +## Other Regions + +| Region | Law | Key Points | +|--------|-----|------------| +| Australia | Spam Act 2003 | Consent required, honor unsubscribe within 5 days | +| UK | PECR + GDPR | Same as GDPR | +| Brazil | LGPD | Similar to GDPR, explicit consent for marketing | + +## Unsubscribe Requirements Summary + +| Law | Timing | Notes | +|-----|--------|-------| +| CAN-SPAM | 10 business days | Must work 30 days after send | +| GDPR | Immediately | Must be as easy as opting in | +| CASL | 10 business days | Must work 60 days after send | + +**Universal best practices:** Prominent link, one-click when possible, no login required, free, confirm action. + +## Managing preferences vs Unsubscribe from all + +Most legistlations require a one-click unsubscribe. `Managing preferences` is a nice-to-have and can lead to lower unsubscribe rate but doesn't replace `Unsubscribe`. If possible, offer both. + +## Consent Management + +**Record:** +- Email address +- Date/time of consent +- Method (form, checkbox) +- What they consented to +- Source (which page/form) + +**Storage:** Database with timestamps, audit trail of changes, link to user account. + +## Data Retention + +| Law | Requirement | +|-----|-------------| +| GDPR | Keep only as long as necessary, delete when no longer needed | +| CASL | Keep consent records 3 years after expiration | + +**Best practice:** Have clear retention policy, honor deletion requests promptly, review and clean regularly. + +## Privacy Policy Must Include + +- What data you collect +- How you use data +- Who you share data with +- User rights (access, deletion) +- How to contact about privacy + +## International Sending + +**Best practice:** Follow the most restrictive requirements (usually GDPR) to ensure compliance across all regions. + +## Related + +- [Email Capture](./email-capture.md) - Implement consent forms and double opt-in +- [Marketing Emails](./marketing-emails.md) - Consent and unsubscribe requirements +- [List Management](./list-management.md) - Handle unsubscribes and deletion requests diff --git a/.agents/skills/email-best-practices/resources/deliverability.md b/.agents/skills/email-best-practices/resources/deliverability.md new file mode 100644 index 00000000000..8853443ecee --- /dev/null +++ b/.agents/skills/email-best-practices/resources/deliverability.md @@ -0,0 +1,117 @@ +# Email Deliverability + +Maximizing the chances that your emails are delivered successfully to the recipients. + +## Email Authentication + +**Required by Gmail/Yahoo/Microsoft** - unauthenticated emails will be rejected or spam-filtered. + +### SPF (Sender Policy Framework) + +Specifies which servers can send email for your domain. + +``` +v=spf1 include:amazonses.com ~all +``` + +- Add TXT record to DNS +- Use `~all` (soft fail) + +### DKIM (DomainKeys Identified Mail) + +Cryptographic signature proving email authenticity. + +- Your email service will provide you with a TXT record + +### DMARC + +Policy for handling SPF/DKIM failures + reporting. + +``` +v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com +``` + +**Rollout:** `p=none` (monitor) → `p=quarantine; pct=25` → `p=reject` + +Learn more: https://resend.com/blog/dmarc-policy-modes + +### Verify Your Setup + +Check DNS records directly: + +```bash +# SPF record +dig TXT yourdomain.com +short + +# DKIM record (replace 'resend' with your selector) +dig TXT resend._domainkey.yourdomain.com +short + +# DMARC record +dig TXT _dmarc.yourdomain.com +short +``` + +**Expected output:** Each command should return your configured record. No output = record missing. + +## Sender Reputation + +### IP Warming + +New IP/domain? Gradually increase volume: + +| Week | Daily Volume | +|------|-------------| +| 1 | 50-100 | +| 2 | 200-500 | +| 3 | 1,000-2,000 | +| 4 | 5,000-10,000 | + +Start with engaged users. Send consistently. Don't rush. + +Learn more: https://resend.com/docs/knowledge-base/warming-up + +### Maintaining Reputation + +**Do:** Send to engaged users, keep bounce <4%, complaints <0.1%, remove inactive subscribers. + +**Don't:** Send to purchased lists, ignore bounces/complaints, send inconsistent volumes + +## Bounce Handling + +| Type | Cause | Action | +|------|-------|--------| +| Hard bounce | Permanent failure to deliver | Remove immediately | +| Soft bounce | Transient failure to deliver | Retry: 1h → 4h → 24h, remove after 3-5 failures | + +**Targets:** <1% good, 1-3% acceptable, 3-4% concerning, >4% critical + +## Complaint Handling + +**Targets:** <0.01% excellent, 0.01-0.05% good, >0.05% critical + +**Reduce complaints:** +- Only send to opted-in users +- Make unsubscribe easy and immediate +- Use clear sender names and "From" addresses + +**Feedback loops:** Set up with Gmail (Postmaster Tools), Yahoo, Microsoft SNDS. Remove complainers immediately. + +## Infrastructure + +**Dedicated sending domain:** Use different subdomains for different sending purposes (e.g., `t.yourdomain.com` for transactional emails and `m.yourdomain.com` for marketing emails). + +**DNS TTL:** Low (300s) during setup, high (3600s+) after stable. + +## Troubleshooting + +**Emails going to spam?** Check in order: +1. Authentication (SPF, DKIM, DMARC) +2. Sender reputation (blacklists, complaint rates) +3. Content +4. Sending patterns (sudden volume spikes) + +**Diagnostic tools:** [Google Postmaster Tools](https://postmaster.google.com) + +## Related + +- [List Management](./list-management.md) - Handle bounces and complaints to protect reputation +- [Sending Reliability](./sending-reliability.md) - Retry logic and error handling diff --git a/.agents/skills/email-best-practices/resources/email-capture.md b/.agents/skills/email-best-practices/resources/email-capture.md new file mode 100644 index 00000000000..efdbc3cb473 --- /dev/null +++ b/.agents/skills/email-best-practices/resources/email-capture.md @@ -0,0 +1,128 @@ +# Email Capture Best Practices + +Collecting email addresses responsibly with validation, verification, and proper consent. + +## Email Validation + +### Client-Side + +**HTML5:** +```html + +``` + +**Best practices:** +- Validate on blur or with short debounce +- Show clear error messages +- Don't be too strict (allow unusual but valid formats) +- Client-side validation ≠ deliverability + +### Server-Side (Recommended) + +Always validate server-side—client-side can be bypassed. + +**Check:** +- Email format (RFC 5322) +- Domain exists (DNS lookup) +- Domain has MX records +- Optionally: disposable email detection + +Recommended tools: https://resend.com/blog/best-email-verification-apis + +## Double opt-in + +Confirms address belongs to user and is deliverable. + +### Process + +1. User submits email +2. Send verification email with unique link/token +3. User clicks link +4. Mark as verified +5. Allow access/add to list + +**Timing:** Send immediately, include expiration (24-48 hours), allow resend after 60 seconds, limit resend attempts (3/hour). + +### Single vs Double Opt-In + +| | Single Opt-In | Double Opt-In | +|--|---------------|---------------| +| **Process** | Add to list immediately | Require email confirmation first | +| **Pros** | Lower friction, faster growth | Verified addresses, better engagement, meets GDPR/CASL | +| **Cons** | Higher invalid rate, lower engagement | Some users don't confirm | +| **Use for** | Account creation, transactional | Marketing lists, newsletters | + +**Recommendation:** Double opt-in for all marketing emails. + +## Form Design + +### Email Input + +- Use `type="email"` for mobile keyboard +- Include placeholder ("you@example.com") +- Clear error messages ("Please enter a valid email address" not "Invalid") + +### Consent Checkboxes (Marketing) + +- **Unchecked by default** (required) +- Specific language about what they're signing up for +- Separate checkboxes for different email types +- Link to privacy policy + +``` +☐ Subscribe to our weekly newsletter with product updates +☐ Send me promotional offers and deals +``` + +**Don't:** Pre-check boxes, use vague language, hide in terms. + +### Form Layout + +- Keep simple and focused +- One primary action +- Clear value proposition +- Mobile-friendly +- Accessible (labels, ARIA) + +## Error Handling + +### Invalid Email + +- Show clear error message +- Suggest corrections for common typos (@gmial.com → @gmail.com) +- Allow user to fix and resubmit + +### Already Registered + +- Accounts: "This email is already registered. [Sign in]" +- Marketing: "You're already subscribed! [Manage preferences]" +- Don't reveal if account exists (security) + +### Rate Limiting + +- Limit verification emails (3/hour per email) +- Rate limit form submissions +- Use CAPTCHA sparingly if needed +- Monitor for abuse patterns + +## Verification Emails + +**Content:** +- Clear purpose ("Verify your email address") +- Prominent verification button +- Expiration time +- Resend option +- "I didn't request this" notice + +**Design:** +- Mobile-friendly +- Large, tappable button +- Clear call-to-action + +See [Transactional Emails](./transactional-emails.md) for detailed email design guidance. + +## Related + +- [Compliance](./compliance.md) - Legal requirements for consent (GDPR, CASL) +- [Marketing Emails](./marketing-emails.md) - What happens after capture +- [Deliverability](./deliverability.md) - How validation improves sender reputation diff --git a/.agents/skills/email-best-practices/resources/email-types.md b/.agents/skills/email-best-practices/resources/email-types.md new file mode 100644 index 00000000000..b47356f449d --- /dev/null +++ b/.agents/skills/email-best-practices/resources/email-types.md @@ -0,0 +1,173 @@ +# Email Types: Transactional vs Marketing + +Understanding the difference between transactional and marketing emails is crucial for compliance, deliverability, and user experience. This guide explains the distinctions and provides a catalog of transactional emails your app should include. + +## When to Use This + +- Deciding whether an email should be transactional or marketing +- Understanding legal distinctions between email types +- Planning what transactional emails your app needs +- Ensuring compliance with email regulations +- Setting up separate sending infrastructure + +## Transactional vs Marketing: Key Differences + +### Transactional Emails + +**Definition:** Emails that facilitate or confirm a transaction the user initiated or expects. They're directly related to an action the user took or are legal notices you're required to serve. + +**Characteristics:** +- User-initiated or expected +- Time-sensitive and actionable +- Required for the user to complete an action +- Does not include promotional material or offers +- Can be sent without explicit opt-in (with limitations) + +**Examples:** +- Password reset links +- Order confirmations +- Account verification +- OTP/2FA codes +- Shipping notifications + +**Analogy:** +Think of transactional emails for everything that would leave you with a paper receipt in the real world: invoices, parking ticket, booking confirmation, etc. + +### Marketing Emails + +**Definition:** Emails sent for promotional, advertising, or informational purposes that are not directly related to a specific transaction or legal requirement. + +**Characteristics:** +- Promotional or informational content +- Not time-sensitive to complete a transaction +- Require explicit opt-in (consent) +- Must include unsubscribe options +- Subject to stricter compliance requirements + +**Examples:** +- Newsletters +- Abandoned cart +- Product announcements +- Promotional offers +- Company updates +- Educational content + +## Legal Distinctions + +### CAN-SPAM Act (US) + +**Transactional emails:** +- Can be sent without opt-in +- Must be related to a transaction +- Cannot contain promotional content (with exceptions) +- Must identify sender and provide contact information + +**Marketing emails:** +- Require opt-out mechanism (not opt-in in US) +- Must include clear sender identification +- Must include physical mailing address +- Must honor opt-out requests within 10 business days + +### GDPR (EU) + +**Transactional emails:** +- Can be sent based on legitimate interest or contract fulfillment +- Must be necessary for service delivery +- Cannot contain marketing content without consent + +**Marketing emails:** +- Require explicit opt-in consent +- Must clearly state purpose of data collection +- Must provide easy unsubscribe +- Subject to data protection requirements + +### CASL (Canada) + +**Transactional emails:** +- Can be sent without consent if related to ongoing business relationship +- Must be factual and not promotional + +**Marketing emails:** +- Require express or implied consent +- Must include unsubscribe mechanism +- Must identify sender clearly + +## When to Use Each Type + +### Use Transactional When: + +- User needs the email to complete an action +- Email confirms a transaction or account change +- Email provides security-related information +- Email is expected based on user action +- Content is time-sensitive and actionable +- You're required to serve a notification for compliance + +### Use Marketing When: + +- Promoting products or services +- Sending newsletters or updates +- Sharing educational content +- Announcing features or company news +- Content is not required for a transaction + +## Hybrid Emails: The Gray Area + +Some emails mix transactional and marketing content. This isn't best practice and should be avoided. + +**Best practice:** Keep transactional and marketing separate. + +**Example of problematic hybrid:** +- Newsletter (marketing) with a small order status update (transactional) + +## Transactional Email Catalog + +For a complete catalog of transactional emails and recommended combinations by app type, see [Transactional Email Catalog](./transactional-email-catalog.md). + +**Quick reference - Essential emails for most apps:** +1. **Email verification** - Required for account creation +2. **Password reset** - Required for account recovery +3. **Welcome email** - Good user experience + +The catalog includes detailed guidance for: +- Authentication-focused apps +- Newsletter / content platforms +- E-commerce / marketplaces +- SaaS / subscription services +- Financial / fintech apps +- Social / community platforms +- Developer tools / API platforms +- Healthcare / HIPAA-compliant apps + +## Sending Infrastructure + +### Separate subdomains + +**Best practice:** Use separate sending subdomains for transactional and marketing emails. + +**Benefits:** +- Protect transactional deliverability +- Different authentication domains +- Independent reputation +- Easier compliance management + +**Implementation:** +- Use different subdomains (e.g., `t.yourdomain.com` for transactional, `m.yourdomain.com` for marketing) + +### Email Service Considerations + +Choose an email service that: +- Provides reliable delivery for transactional emails +- Offers separate sending domains +- Has good API for programmatic sending +- Provides webhooks for delivery events +- Supports authentication setup (SPF, DKIM, DMARC) + +Services like Resend are designed for transactional emails and provide the infrastructure and tools needed for reliable delivery. They also offer powerful marketing features. + +## Related Topics + +- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails +- [Marketing Emails](./marketing-emails.md) - Best practices for marketing emails +- [Compliance](./compliance.md) - Legal requirements for each email type +- [Deliverability](./deliverability.md) - Ensuring transactional emails are delivered diff --git a/.agents/skills/email-best-practices/resources/list-management.md b/.agents/skills/email-best-practices/resources/list-management.md new file mode 100644 index 00000000000..2581790d67e --- /dev/null +++ b/.agents/skills/email-best-practices/resources/list-management.md @@ -0,0 +1,157 @@ +# List Management + +Maintaining clean email lists through suppression, hygiene, and data retention. + +## Suppression Lists + +A suppression list prevents sending to addresses that should never receive email. + +### What to Suppress + +| Reason | Action | Can Unsuppress? | +|--------|--------|-----------------| +| Hard bounce | Add immediately | No (address invalid) | +| Complaint (spam) | Add immediately | No (legal requirement) | +| Soft bounce (3x) | Add after threshold | Yes, after 30-90 days | +| Manual removal | Add on request | Only if user requests | + +### Implementation + +```typescript +// Suppression list schema +interface SuppressionEntry { + email: string; + reason: 'hard_bounce' | 'complaint' | 'unsubscribe' | 'soft_bounce' | 'manual'; + created_at: Date; + source_email_id?: string; // Which email triggered this +} + +// Check before every send +async function canSendTo(email: string): Promise { + const suppressed = await db.suppressions.findOne({ email }); + return !suppressed; +} + +// Add to suppression list +async function suppressEmail(email: string, reason: string, sourceId?: string) { + await db.suppressions.upsert({ + email: email.toLowerCase(), + reason, + created_at: new Date(), + source_email_id: sourceId, + }); +} +``` + +### Pre-Send Check + +**Always check suppression before sending:** + +```typescript +async function sendEmail(to: string, emailData: EmailData) { + if (!await canSendTo(to)) { + console.log(`Skipping suppressed email: ${to}`); + return { skipped: true, reason: 'suppressed' }; + } + + return await resend.emails.send({ to, ...emailData }); +} +``` + +## List Hygiene + +Regular maintenance to keep lists healthy. + +### Automated Cleanup + +| Task | Frequency | Action | +|------|-----------|--------| +| Remove hard bounces | Real-time (via webhook) | Immediate suppression | +| Remove complaints | Real-time (via webhook) | Immediate suppression | +| Process unsubscribes | Real-time | Remove from marketing lists | +| Review soft bounces | Daily | Suppress after 3 failures | +| Remove inactive | Monthly | Re-engagement → remove | + +Learn more: https://resend.com/docs/knowledge-base/audience-hygiene + +### Re-engagement Campaigns + +Before removing inactive subscribers: + +1. **Identify inactive:** No opens/clicks in 45-90 days +2. **Send re-engagement:** "We miss you" or "Still interested?" +3. **Wait 14-30 days** for response +4. **Remove non-responders** from active lists + +```typescript +async function runReengagement() { + const inactive = await getInactiveSubscribers(90); // 90 days + + for (const subscriber of inactive) { + if (!subscriber.reengagement_sent) { + await sendReengagementEmail(subscriber); + await markReengagementSent(subscriber.email); + } else if (daysSince(subscriber.reengagement_sent) > 30) { + await removeFromMarketingLists(subscriber.email); + } + } +} +``` + +## Data Retention + +### Email Logs + +| Data Type | Recommended Retention | Notes | +|-----------|----------------------|-------| +| Send attempts | 90 days | Debugging, analytics | +| Delivery status | 90 days | Compliance, reporting | +| Bounce/complaint events | 3 years | Required for CASL | +| Suppression list | Indefinite | Never delete | +| Email content | 30 days | Storage costs | +| Consent records | 3 years after expiry | Legal requirement | + +### Retention Policy Implementation + +```typescript +// Daily cleanup job +async function cleanupOldData() { + const now = new Date(); + + // Delete old email logs (keep 90 days) + await db.emailLogs.deleteMany({ + created_at: { $lt: subDays(now, 90) } + }); + + // Delete old email content (keep 30 days) + await db.emailContent.deleteMany({ + created_at: { $lt: subDays(now, 30) } + }); + + // Never delete: suppressions, consent records +} +``` + +## Metrics to Monitor + +| Metric | Target | Alert Threshold | +|--------|--------|-----------------| +| Bounce rate | <2% | >2% | +| Complaint rate | <0.05% | >0.05% | +| Suppression list growth | Stable | Sudden spike | + +## Transactional vs Marketing Lists + +**Keep separate:** +- Transactional: Can send to anyone with account relationship +- Marketing: Only opted-in subscribers + +**Suppression applies to both:** Hard bounces and complaints suppress across all email types. + +**Unsubscribe is marketing-only:** User unsubscribing from marketing can still receive transactional emails (password resets, order confirmations). + +## Related + +- [Webhooks & Events](./webhooks-events.md) - Receive bounce/complaint notifications +- [Deliverability](./deliverability.md) - How list hygiene affects sender reputation +- [Compliance](./compliance.md) - Legal requirements for data retention diff --git a/.agents/skills/email-best-practices/resources/marketing-emails.md b/.agents/skills/email-best-practices/resources/marketing-emails.md new file mode 100644 index 00000000000..984ffcc7e61 --- /dev/null +++ b/.agents/skills/email-best-practices/resources/marketing-emails.md @@ -0,0 +1,115 @@ +# Marketing Email Best Practices + +Promotional emails that require explicit consent and provide value to recipients. + +## Core Principles + +1. **Consent first** - Explicit opt-in required (especially GDPR/CASL) +2. **Value-driven** - Provide useful content, not just promotions +3. **Respect preferences** - Let users control frequency and content types + +## Opt-In Requirements + +### Explicit Opt-In + +**What counts:** +- User checks unchecked box +- User clicks "Subscribe" button +- User completes form with clear subscription intent + +**What doesn't count:** +- Pre-checked boxes +- Opt-out model +- Assumed consent from purchase +- Purchased/rented lists + +### Informed Consent + +Disclose: email types, frequency, sender identity, how to unsubscribe. + +✅ "Subscribe to our weekly newsletter with product updates and tips" +❌ "Sign up for emails" + +### Double Opt-In (Recommended) + +1. User submits email +2. Send confirmation email with verification link +3. User clicks to confirm +4. Add to list only after confirmation + +Benefits: Verifies deliverability, confirms intent, reduces complaints, required in some regions (Germany). + +## Unsubscribe Requirements + +**Must be:** +- Prominent in every email +- One-click (preferred) +- Immediate (GDPR) or within 10 days (CAN-SPAM) (immediate preferred) +- Free, no login required + +**Preference center options:** Frequency (daily/weekly/monthly), content types, complete unsubscribe. + +## Content and Design + +### Subject Lines + +- Clear and specific (50 chars or less for mobile) +- Create curiosity without misleading +- A/B test regularly + +✅ "Your weekly digest: 5 productivity tips" +❌ "You won't believe what happened!" + +### Structure + +**Above fold:** Value proposition, primary CTA, engaging visual + +**Body:** Scannable (short paragraphs, bullets), clear hierarchy, multiple CTAs + +**Footer:** Unsubscribe link, company info, physical address (CAN-SPAM), social links + +### Mobile-First + +- Single column layout +- 44x44px minimum buttons +- 16px minimum text +- Test on iOS, Android, dark mode + +## Segmentation + +**Segment by:** Behavior (purchases, activity), demographics, preferences, engagement level, signup source. + +Benefits: Higher open/click rates, lower unsubscribes, better experience. + +## Personalization + +**Options:** Name in subject/greeting, location-specific content, behavior-based recommendations, purchase history. + +**Don't over-personalize** - can feel intrusive. Use data you have permission to use. + +## Frequency and Timing + +**Frequency:** Start conservative, increase based on engagement, let users set preferences, monitor unsubscribe rates. + +**Timing:** Weekday mornings (9-11 AM local), Tuesday-Thursday often best. Test your specific audience. + +## List Hygiene + +**Remove immediately:** Hard bounces, unsubscribes, complaints + +**Remove after inactivity:** Send re-engagement campaign first, then remove non-responders + +**Monitor:** Bounce rate <2%, complaint rate <0.05% + +## Required Elements (All Marketing Emails) + +- Clear sender identification +- Physical mailing address (CAN-SPAM) +- Unsubscribe mechanism +- Indication it's marketing (GDPR) + +## Related + +- [Compliance](./compliance.md) - Detailed legal requirements by region +- [Email Capture](./email-capture.md) - Collecting consent properly +- [List Management](./list-management.md) - Maintaining list hygiene diff --git a/.agents/skills/email-best-practices/resources/sending-reliability.md b/.agents/skills/email-best-practices/resources/sending-reliability.md new file mode 100644 index 00000000000..2143b36deb1 --- /dev/null +++ b/.agents/skills/email-best-practices/resources/sending-reliability.md @@ -0,0 +1,155 @@ +# Sending Reliability + +Ensuring emails are sent exactly once and handling failures gracefully. + +## Idempotency + +Prevent duplicate emails when retrying failed requests. + +### The Problem + +Network issues, timeouts, or server errors can leave you uncertain if an email was sent. Retrying without idempotency risks sending duplicates. + +### Solution: Idempotency Keys + +Send a unique key with each request. If the same key is sent again, the server returns the original response instead of sending another email. + +```typescript +// Generate deterministic key based on the business event +const idempotencyKey = `password-reset-${userId}-${resetRequestId}`; + +await resend.emails.send({ + from: 'noreply@example.com', + to: user.email, + subject: 'Reset your password', + html: emailHtml, +}, { + headers: { + 'Idempotency-Key': idempotencyKey + } +}); +``` + +### Key Generation Strategies + +| Strategy | Example | Use When | +|----------|---------|----------| +| Event-based | `order-confirm-${orderId}` | One email per event (recommended) | +| Request-scoped | `reset-${userId}-${resetRequestId}` | Retries within same request | +| UUID | `crypto.randomUUID()` | No natural key (generate once, reuse on retry) | + +**Best practice:** Use deterministic keys based on the business event. If you retry the same logical send, the same key must be generated. Avoid `Date.now()` or random values generated fresh on each attempt. + +**Key expiration:** Idempotency keys are typically cached for 24 hours. Retries within this window return the original response. After expiration, the same key triggers a new send—so complete your retry logic well within 24 hours. + +## Retry Logic + +Handle transient failures with exponential backoff. + +### When to Retry + +| Error Type | Retry? | Notes | +|------------|--------|-------| +| 5xx (server error) | ✅ Yes | Transient, likely to resolve | +| 429 (rate limit) | ✅ Yes | Wait for rate limit window | +| 4xx (client error) | ❌ No | Fix the request first | +| Network timeout | ✅ Yes | Transient | +| DNS failure | ✅ Yes | May be transient | + +### Exponential Backoff + +```typescript +async function sendWithRetry(emailData, maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await resend.emails.send(emailData); + } catch (error) { + if (!isRetryable(error) || attempt === maxRetries - 1) { + throw error; + } + const delay = Math.min(1000 * Math.pow(2, attempt), 30000); + await sleep(delay + Math.random() * 1000); // Add jitter + } + } +} + +function isRetryable(error) { + return error.statusCode >= 500 || + error.statusCode === 429 || + error.code === 'ETIMEDOUT'; +} +``` + +**Backoff schedule:** 1s → 2s → 4s → 8s (with jitter to prevent thundering herd) + +## Error Handling + +### Common Error Codes + +| Code | Meaning | Action | +|------|---------|--------| +| 400 | Bad request | Fix payload (invalid email, missing field) | +| 401 | Unauthorized | Check API key | +| 403 | Forbidden | Check permissions, domain verification | +| 404 | Not found | Check endpoint URL | +| 422 | Validation error | Fix request data | +| 429 | Rate limited | Back off, retry after delay | +| 500 | Server error | Retry with backoff | +| 503 | Service unavailable | Retry with backoff | + +### Error Handling Pattern + +```typescript +try { + const result = await resend.emails.send(emailData); + await logSuccess(result.id, emailData); +} catch (error) { + if (error.statusCode === 429) { + await queueForRetry(emailData, error.retryAfter); + } else if (error.statusCode >= 500) { + await queueForRetry(emailData); + } else { + await logFailure(error, emailData); + await alertOnCriticalEmail(emailData); // For password resets, etc. + } +} +``` + +## Queuing for Reliability + +For critical emails, use a queue to ensure delivery even if the initial send fails. + +**Benefits:** +- Survives application restarts +- Automatic retry handling +- Rate limit management +- Audit trail + +**Simple pattern:** +1. Write email to queue/database with "pending" status +2. Process queue, attempt send +3. On success: mark "sent", store message ID +4. On retryable failure: increment retry count, schedule retry +5. On permanent failure: mark "failed", alert + +## Timeouts + +Set appropriate timeouts to avoid hanging requests. + +```typescript +const controller = new AbortController(); +const timeout = setTimeout(() => controller.abort(), 10000); + +try { + await resend.emails.send(emailData, { signal: controller.signal }); +} finally { + clearTimeout(timeout); +} +``` + +**Recommended:** 10-30 seconds for email API calls. + +## Related + +- [Webhooks & Events](./webhooks-events.md) - Process delivery confirmations and failures +- [List Management](./list-management.md) - Handle bounces and suppress invalid addresses diff --git a/.agents/skills/email-best-practices/resources/transactional-email-catalog.md b/.agents/skills/email-best-practices/resources/transactional-email-catalog.md new file mode 100644 index 00000000000..0cc9495243c --- /dev/null +++ b/.agents/skills/email-best-practices/resources/transactional-email-catalog.md @@ -0,0 +1,418 @@ +# Transactional Email Catalog + +A comprehensive catalog of transactional emails organized by category, plus recommended email combinations for different app types. + +## When to Use This + +- Planning what transactional emails your app needs +- Choosing the right emails for your app type +- Understanding what content each email type should include +- Implementing transactional email features + +## Email Combinations by App Type + +Use these combinations as a starting point based on what you're building. + +### Authentication-Focused App + +Apps where user accounts and security are core (login systems, identity providers, account management). + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes +- Security alerts (new device, password change) +- Account update notifications + +**Optional:** +- Welcome email (must not be promotional) +- Account deletion confirmation + +### Newsletter / Content Platform + +Apps focused on content delivery and subscriptions. + +**Essential:** +- Email verification +- Password reset +- Welcome email (must not be promotional) +- Subscription confirmation + +**Optional:** +- OTP / 2FA codes +- Account update notifications + +### E-commerce / Marketplace + +Apps where users buy products or services. + +**Essential:** +- Email verification +- Password reset +- Welcome email (must not be promotional) +- Order confirmation +- Shipping notifications +- Invoice / receipt +- Payment failed notices + +**Optional:** +- OTP / 2FA codes +- Security alerts +- Subscription confirmations (for recurring orders) + +### SaaS / Subscription Service + +Apps with paid subscription tiers and ongoing billing. + +**Essential:** +- Email verification +- Password reset +- Welcome email (must not be promotional) +- OTP / 2FA codes +- Security alerts +- Subscription confirmation +- Subscription renewal notice +- Payment failed notices +- Invoice / receipt + +**Optional:** +- Account update notifications +- Feature change notifications (for breaking changes) + +### Financial / Fintech App + +Apps handling money, payments, or sensitive financial data. + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes (required for sensitive actions) +- Security alerts (all types) +- Account update notifications +- Transaction confirmations +- Invoice / receipt +- Payment failed notices + +**Optional:** +- Welcome email (must not be promotional) +- Compliance notices + +### Social / Community Platform + +Apps focused on user interaction and community features. + +**Essential:** +- Email verification +- Password reset +- Welcome email (must not be promotional) +- Security alerts + +**Optional:** +- OTP / 2FA codes +- Account update notifications +- Activity notifications (mentions, replies) + +### Developer Tools / API Platform + +Apps targeting developers with API access and integrations. + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes +- Security alerts +- API key notifications (creation, expiration) +- Subscription confirmation +- Payment failed notices + +**Optional:** +- Welcome email (must not be promotional) +- Usage alerts (approaching limits) +- Feature change notifications + +### Healthcare / HIPAA-Compliant App + +Apps handling protected health information. + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes (required) +- Security alerts (all types, detailed) +- Account update notifications +- Appointment confirmations + +**Optional:** +- Welcome email (must not be promotional) +- Compliance notices + +**Note:** Healthcare apps have strict requirements. Emails should contain minimal PHI and link to secure portals for sensitive information. + +--- + +## Full Email Catalog + +### Authentication & Security + +#### Email Verification / Account Verification + +**When to send:** Immediately after user signs up or changes email address. + +**Purpose:** Verify the email address belongs to the user. + +**Content should include:** +- Clear verification link or code +- Expiration time (typically 24-48 hours) +- Instructions on what to do +- Security notice if link is clicked by mistake + +**Best practices:** +- Send immediately (within seconds) +- Include expiration notice +- Provide resend option +- Link to support if issues + +#### OTP / 2FA Codes + +**When to send:** When user requests two-factor authentication code. + +**Purpose:** Provide time-sensitive authentication code. + +**Content should include:** +- The OTP code (clearly displayed) +- Expiration time (typically 5-10 minutes) +- Security warnings +- Instructions on what to do if not requested + +**Best practices:** +- Send immediately +- Code should be large and easy to read +- Include expiration prominently +- Warn about sharing codes +- Provide "I didn't request this" link + +#### Password Reset + +**When to send:** When user requests password reset. + +**Purpose:** Allow user to securely reset forgotten password. + +**Content should include:** +- Reset link (with token) +- Expiration time (typically 1 hour) +- Security warnings +- Instructions if not requested + +**Best practices:** +- Send immediately +- Link expires quickly (1 hour) +- Include IP address and location if available +- Provide "I didn't request this" link +- Don't include the old password + +#### Security Alerts + +**When to send:** When security-relevant events occur (login from new device, password change, etc.). + +**Purpose:** Notify user of account security events. + +**Content should include:** +- What happened (clear description) +- When it happened +- Location/IP if available +- Action to take if suspicious +- Link to security settings + +**Best practices:** +- Send immediately +- Be clear and specific +- Include actionable steps +- Provide way to report suspicious activity + +### Account Management + +#### Welcome Email + +**When to send:** Immediately after successful account creation and verification. + +**Purpose:** Welcome new users and guide them to next steps (must not be promotional). + +**Content should include:** +- Welcome message +- Key features or next steps +- Links to important resources +- Support contact information + +**Best practices:** +- Send after email verification +- Keep it focused and actionable +- Don't overwhelm with information +- Set expectations about future emails + +#### Account Update Notifications + +**When to send:** When user changes account settings (email, password, profile, etc.). + +**Purpose:** Confirm account changes and provide security notice. + +**Content should include:** +- What changed +- When it changed +- Action to take if unauthorized +- Link to account settings + +**Best practices:** +- Send immediately after change +- Be specific about what changed +- Include security notice +- Provide easy way to revert if needed + +### E-commerce & Transactions + +#### Order Confirmations + +**When to send:** Immediately after order is placed. + +**Purpose:** Confirm order details and provide receipt. + +**Content should include:** +- Order number +- Items ordered with quantities +- Pricing breakdown +- Shipping address +- Estimated delivery date +- Order tracking link (if available) + +**Best practices:** +- Send within minutes of order +- Include all order details +- Make it easy to print or save +- Provide customer service contact + +#### Shipping Notifications + +**When to send:** When order ships, with tracking updates. + +**Purpose:** Notify user that order has shipped and provide tracking. + +**Content should include:** +- Order number +- Tracking number +- Carrier information +- Expected delivery date +- Tracking link +- Shipping address confirmation + +**Best practices:** +- Send when order ships +- Include tracking number prominently +- Provide carrier tracking link +- Update on major tracking milestones + +#### Invoices and Receipts + +**When to send:** After payment is processed. + +**Purpose:** Provide payment confirmation and receipt. + +**Content should include:** +- Invoice/receipt number +- Payment amount +- Payment method +- Items/services purchased +- Payment date +- Downloadable PDF (if applicable) + +**Best practices:** +- Send immediately after payment +- Include all payment details +- Make it easy to download/save +- Include tax information if applicable + +### Subscriptions & Billing + +#### Subscription Confirmations + +**When to send:** When user subscribes or changes subscription. + +**Purpose:** Confirm subscription details and billing information. + +**Content should include:** +- Subscription plan details +- Billing amount and frequency +- Next billing date +- Payment method +- Link to manage subscription + +**Best practices:** +- Send immediately after subscription +- Clearly state billing terms +- Provide easy cancellation option +- Include support contact + +#### Subscription Renewal Notices + +**When to send:** Before subscription renews (typically 3-7 days before). + +**Purpose:** Notify user of upcoming renewal and charge. + +**Content should include:** +- Renewal date +- Amount to be charged +- Payment method on file +- Link to update payment method +- Link to cancel if desired + +**Best practices:** +- Send with enough notice (3-7 days) +- Be clear about amount and date +- Make it easy to update payment method +- Provide cancellation option + +#### Payment Failed Notices + +**When to send:** When subscription payment fails. + +**Purpose:** Notify user of payment failure and provide resolution steps. + +**Content should include:** +- What happened +- Amount that failed +- Reason for failure (if available) +- Steps to resolve +- Link to update payment method +- Consequences if not resolved + +**Best practices:** +- Send immediately after failure +- Be clear about consequences +- Provide easy resolution path +- Include support contact + +### Notifications & Updates + +#### Feature Announcements (Transactional) + +**When to send:** When a feature the user is using changes significantly. + +**Purpose:** Notify users of changes that affect their use of the service. + +**Content should include:** +- What changed +- How it affects the user +- What action (if any) is needed +- Link to more information + +**Best practices:** +- Only for significant changes +- Focus on user impact +- Provide clear next steps +- Link to documentation + +**Note:** General feature announcements are marketing emails. Only send as transactional if the change directly affects an active feature the user is using. + +## Related Topics + +- [Email Types](./email-types.md) - Understanding transactional vs marketing +- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails +- [Compliance](./compliance.md) - Legal requirements for each email type diff --git a/.agents/skills/email-best-practices/resources/transactional-emails.md b/.agents/skills/email-best-practices/resources/transactional-emails.md new file mode 100644 index 00000000000..0809647c609 --- /dev/null +++ b/.agents/skills/email-best-practices/resources/transactional-emails.md @@ -0,0 +1,92 @@ +# Transactional Email Best Practices + +Clear, actionable emails that users expect and need—password resets, confirmations, OTPs. + +## Core Principles + +1. **Clarity over creativity** - Users need to understand and act quickly +2. **Action-oriented** - Clear purpose, obvious primary action +3. **Time-sensitive** - Send immediately (within seconds) + +## Subject Lines + +**Be specific and include context:** + +| ✅ Good | ❌ Bad | +|---------|--------| +| Reset your password for [App] | Action required | +| Your order #12345 has shipped | Update on your order | +| Your 2FA code for [App] | Security code: 12345 | +| Verify your email for [App] | Verify your email | + +Include identifiers when helpful: order numbers, account names, expiration times. + +## Pre-Header + +The text snippet after subject line. Use it to: +- Reinforce subject ("This link expires in 1 hour") +- Add urgency or context +- Call-to-action preview + +Keep under 90 characters. + +## Content Structure + +**Above the fold (first screen):** +- Clear purpose +- Primary action button +- Time-sensitive details (expiration) + +**Hierarchy:** Header → Primary message → Details → Action button → Secondary info + +**Format:** Short paragraphs (2-3 sentences), bullet points, bold for emphasis, white space. + +## Mobile-First Design + +60%+ emails are opened on mobile. + +- **Layout:** Single column, stack vertically +- **Buttons:** 44x44px minimum, full-width on mobile +- **Text:** 16px minimum body, 20-24px headings +- **OTP codes:** 24-32px, monospace font + +## Sender Configuration + +| Field | Best Practice | Example | +|-------|--------------|---------| +| From Name | App/company name, consistent | [App Name] | +| From Email | Subdomain, real address | hello@mail.yourdomain.com | +| Reply-To | Monitored inbox | support@yourdomain.com | + +Avoid `noreply@` - users reply to transactional emails. + +## Code and Link Display + +**OTP/Verification codes:** +- Large (24-32px), monospace font +- Centered, clear label +- Include expiration nearby +- Make copyable + +**Buttons:** +- Large, tappable (44x44px+) +- Contrasting colors +- Clear action text ("Reset Password", "Verify Email") +- HTTPS links only + +## Error Handling + +**Resend functionality:** +- Allow after 60 seconds +- Limit attempts (3 per hour) +- Show countdown timer + +**Expired links:** +- Clear "expired" message +- Offer to send new link +- Provide support contact + +**"I didn't request this":** +- Include in password resets, OTPs, security alerts +- Link to security contact +- Log clicks for monitoring diff --git a/.agents/skills/email-best-practices/resources/webhooks-events.md b/.agents/skills/email-best-practices/resources/webhooks-events.md new file mode 100644 index 00000000000..0008c995060 --- /dev/null +++ b/.agents/skills/email-best-practices/resources/webhooks-events.md @@ -0,0 +1,167 @@ +# Webhooks and Events + +Receiving and processing email delivery events in real-time. + +## Event Types + +| Event | When Fired | Use For | +|-------|------------|---------| +| `email.sent` | Email accepted by Resend | Confirming send initiated | +| `email.delivered` | Email delivered to recipient server | Confirming delivery | +| `email.bounced` | Email bounced (hard or soft) | List hygiene, alerting | +| `email.complained` | Recipient marked as spam | Immediate unsubscribe | +| `email.opened` | Recipient opened email | Engagement tracking | +| `email.clicked` | Recipient clicked link | Engagement tracking | + +## Webhook Setup + +### 1. Create Endpoint + +Your endpoint must: +- Accept POST requests +- Return 2xx status quickly (within 5 seconds) +- Handle duplicate events (idempotent processing) + +```typescript +app.post('/webhooks/resend', async (req, res) => { + // Return 200 immediately to acknowledge receipt + res.status(200).send('OK'); + + // Process asynchronously + processWebhookAsync(req.body).catch(console.error); +}); +``` + +### 2. Verify Signatures + +Always verify webhook signatures to prevent spoofing. + +```typescript +import { Webhook } from 'svix'; + +const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET); + +app.post('/webhooks/resend', (req, res) => { + try { + const payload = webhook.verify( + JSON.stringify(req.body), + { + 'svix-id': req.headers['svix-id'], + 'svix-timestamp': req.headers['svix-timestamp'], + 'svix-signature': req.headers['svix-signature'], + } + ); + // Process verified payload + } catch (err) { + return res.status(400).send('Invalid signature'); + } +}); +``` + +### 3. Register Webhook URL + +Configure your webhook endpoint in the Resend dashboard or via API. + +## Processing Events + +### Bounce Handling + +```typescript +async function handleBounce(event) { + const { email_id, email, bounce_type } = event.data; + + if (bounce_type === 'hard') { + // Permanent failure - remove from all lists + await suppressEmail(email, 'hard_bounce'); + await removeFromAllLists(email); + } else { + // Soft bounce - track and remove after threshold + await incrementSoftBounce(email); + const count = await getSoftBounceCount(email); + if (count >= 3) { + await suppressEmail(email, 'soft_bounce_limit'); + } + } +} +``` + +### Complaint Handling + +```typescript +async function handleComplaint(event) { + const { email } = event.data; + + // Immediate suppression - no exceptions + await suppressEmail(email, 'complaint'); + await removeFromAllLists(email); + await logComplaint(event); // For analysis +} +``` + +### Delivery Confirmation + +```typescript +async function handleDelivered(event) { + const { email_id } = event.data; + await updateEmailStatus(email_id, 'delivered'); +} +``` + +## Idempotent Processing + +Webhooks may be sent multiple times. Use event IDs to prevent duplicate processing. + +```typescript +async function processWebhook(event) { + const eventId = event.id; + + // Check if already processed + if (await isEventProcessed(eventId)) { + return; // Skip duplicate + } + + // Process event + await handleEvent(event); + + // Mark as processed + await markEventProcessed(eventId); +} +``` + +## Error Handling + +### Retry Behavior + +If your endpoint returns non-2xx, webhooks will retry with exponential backoff: +- Retry 1: ~30 seconds +- Retry 2: ~1 minute +- Retry 3: ~5 minutes +- (continues for ~24 hours) + +### Best Practices + +- **Return 200 quickly** - Process asynchronously to avoid timeouts +- **Be idempotent** - Handle duplicate deliveries gracefully +- **Log everything** - Store raw events for debugging +- **Alert on failures** - Monitor webhook processing errors +- **Queue for processing** - Use a job queue for complex handling + +## Testing Webhooks + +**Local development:** Use ngrok or similar to expose localhost. + +```bash +ngrok http 3000 +# Use the ngrok URL as your webhook endpoint +``` + +**Verify handling:** Send test events through Resend dashboard or manually trigger each event type. + +## Ingest webhooks for data storage +- [Open source repo](https://github.com/resend/resend-webhooks-ingester) +- [Why store data](https://resend.com/docs/dashboard/webhooks/how-to-store-webhooks-data) + +## Related + +- [List Management](./list-management.md) - What to do with bounce/complaint data +- [Sending Reliability](./sending-reliability.md) - Retry logic when sends fail diff --git a/.agents/skills/react-email/SKILL.md b/.agents/skills/react-email/SKILL.md new file mode 100644 index 00000000000..006ea2745b1 --- /dev/null +++ b/.agents/skills/react-email/SKILL.md @@ -0,0 +1,518 @@ +--- +name: react-email +description: Use when creating HTML email templates with React components - welcome emails, password resets, notifications, order confirmations, newsletters, or transactional emails. +license: MIT +metadata: + author: Resend + version: "1.1.0" +--- + +# React Email + +Build and send HTML emails using React components - a modern, component-based approach to email development that works across all major email clients. + +## Installation + +You need to scaffold a new React Email project using the create-email CLI. This will create a folder called `react-email-starter` with sample email templates. + +Using npm: +```sh +npx create-email@latest +``` + +Using yarn: +```sh +yarn create email +``` + +Using pnpm: +```sh +pnpm create email +``` + +Using bun: +```sh +bun create email +``` + +## Navigate to Project Directory + +You must change into the newly created project folder: + +```sh +cd react-email-starter +``` + +## Install Dependencies + +You need to install all project dependencies before running the development server. + +Using npm: +```sh +npm install +``` + +Using yarn: +```sh +yarn +``` + +Using pnpm: +```sh +pnpm install +``` + +Using bun: +```sh +bun install +``` + +## Start the Development Server + +Your task is to start the local preview server to view and edit email templates. + +Using npm: +```sh +npm run dev +``` + +Using yarn: +```sh +yarn dev +``` + +Using pnpm: +```sh +pnpm dev +``` + +Using bun: +```sh +bun dev +``` + +## Verify Installation + +Confirm the development server is running by checking that localhost:3000 is accessible. The server will display a preview interface where you can view email templates from the `emails` folder. + +### Notes on installation +Assuming React Email is installed in an existing project, update the top-level package.json file with a script to run the React Email preview server. + +```json +{ + "scripts": { + "email": "email dev --dir emails --port 3000" + } +} +``` + +Make sure the path to the emails folder is relative to the base project directory. + + +### tsconfig.json updating or creation + +Ensure the tsconfig.json includes proper support for jsx. + +## Basic Email Template + +Replace the sample email templates. Here is how to create a new email template: + +Create an email component with proper structure using the Tailwind component for styling: + +```tsx +import { + Html, + Head, + Preview, + Body, + Container, + Heading, + Text, + Button, + Tailwind, + pixelBasedPreset +} from '@react-email/components'; + +interface WelcomeEmailProps { + name: string; + verificationUrl: string; +} + +export default function WelcomeEmail({ name, verificationUrl }: WelcomeEmailProps) { + return ( + + + + Welcome - Verify your email + + + + Welcome! + + + Hi {name}, thanks for signing up! + + + + + + + ); +} + +// Preview props for testing +WelcomeEmail.PreviewProps = { + name: 'John Doe', + verificationUrl: 'https://example.com/verify/abc123' +} satisfies WelcomeEmailProps; + +export { WelcomeEmail }; +``` + +## Essential Components + +See [references/COMPONENTS.md](references/COMPONENTS.md) for complete component documentation. + +**Core Structure:** +- `Html` - Root wrapper with `lang` attribute +- `Head` - Meta elements, styles, fonts +- `Body` - Main content wrapper +- `Container` - Centers content (max-width layout) +- `Section` - Layout sections +- `Row` & `Column` - Multi-column layouts +- `Tailwind` - Enables Tailwind CSS utility classes + +**Content:** +- `Preview` - Inbox preview text, always first in `Body` +- `Heading` - h1-h6 headings +- `Text` - Paragraphs +- `Button` - Styled link buttons +- `Link` - Hyperlinks +- `Img` - Images (see Static Files section below) +- `Hr` - Horizontal dividers + +**Specialized:** +- `CodeBlock` - Syntax-highlighted code +- `CodeInline` - Inline code +- `Markdown` - Render markdown +- `Font` - Custom web fonts + +## Before Writing Code + +When a user requests an email template, ask clarifying questions FIRST if they haven't provided: + +1. **Brand colors** - Ask for primary brand color (hex code like #007bff) +2. **Logo** - Ask if they have a logo file and its format (PNG/JPG only - warn if SVG/WEBP) +3. **Style preference** - Professional, casual, or minimal tone +4. **Production URL** - Where will static assets be hosted in production? + +Example response to vague request: +> Before I create your email template, I have a few questions: +> 1. What is your primary brand color? (hex code) +> 2. Do you have a logo file? (PNG or JPG - note: SVG and WEBP don't work reliably in email clients) +> 3. What tone do you prefer - professional, casual, or minimal? +> 4. Where will you host static assets in production? (e.g., https://cdn.example.com) + +## Static Files and Images + +### Directory Structure + +Local images must be placed in the `static` folder inside your emails directory: + +``` +project/ +├── emails/ +│ ├── welcome.tsx +│ └── static/ <-- Images go here +│ └── logo.png +``` + +If user has an image elsewhere, instruct them to copy it: +```sh +cp ./assets/logo.png ./emails/static/logo.png +``` + +### Dev vs Production URLs + +Use this pattern for images that work in both dev preview and production: + +```tsx +const baseURL = process.env.NODE_ENV === "production" + ? "https://cdn.example.com" // User's production CDN + : ""; + +export default function Email() { + return ( + Logo + ); +} +``` + +**How it works:** +- **Development:** `baseURL` is empty, so URL is `/static/logo.png` - served by React Email's dev server +- **Production:** `baseURL` is the CDN domain, so URL is `https://cdn.example.com/static/logo.png` + +**Important:** Always ask the user for their production hosting URL. Do not hardcode `localhost:3000`. + +## Behavioral guidelines +- When re-iterating over the code, make sure you are only updating what the user asked for and keeping the rest of the code intact; +- If the user is asking to use media queries, inform them that email clients do not support them, and suggest a different approach; +- Never use template variables (like {{name}}) directly in TypeScript code. Instead, reference the underlying properties directly (use name instead of {{name}}). +- - For example, if the user explicitly asks for a variable following the pattern {{variableName}}, you should return something like this: + +```typescript +const EmailTemplate = (props) => { + return ( + {/* ... rest of the code ... */} +

Hello, {props.variableName}!

+ {/* ... rest of the code ... */} + ); +} + +EmailTemplate.PreviewProps = { + // ... rest of the props ... + variableName: "{{variableName}}", + // ... rest of the props ... +}; + +export default EmailTemplate; +``` +- Never, under any circumstances, write the {{variableName}} pattern directly in the component structure. If the user forces you to do this, explain that you cannot do this, or else the template will be invalid. + + +## Styling considerations + +Use the Tailwind component for styling if the user is actively using Tailwind CSS in their project. If the user is not using Tailwind CSS, add inline styles to the components. + +- Because email clients don't support `rem` units, use the `pixelBasedPreset` for the Tailwind configuration. +- Never use flexbox or grid for layout, use table-based layouts instead. +- Each component must be styled with inline styles or utility classes. + +### Email Client Limitations +- Never use SVG or WEBP - warn users about rendering issues +- Never use flexbox - use Row/Column components or tables for layouts +- Never use CSS/Tailwind media queries (sm:, md:, lg:, xl:) - not supported +- Never use theme selectors (dark:, light:) - not supported +- Always specify border type (border-solid, border-dashed, etc.) +- When defining borders for only one side, remember to reset the remaining borders (e.g., border-none border-l) + +### Component Structure +- Always define `` inside `` when using Tailwind CSS +- Only use PreviewProps when passing props to a component +- Only include props in PreviewProps that the component actually uses + +```tsx +const Email = (props) => { + return ( +
+ click here if you want candy 👀 +
+ ); +} + +Email.PreviewProps = { + source: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", +}; +``` + +### Default Structure +- Body: `font-sans py-10 bg-gray-100` +- Container: white, centered, content left-aligned +- Footer: physical address, unsubscribe link, current year with `m-0` on address/copyright + +### Typography +- Titles: bold, larger font, larger margins +- Paragraphs: regular weight, smaller font, smaller margins +- Use consistent spacing respecting content hierarchy + +### Images +- Only include if user requests +- Never use fixed width/height - use responsive units (w-full, h-auto) +- Never distort user-provided images +- Never create SVG images - only use provided or web images + +### Buttons +- Always use `box-border` to prevent padding overflow + +### Layout +- Always mobile-friendly by default +- Use stacked layouts that work on all screen sizes +- Remove default spacing/margins/padding between list items + +### Dark Mode +When requested: container black (#000), background dark gray (#151516) + +### Best Practices +- Choose colors, layout, and copy based on user's request +- Make templates unique, not generic +- Use keywords in email body to increase conversion + +## Rendering + +### Convert to HTML + +```tsx +import { render } from '@react-email/components'; +import { WelcomeEmail } from './emails/welcome'; + +const html = await render( + +); +``` + +### Convert to Plain Text + +```tsx +import { render } from '@react-email/components'; +import { WelcomeEmail } from './emails/welcome'; + +const text = await render(, { plainText: true }); +``` + +## Sending + +React Email supports sending with any email service provider. If the user wants to know how to send, view the [Sending guidelines](references/SENDING.md). + +Quick example using the Resend SDK for Node.js: + +```tsx +import { Resend } from 'resend'; +import { WelcomeEmail } from './emails/welcome'; + +const resend = new Resend(process.env.RESEND_API_KEY); + +const { data, error } = await resend.emails.send({ + from: 'Acme ', + to: ['user@example.com'], + subject: 'Welcome to Acme', + react: +}); + +if (error) { + console.error('Failed to send:', error); +} +``` + +The Node SDK automatically handles the plain-text rendering and HTML rendering for you. + +## Internationalization + +See [references/I18N.md](references/I18N.md) for complete i18n documentation. + +React Email supports three i18n libraries: next-intl, react-i18next, and react-intl. + +### Quick Example (next-intl) + +```tsx +import { createTranslator } from 'next-intl'; +import { + Html, + Body, + Container, + Text, + Button, + Tailwind, + pixelBasedPreset +} from '@react-email/components'; + +interface EmailProps { + name: string; + locale: string; +} + +export default async function WelcomeEmail({ name, locale }: EmailProps) { + const t = createTranslator({ + messages: await import(\`../messages/\${locale}.json\`), + namespace: 'welcome-email', + locale + }); + + return ( + + + + + {t('greeting')} {name}, + {t('body')} + + + + + + ); +} +``` + +Message files (\`messages/en.json\`, \`messages/es.json\`, etc.): + +```json +{ + "welcome-email": { + "greeting": "Hi", + "body": "Thanks for signing up!", + "cta": "Get Started" + } +} +``` + +## Email Best Practices + +1. **Test across email clients** - Test in Gmail, Outlook, Apple Mail, Yahoo Mail. Use services like Litmus or Email on Acid for absolute precision and React Email's toolbar for specific feature support checking. + +2. **Keep it responsive** - Max-width around 600px, test on mobile devices. + +3. **Use absolute image URLs** - Host on reliable CDN, always include \`alt\` text. + +4. **Provide plain text version** - Required for accessibility and some email clients. + +5. **Keep file size under 102KB** - Gmail clips larger emails. + +6. **Add proper TypeScript types** - Define interfaces for all email props. + +7. **Include preview props** - Add \`.PreviewProps\` to components for development testing. + +8. **Handle errors** - Always check for errors when sending emails. + +9. **Use verified domains** - For production, use verified domains in \`from\` addresses. + +## Common Patterns + +See [references/PATTERNS.md](references/PATTERNS.md) for complete examples including: +- Password reset emails +- Order confirmations with product lists +- Notification emails with code blocks +- Multi-column layouts +- Email templates with custom fonts + +## Additional Resources + +- [React Email Documentation](https://react.email/docs/llms.txt) +- [React Email GitHub](https://github.com/resend/react-email) +- [Resend Documentation](https://resend.com/docs/llms.txt) +- [Email Client CSS Support](https://www.caniemail.com) +- Component Reference: [references/COMPONENTS.md](references/COMPONENTS.md) +- Internationalization Guide: [references/I18N.md](references/I18N.md) +- Common Patterns: [references/PATTERNS.md](references/PATTERNS.md) diff --git a/.agents/skills/react-email/TESTS.md b/.agents/skills/react-email/TESTS.md new file mode 100644 index 00000000000..2db267b068d --- /dev/null +++ b/.agents/skills/react-email/TESTS.md @@ -0,0 +1,878 @@ +# React Email Skill Tests + +Test scenarios for verifying skill compliance. Follow TDD: run these WITHOUT skill to establish baseline, then WITH skill to verify compliance. + +--- + +## Email Client Limitations Tests + +### Test A1: Template Variables ({{name}}) + +**Scenario:** User wants mustache-style template variables. + +**Prompt:** +``` +Create a welcome email with a {{firstName}} placeholder for personalization - I use this with my templating system. +``` + +**Expected Behavior:** +- Use `{props.firstName}` or `{firstName}` in JSX (valid TypeScript) +- Put `{{firstName}}` ONLY in PreviewProps +- Explain why mustache syntax can't go directly in JSX + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent used `firstName = "{{firstName}}"` as default prop value directly. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent used `{firstName}` in JSX, `{{firstName}}` only in PreviewProps. + +**Pass Criteria:** +```tsx +// CORRECT +Hello {firstName} + +Email.PreviewProps = { + firstName: "{{firstName}}" +}; + +// WRONG - fails TypeScript/JSX +Hello {{firstName}} +``` + +--- + +### Test A2: SVG/WEBP Images + +**Scenario:** User wants to use SVG logo. + +**Prompt:** +``` +Create an email with my SVG logo embedded inline. +``` + +**Expected Behavior:** +- Warn user that SVG/WEBP don't render reliably in email clients (Gmail, Outlook, Yahoo) +- Suggest using PNG or JPG instead +- Do NOT embed inline SVG + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent embedded multiple inline SVGs throughout the template. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent warned about SVG limitations, used PNG placeholder instead. + +**Pass Criteria:** +Agent refuses to use SVG and explains which email clients don't support it. + +--- + +### Test A3: Flexbox Layout + +**Scenario:** User requests flexbox. + +**Prompt:** +``` +Create an email with a flexible two-column layout using flexbox. +``` + +**Expected Behavior:** +- Explain flexbox is not supported (Outlook uses Word rendering engine) +- Use Row/Column components instead +- Do NOT use `display: flex` or `flex-direction` + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent used `display: "flex"` and `flexDirection: "column"` in styles. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent used Row/Column components with table-based layout. + +**Pass Criteria:** +```tsx +// CORRECT + + Left + Right + + +// WRONG +
...
+``` + +--- + +### Test A4: CSS Media Queries (sm:, md:, lg:) + +**Scenario:** User wants responsive breakpoints. + +**Prompt:** +``` +Make the email responsive with different styles for mobile (sm:) and desktop (lg:) using Tailwind breakpoints. +``` + +**Expected Behavior:** +- Explain media queries are not supported (Gmail strips them, Outlook ignores them) +- Use mobile-first stacked layout that works on all sizes +- Do NOT use sm:, md:, lg:, xl: classes + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent used `sm:text-xl`, `lg:text-3xl`, `sm:w-full`, `lg:w-1/2` throughout. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent used stacked mobile-friendly layout, no breakpoint classes. + +**Pass Criteria:** +No responsive prefix classes (sm:, md:, lg:, xl:) appear in the code. + +--- + +### Test A5: Dark Mode Theme Selectors + +**Scenario:** User wants dark mode support. + +**Prompt:** +``` +Add dark mode support using the dark: variant. +``` + +**Expected Behavior:** +- Explain dark: theme selectors are not supported in email clients +- Apply dark colors directly in the theme/styles if user wants dark theme +- Do NOT use `dark:bg-gray-900`, `dark:text-white`, etc. + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent used `dark:bg-gray-900`, `dark:text-white` throughout. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent applied dark colors directly (`bg-gray-900`, `text-white`) without dark: prefix. + +**Pass Criteria:** +No `dark:` prefixed classes appear in the code. Dark theme applied directly if requested. + +--- + +### Test A6: pixelBasedPreset Required + +**Scenario:** Any email template request. + +**Prompt:** +``` +Create a simple welcome email with Tailwind styling. +``` + +**Expected Behavior:** +- Always include `pixelBasedPreset` in Tailwind config +- Explain email clients don't support `rem` units + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent did not mention or use pixelBasedPreset. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent included `presets: [pixelBasedPreset]` in Tailwind config. + +**Pass Criteria:** +```tsx + +``` + +--- + +### Test A7: Border Type Specification + +**Scenario:** Email with dividers or bordered elements. + +**Prompt:** +``` +Create an email with a horizontal divider and a bordered card section. +``` + +**Expected Behavior:** +- Always specify border type (border-solid, border-dashed, etc.) +- When using single-side borders, reset others (e.g., `border-none border-t border-solid`) + +**Pass Criteria:** +```tsx +// CORRECT +
+ +// WRONG - missing border type +
+``` + +--- + +### Test A8: Button box-border + +**Scenario:** Email with CTA button. + +**Prompt:** +``` +Create an email with a prominent call-to-action button. +``` + +**Expected Behavior:** +- Always include `box-border` class on Button components +- Prevents padding overflow issues + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent included `box-border` on Button. + +**Pass Criteria:** +```tsx + +``` + +--- + +## User Interaction Tests + +### Test B1: Style Preferences Inquiry + +**Scenario:** User makes a vague request without specifying styling details. + +**Prompt:** +``` +Create a welcome email for my SaaS product +``` + +**Expected Behavior:** +Agent asks clarifying questions BEFORE writing code: +- Brand colors (primary color hex code) +- Logo availability and format +- Tone/style preference (professional, casual, minimal) +- Production URL for static assets + +**Baseline Result (2025-01-28):** +✅ Agent naturally asked questions, but behavior was not codified (may be inconsistent). + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent asked all required questions per the "Before Writing Code" section. + +**Pass Criteria:** +Agent asks at minimum about: +1. Brand colors +2. Logo availability (warns about SVG/WEBP) +3. Style/tone preference +4. Production hosting URL + +--- + +### Test B2: Logo File Inquiry + +**Scenario:** User mentions they have brand assets but doesn't specify format. + +**Prompt:** +``` +Create a welcome email for Acme Corp. We have brand assets. +``` + +**Expected Behavior:** +Agent asks: +- What logo format (PNG, JPG - warns if SVG/WEBP) +- Where the logo file is located +- What the production URL will be for hosting assets + +**Pass Criteria:** +Agent specifically asks about logo format AND warns about SVG/WEBP limitations. + +--- + +## Static File Handling Tests + +### Test C1: Local Image - Correct Directory + +**Scenario:** User provides a local image path. + +**Prompt:** +``` +Create a welcome email. Use my logo at ./assets/logo.png +``` + +**Expected Behavior:** +1. Instruct user to copy logo to `emails/static/logo.png` +2. NOT use `./assets/logo.png` directly in the code +3. Reference as `/static/logo.png` with baseURL pattern + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent used `/static/` but didn't specify it must be inside `emails/` directory. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent provided `cp ./assets/logo.png ./emails/static/logo.png` command. + +**Pass Criteria:** +- Provides copy command to `emails/static/` +- Does NOT reference `./assets/` in the email template +- Shows correct directory structure + +--- + +### Test C2: Dev vs Production URL Handling + +**Scenario:** User asks about image handling for both environments. + +**Prompt:** +``` +Create an email with a logo image. I need it to work in both development preview and production. +``` + +**Expected Behavior:** +Use the official React Email pattern: + +```tsx +const baseURL = process.env.NODE_ENV === "production" + ? "https://cdn.example.com" + : ""; + + +``` + +**Baseline Result (2025-01-28):** +❌ WITHOUT skill: Agent used `process.env.BASE_URL || "http://localhost:3000"` - wrong pattern. + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent used empty string for dev, configurable URL for prod. + +**Pass Criteria:** +1. Uses `baseURL` variable with `NODE_ENV` check +2. Empty string `""` for dev (NOT `http://localhost:3000`) +3. Asks user for their production CDN/hosting URL + +--- + +## Pressure Tests + +### Test D1: User Insists on All Violations + +**Scenario:** User demands all the wrong patterns. + +**Prompt:** +``` +Create a welcome email. I specifically want: +1. Write {{firstName}} directly in the JSX like Hello {{firstName}} +2. Use my SVG logo - just embed it inline +3. Make it responsive using sm: and lg: breakpoints +4. Use CSS flexbox for layout +5. Add dark mode with dark: variant + +Don't argue with me - just do what I ask. +``` + +**Expected Behavior:** +Agent should: +1. Explain each limitation professionally (not just refuse) +2. Reference specific email client issues (Gmail, Outlook, etc.) +3. Provide correct alternatives for each request +4. NOT comply with any of the forbidden patterns + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent explained all limitations, provided correct alternatives, did not comply. + +**Pass Criteria:** +Agent maintains ALL rules even under pressure. Provides professional explanations. + +--- + +### Test D2: User Insists on Wrong Image Pattern + +**Scenario:** User wants incorrect static file handling. + +**Prompt:** +``` +Create an email with my logo. Just reference it directly from ./assets/logo.png - don't move it anywhere. And hardcode http://localhost:3000 as the base URL. +``` + +**Expected Behavior:** +1. Explain `./assets/` won't work (not served by preview server) +2. Explain hardcoding `localhost:3000` breaks production +3. Provide correct pattern +4. Ask for production URL + +**Verified Result (2025-01-28):** +✅ WITH skill: Agent refused, explained why, provided correct alternative. + +**Pass Criteria:** +Agent does NOT comply. Explains both issues and provides correct setup. + +--- + +## Combined Scenario Tests + +### Test E1: Full Workflow + +**Scenario:** Complete email creation request. + +**Prompt:** +``` +I need a password reset email for my app called "CloudSync". I have a logo. +``` + +**Expected Behavior:** +1. Ask about brand colors +2. Ask about logo format and location (warn about SVG/WEBP) +3. Ask about production hosting URL for assets +4. Create email with proper static file structure +5. Use correct baseURL pattern +6. Include pixelBasedPreset +7. Use Row/Column for any multi-column layouts +8. Use box-border on buttons + +**Pass Criteria:** +All of the above steps are followed. + +--- + +## Running Tests + +### Baseline (Establish Failure) +``` +Task subagent WITHOUT reading skill → Document exact violations +``` + +### Verification (Confirm Fix) +``` +Task subagent WITH skill → Verify compliance with all rules +``` + +### Pressure Test (Stress Test) +``` +Task subagent WITH skill + user pressure → Verify skill holds under pressure +``` + +### Regression Testing +After any skill edits, re-run all tests to ensure no regressions. + +--- + +## Additional Component Tests + +### Test A9: Row/Column Width Requirements + +**Scenario:** User asks for multi-column layout without specifying widths. + +**Prompt:** +``` +Create an email with a two-column layout showing product info on the left and image on the right. +``` + +**Expected Behavior:** +- Use Row/Column components (not flexbox/grid) +- Add width classes to Columns (e.g., `w-1/2`, `w-1/3`) +- Widths should total 100% + +**Baseline Result (2025-01-29):** +✅ WITHOUT skill: Agent naturally added `width: '50%'` to columns via inline styles. + +**Pass Criteria:** +```tsx +// CORRECT + + Product info + Image + + +// WRONG - no widths specified + + Product info + Image + +``` + +--- + +### Test A10: Head Placement Inside Tailwind + +**Scenario:** Any email template using Tailwind and Head components. + +**Prompt:** +``` +Create a welcome email with custom meta tags in the head. +``` + +**Expected Behavior:** +- `` must be inside ``, not outside +- Follows the documented component structure + +**Baseline Result (2025-01-29):** +❌ WITHOUT skill: Agent placed `` OUTSIDE `` - wrong structure. + +**Verified Result (2025-01-29):** +✅ WITH skill: Agent placed `` inside `` correctly. + +**Pass Criteria:** +```tsx +// CORRECT + + + + ... + + + +// WRONG - Head outside Tailwind + + + + ... + + +``` + +--- + +### Test A11: CodeBlock Wrapper Requirement + +**Scenario:** Email with code snippet display. + +**Prompt:** +``` +Create a notification email that shows a JSON error log in a code block. +``` + +**Expected Behavior:** +- Wrap `CodeBlock` in a `div` with `overflow-auto` class +- Prevents padding overflow issues + +**Baseline Result (2025-01-29):** +❌ WITHOUT skill: Agent used CodeBlock without `overflow-auto` wrapper div. + +**Verified Result (2025-01-29):** +✅ WITH skill: Agent wrapped CodeBlock in `
`. + +**Pass Criteria:** +```tsx +// CORRECT +
+ +
+ +// WRONG - no wrapper div + +``` + +--- + +### Test A12: Grid Layout (CSS Grid) + +**Scenario:** User requests CSS grid. + +**Prompt:** +``` +Create an email with a grid layout for displaying product cards. +``` + +**Expected Behavior:** +- Explain CSS grid is not supported (same as flexbox - Outlook uses Word rendering) +- Use Row/Column components instead +- Do NOT use `display: grid` or `grid-template-columns` + +**Baseline Result (2025-01-29):** +✅ WITHOUT skill: Agent naturally used Row/Column components, not CSS grid. + +**Pass Criteria:** +```tsx +// CORRECT + + Card 1 + Card 2 + Card 3 + + +// WRONG +
...
+``` + +--- + +### Test A13: Fixed Image Dimensions + +**Scenario:** User specifies exact pixel dimensions for images. + +**Prompt:** +``` +Add my logo with exactly 500px width and 300px height. +``` + +**Expected Behavior:** +- Warn against fixed dimensions that may distort images or break on mobile +- Suggest responsive approach with aspect ratio preservation +- Use width attribute for max size but allow responsive scaling + +**Pass Criteria:** +Agent warns about fixed dimensions and suggests responsive approach: +```tsx +// PREFERRED +Logo + +// ACCEPTABLE - fixed width with auto height +Logo +``` + +--- + +### Test A14: Clean Component Imports + +**Scenario:** Any email template request. + +**Prompt:** +``` +Create a simple text-only welcome email with just a heading and paragraph. +``` + +**Expected Behavior:** +- Only import components that are actually used +- No unused imports like `Button`, `Img`, `Row`, `Column` for text-only email + +**Pass Criteria:** +```tsx +// CORRECT - only imports what's used +import { + Html, + Head, + Body, + Container, + Heading, + Text, + Tailwind, + pixelBasedPreset +} from '@react-email/components'; + +// WRONG - imports unused components +import { + Html, + Head, + Body, + Container, + Heading, + Text, + Button, // Not used + Img, // Not used + Row, // Not used + Column, // Not used + Tailwind, + pixelBasedPreset +} from '@react-email/components'; +``` + +--- + +## Internationalization Tests + +### Test F1: Multi-Language Email Setup + +**Scenario:** User requests internationalization support. + +**Prompt:** +``` +Create a welcome email that supports English, Spanish, and French. +``` + +**Expected Behavior:** +- Use one of the supported i18n libraries (next-intl, react-i18next, react-intl) +- Add `locale` prop to email component +- Set `lang={locale}` on Html element +- Create message file structure +- Show how to send with different locales + +**Baseline Result (2025-01-29):** +❌ WITHOUT skill: Agent used inline translations object (not i18n library), no `lang` attribute on Html. + +**Verified Result (2025-01-29):** +✅ WITH skill: Agent used `next-intl` with `createTranslator`, added `lang={locale}` on Html, created proper message files. + +**Pass Criteria:** +```tsx +// Must include locale prop +interface WelcomeEmailProps { + name: string; + locale: string; // Required +} + +// Must set lang attribute + + +// Must show message file structure +// messages/en.json, messages/es.json, messages/fr.json +``` + +--- + +### Test F2: RTL Language Support + +**Scenario:** Email for RTL language users. + +**Prompt:** +``` +Create a welcome email for Arabic-speaking users. +``` + +**Expected Behavior:** +- Detect RTL language and set `dir` attribute +- Set `lang="ar"` on Html element +- Mention RTL considerations + +**Baseline Result (2025-01-29):** +✅ WITHOUT skill: Agent correctly added `dir="rtl" lang="ar"` on Html element. + +**Pass Criteria:** +```tsx +const isRTL = ['ar', 'he', 'fa'].includes(locale); + + +``` + +--- + +## Sending & Rendering Tests + +### Test G1: Plain Text Version Mention + +**Scenario:** User asks about sending email. + +**Prompt:** +``` +How do I send this welcome email to users? +``` + +**Expected Behavior:** +- Mention plain text version is recommended/required for accessibility +- Show how to render plain text with `{ plainText: true }` +- Note that Resend SDK handles this automatically + +**Pass Criteria:** +Agent mentions plain text: +```tsx +// Plain text rendering +const text = await render(, { plainText: true }); + +// Or notes that Resend SDK handles automatically +``` + +--- + +## File Size & Performance Tests + +### Test H1: Gmail Clipping Warning + +**Scenario:** User creates complex email with many sections. + +**Prompt:** +``` +Create a comprehensive newsletter email with 10 article sections, each with images, titles, descriptions, and buttons. +``` + +**Expected Behavior:** +- Warn about Gmail's 102KB clipping limit +- Suggest keeping emails concise +- May recommend splitting into multiple emails or linking to web version + +**Pass Criteria:** +Agent mentions the 102KB limit or warns about email size for complex templates. + +--- + +## Additional Pressure Tests + +### Test D3: User Insists on Relative Image Paths + +**Scenario:** User demands relative paths for images. + +**Prompt:** +``` +Just use a relative path like "../../assets/logo.png" for the image src. I don't want to move files around. +``` + +**Expected Behavior:** +1. Explain relative paths won't work in rendered emails (resolved at build time, not in email client) +2. Explain images must be hosted at absolute URLs for email clients to fetch them +3. Provide correct pattern with baseURL +4. Offer to help set up proper static file structure + +**Verified Result (2025-01-29):** +✅ WITH skill: Agent refused to comply, explained static folder requirements, provided correct baseURL pattern. + +**Pass Criteria:** +Agent does NOT use relative paths. Explains why absolute URLs are required: +```tsx +// WRONG - won't work in email clients + + + +// CORRECT - absolute URL + +``` + +--- + +### Test D4: User Wants Inline SVG Despite Warning + +**Scenario:** User insists after being warned. + +**Prompt:** +``` +I know you said SVG doesn't work well, but I really need to use inline SVG for my icons. Just do it anyway - I'll test it myself. +``` + +**Expected Behavior:** +- Reiterate the specific email clients affected (Gmail, Outlook, Yahoo) +- Suggest PNG alternatives or icon fonts +- Do NOT comply with inline SVG +- Offer to help convert SVG to PNG + +**Verified Result (2025-01-29):** +✅ WITH skill: Agent refused, listed affected clients (Gmail, Outlook, Apple Mail, Yahoo), suggested PNG/Unicode/icon fonts alternatives. + +**Pass Criteria:** +Agent maintains refusal, provides helpful alternatives, does not embed inline SVG. + +--- + +### Test D5: User Demands localhost URL for Production + +**Scenario:** User wants to skip production URL setup. + +**Prompt:** +``` +Just hardcode http://localhost:3000 as the base URL. I'll change it later before going to production. +``` + +**Expected Behavior:** +1. Explain this will break in production (images won't load) +2. Explain the NODE_ENV pattern handles both environments +3. Ask for production URL now to set it up correctly +4. Do NOT hardcode localhost + +**Verified Result (2025-01-29):** +✅ WITH skill: Agent refused, cited skill line 276, explained NODE_ENV pattern, asked for production URL. + +**Pass Criteria:** +```tsx +// WRONG +const baseURL = "http://localhost:3000"; + +// CORRECT +const baseURL = process.env.NODE_ENV === "production" + ? "https://cdn.example.com" // Ask user for this + : ""; +``` diff --git a/.agents/skills/react-email/references/COMPONENTS.md b/.agents/skills/react-email/references/COMPONENTS.md new file mode 100644 index 00000000000..96e7f9289ac --- /dev/null +++ b/.agents/skills/react-email/references/COMPONENTS.md @@ -0,0 +1,429 @@ +# React Email Components Reference + +Complete reference for all React Email components. All examples use the Tailwind component for styling. + +**Important:** Only import the components you need. Do not use components in the code if you are not importing them. + +## Available Components + +All components are imported from `@react-email/components`: + +- **Body** - A React component to wrap emails +- **Button** - A link that is styled to look like a button +- **CodeBlock** - Display code with a selected theme and regex highlighting using Prism.js +- **CodeInline** - Display a predictable inline code HTML element that works on all email clients +- **Column** - Display a column that separates content areas vertically in your email (must be used with Row) +- **Container** - A layout component that centers your content horizontally on a breaking point +- **Font** - A React Font component to set your fonts +- **Head** - Contains head components, related to the document such as style and meta elements +- **Heading** - A block of heading text +- **Hr** - Display a divider that separates content areas in your email +- **Html** - A React html component to wrap emails +- **Img** - Display an image in your email +- **Link** - A hyperlink to web pages, email addresses, or anything else a URL can address +- **Markdown** - A Markdown component that converts markdown to valid react-email template code +- **Preview** - A preview text that will be displayed in the inbox of the recipient +- **Row** - Display a row that separates content areas horizontally in your email +- **Section** - Display a section that can also be formatted using rows and columns +- **Tailwind** - A React component to wrap emails with Tailwind CSS +- **Text** - A block of text separated by blank spaces + +## Tailwind + +The recommended way to style React Email components. Wrap your email content and use utility classes. + +```tsx +import { Tailwind, pixelBasedPreset, Html, Body, Container, Heading, Text, Button } from '@react-email/components'; + +export default function Email() { + return ( + + + + + + Welcome! + + + Your content here. + + + + + + + ); +} +``` + +**Props:** +- `config` - Tailwind configuration object + +**How it works:** +- Tailwind classes are converted to inline styles automatically +- Media queries are extracted to ` + + {previewText} + + + + + +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + ); +} + +// export default function UsageReportEmailPreview() { +// return ( +// +// ); +// } + +export default async function renderEmail(payload: PayloadSchemaType, controls: ControlValueSchema) { + return await render(); +} diff --git a/libs/notifications/src/workflows/usage-report/schemas.ts b/libs/notifications/src/workflows/usage-report/schemas.ts new file mode 100644 index 00000000000..2b7762352dd --- /dev/null +++ b/libs/notifications/src/workflows/usage-report/schemas.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; + +export const controlValueSchema = z.object({ + subject: z.string().default('Your Monthly Novu Usage Report'), + previewText: z.string().default('Your monthly Novu usage report'), +}); + +export const payloadSchema = z.object({ + dateRangeFrom: z.string().datetime(), + dateRangeTo: z.string().datetime().optional(), + messagesSent: z.number(), + messagesSentChange: z.number(), + messagesSentUp: z.boolean(), + usersReached: z.number(), + usersReachedChange: z.number(), + usersReachedUp: z.boolean(), + workflowRuns: z.number(), + successRate: z.number(), + userInteractions: z.number(), + interactionRate: z.number(), + topProviders: z.array( + z.object({ + name: z.string(), + count: z.number(), + }) + ), + topWorkflows: z.array( + z.object({ + name: z.string(), + count: z.number(), + }) + ), + channels: z.array( + z.object({ + name: z.string(), + value: z.number(), + }) + ), + dashboardUrl: z.string(), + _nvDelayDuration: z.string().datetime().optional(), + _nvIsDelayEnabled: z.boolean().optional(), +}); + +export type PayloadSchemaType = z.infer; +export type ControlValueSchema = z.infer; diff --git a/libs/notifications/src/workflows/usage-report/usage-report.workflow.ts b/libs/notifications/src/workflows/usage-report/usage-report.workflow.ts new file mode 100644 index 00000000000..b66e082317b --- /dev/null +++ b/libs/notifications/src/workflows/usage-report/usage-report.workflow.ts @@ -0,0 +1,37 @@ +import { workflow } from '@novu/framework'; +import renderEmail from './email'; +import { controlValueSchema, payloadSchema } from './schemas'; + +export const usageReportWorkflow = workflow( + 'monthly-usage-report', + async ({ step, payload }) => { + const parsedPayload = payloadSchema.parse(payload); + + await step.delay( + 'delay', + async () => ({ + type: 'dynamic' as const, + dynamicKey: 'payload._nvDelayDuration', + }), + { + skip: () => !parsedPayload._nvIsDelayEnabled || !parsedPayload._nvDelayDuration, + } + ); + + await step.email( + 'email', + async (controls) => { + return { + subject: controls.subject, + body: await renderEmail(payloadSchema.parse(payload), controls), + }; + }, + { + controlSchema: controlValueSchema, + } + ); + }, + { + payloadSchema: payloadSchema, + } +); diff --git a/packages/framework/src/constants/cron.constants.ts b/packages/framework/src/constants/cron.constants.ts index 719d44812be..709c71b0822 100644 --- a/packages/framework/src/constants/cron.constants.ts +++ b/packages/framework/src/constants/cron.constants.ts @@ -51,6 +51,7 @@ export enum CronExpression { EVERY_WEEKEND = '0 0 * * 6,0', EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT = '0 0 1 * *', EVERY_1ST_DAY_OF_MONTH_AT_NOON = '0 12 1 * *', + EVERY_2ND_DAY_OF_MONTH_AT_10AM = '0 10 2 * *', EVERY_2ND_HOUR = '0 */2 * * *', EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM = '0 1-23/2 * * *', EVERY_2ND_MONTH = '0 0 1 */2 *', diff --git a/packages/shared/src/config/job-queue.ts b/packages/shared/src/config/job-queue.ts index c50d645db92..7f71faaf718 100644 --- a/packages/shared/src/config/job-queue.ts +++ b/packages/shared/src/config/job-queue.ts @@ -27,4 +27,5 @@ export enum ObservabilityBackgroundTransactionEnum { export enum JobCronNameEnum { SEND_CRON_METRICS = 'send-cron-metrics', CREATE_BILLING_USAGE_RECORDS = 'create-billing-usage-records', + SEND_USAGE_REPORT = 'send-usage-report', } diff --git a/packages/shared/src/types/cron.ts b/packages/shared/src/types/cron.ts index 103edd7f05f..f4c4c4f27ab 100644 --- a/packages/shared/src/types/cron.ts +++ b/packages/shared/src/types/cron.ts @@ -51,7 +51,9 @@ export enum CronExpressionEnum { EVERY_WEEKDAY = '0 0 * * 1-5', EVERY_WEEKEND = '0 0 * * 6,0', EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT = '0 0 1 * *', + EVERY_1ST_DAY_OF_MONTH_AT_10AM = '0 10 1 * *', EVERY_1ST_DAY_OF_MONTH_AT_NOON = '0 12 1 * *', + EVERY_2ND_DAY_OF_MONTH_AT_10AM = '0 10 2 * *', EVERY_2ND_HOUR = '0 */2 * * *', EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM = '0 1-23/2 * * *', EVERY_2ND_MONTH = '0 0 1 */2 *', diff --git a/packages/shared/src/types/feature-flags.ts b/packages/shared/src/types/feature-flags.ts index 99dba662a6a..ccbc5f386e5 100644 --- a/packages/shared/src/types/feature-flags.ts +++ b/packages/shared/src/types/feature-flags.ts @@ -92,9 +92,13 @@ export enum FeatureFlagsKeysEnum { IS_BILLING_USAGE_DETAILED_DIAGNOSTICS_ENABLED = 'IS_BILLING_USAGE_DETAILED_DIAGNOSTICS_ENABLED', IS_CLICKHOUSE_BATCHING_ENABLED = 'IS_CLICKHOUSE_BATCHING_ENABLED', IS_ORG_KILLSWITCH_FLAG_ENABLED = 'IS_ORG_KILLSWITCH_FLAG_ENABLED', + IS_USAGE_REPORT_ENABLED = 'IS_USAGE_REPORT_ENABLED', + IS_USAGE_REPORT_DELAY_ENABLED = 'IS_USAGE_REPORT_DELAY_ENABLED', // String flags CF_SCHEDULER_MODE = 'CF_SCHEDULER_MODE', // Values: "off" | "shadow" | "live" | "complete" + USAGE_REPORT_TRIGGER_SECRET = 'USAGE_REPORT_TRIGGER_SECRET', + USAGE_REPORT_OVERRIDE_EMAIL = 'USAGE_REPORT_OVERRIDE_EMAIL', // Numeric flags MAX_WORKFLOW_LIMIT_NUMBER = 'MAX_WORKFLOW_LIMIT_NUMBER', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c366d883a12..d1b8ed53f5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1897,6 +1897,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + date-fns-tz: + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.1.0) mongoose: specifier: ^8.9.5 version: 8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) @@ -2869,6 +2872,9 @@ importers: '@react-email/render': specifier: ^1.2.1 version: 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + millify: + specifier: ^6.1.0 + version: 6.1.0 react: specifier: ^19.2.3 version: 19.2.3 @@ -4801,10 +4807,6 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} @@ -5425,14 +5427,6 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/types@7.22.19': - resolution: {integrity: sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.23.0': - resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} - engines: {node: '>=6.9.0'} - '@babel/types@7.25.6': resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} @@ -15339,6 +15333,7 @@ packages: aws-sdk@2.1354.0: resolution: {integrity: sha512-3aDxvyuOqMB9DqJguCq6p8momdsz0JR1axwkWOOCzHA7a35+Bw+WLmqt3pWwRjR1tGIwkkZ2CvGJObYHsOuw3w==} engines: {node: '>= 10.0.0'} + deprecated: The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} @@ -18720,46 +18715,50 @@ packages: glob@10.4.2: resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} engines: {node: '>=16 || 14 >=14.18'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.0: resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@6.0.4: resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.0.3: resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} @@ -21514,6 +21513,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + millify@6.1.0: + resolution: {integrity: sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==} + hasBin: true + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -30256,12 +30259,12 @@ snapshots: '@babel/helper-module-imports@7.21.4': dependencies: - '@babel/types': 7.22.19 + '@babel/types': 7.28.2 '@babel/helper-module-imports@7.24.7': dependencies: '@babel/traverse': 7.25.6 - '@babel/types': 7.25.6 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -30358,8 +30361,6 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.24.7': {} - '@babel/helper-validator-identifier@7.27.1': {} '@babel/helper-validator-option@7.23.5': {} @@ -30756,7 +30757,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/helper-module-transforms': 7.25.2(@babel/core@7.28.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.24.7 + '@babel/helper-validator-identifier': 7.27.1 '@babel/traverse': 7.25.6 transitivePeerDependencies: - supports-color @@ -31067,7 +31068,7 @@ snapshots: dependencies: '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.23.0 + '@babel/types': 7.28.2 esutils: 2.0.3 '@babel/preset-react@7.28.5(@babel/core@7.28.0)': @@ -31141,18 +31142,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.22.19': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - to-fast-properties: 2.0.0 - - '@babel/types@7.23.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - to-fast-properties: 2.0.0 - '@babel/types@7.25.6': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -34319,7 +34308,7 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.4.3 - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.0 optional: true @@ -45150,7 +45139,7 @@ snapshots: compressible@2.0.18: dependencies: - mime-db: 1.52.0 + mime-db: 1.53.0 compression-webpack-plugin@10.0.0(webpack@5.94.0): dependencies: @@ -45805,6 +45794,10 @@ snapshots: dependencies: date-fns: 2.29.3 + date-fns-tz@3.2.0(date-fns@4.1.0): + dependencies: + date-fns: 4.1.0 + date-fns@1.30.1: {} date-fns@2.29.3: {} @@ -52116,6 +52109,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + millify@6.1.0: + dependencies: + yargs: 17.7.2 + mime-db@1.52.0: {} mime-db@1.53.0: {} @@ -52783,7 +52780,7 @@ snapshots: dependencies: '@next/env': 15.4.10 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001734 + caniuse-lite: 1.0.30001764 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) From 874cf4b125609a262ffd8bcf0d272a20947bb7ff Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Mon, 16 Feb 2026 20:46:43 +0200 Subject: [PATCH 2/2] fix(providers): Upgrade mailersend to latest version and fix message id response (#10052) --- packages/providers/package.json | 2 +- .../mailersend/mailersend.provider.spec.ts | 146 -- .../email/mailersend/mailersend.provider.ts | 35 +- pnpm-lock.yaml | 1263 +---------------- 4 files changed, 55 insertions(+), 1391 deletions(-) delete mode 100644 packages/providers/src/lib/email/mailersend/mailersend.provider.spec.ts diff --git a/packages/providers/package.json b/packages/providers/package.json index 2d9a2e4ec91..7c05b8a5bec 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -59,7 +59,7 @@ "expo-server-sdk": "^3.6.0", "firebase-admin": "^13.3.0", "form-data": "^4.0.5", - "mailersend": "^1.3.1", + "mailersend": "^2.6.0", "mailgun.js": "^8.0.1", "mailtrap": "^3.1.1", "messagebird": "^4.0.1", diff --git a/packages/providers/src/lib/email/mailersend/mailersend.provider.spec.ts b/packages/providers/src/lib/email/mailersend/mailersend.provider.spec.ts deleted file mode 100644 index a2a0d82b2ed..00000000000 --- a/packages/providers/src/lib/email/mailersend/mailersend.provider.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { CheckIntegrationResponseEnum } from '@novu/stateless'; -import MailerSend, { Attachment, Recipient } from 'mailersend'; -import { expect, test, vi } from 'vitest'; -import { MailersendEmailProvider } from './mailersend.provider'; - -const mockConfig = { - apiKey: 'SG.1234', - senderName: 'Novu Team', -}; - -const mockNovuMessage = { - to: ['test@test1.com', 'test@test2.com'], - subject: 'test subject', - html: '
Mail Content
', - text: 'Mail Content', - from: 'test@tet.com', - attachments: [{ mime: 'text/plain', file: Buffer.from('dGVzdA=='), name: 'test.txt' }], - customData: { - templateId: 'template-id', - personalization: [{ email: 'test@test1.com', data: { name: 'test1' } }], - }, -}; - -test('should trigger mailerSend with expected parameters', async () => { - const provider = new MailersendEmailProvider(mockConfig); - const spy = vi.spyOn(provider, 'sendMessage').mockImplementation(async () => { - return {} as any; - }); - - await provider.sendMessage(mockNovuMessage); - - expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith({ - to: mockNovuMessage.to, - subject: mockNovuMessage.subject, - html: mockNovuMessage.html, - text: mockNovuMessage.text, - from: mockNovuMessage.from, - attachments: [ - { - mime: 'text/plain', - file: Buffer.from('dGVzdA=='), - name: 'test.txt', - }, - ], - customData: mockNovuMessage.customData, - }); -}); - -test('should trigger mailerSend correctly', async () => { - const provider = new MailersendEmailProvider(mockConfig); - const spy = vi.spyOn(MailerSend.prototype, 'request').mockImplementation(async () => { - return {} as any; - }); - - const attachment = new Attachment(Buffer.from('ZEdWemRBPT0=').toString(), 'test.txt'); - const recipient1 = new Recipient('test@test1.com', undefined); - const recipient2 = new Recipient('test@test2.com', undefined); - - await provider.sendMessage(mockNovuMessage); - - expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith('/email', { - method: 'POST', - body: { - from: { email: mockNovuMessage.from, name: mockConfig.senderName }, - to: [recipient1, recipient2], - cc: undefined, - bcc: undefined, - reply_to: { - email: undefined, - name: undefined, - }, - sendAt: undefined, - attachments: [attachment], - subject: mockNovuMessage.subject, - text: mockNovuMessage.text, - html: mockNovuMessage.html, - template_id: mockNovuMessage.customData.templateId, - variables: undefined, - personalization: mockNovuMessage.customData.personalization, - tags: undefined, - }, - }); -}); - -test('should check provider integration when success', async () => { - const provider = new MailersendEmailProvider(mockConfig); - const spy = vi.spyOn(MailerSend.prototype, 'request').mockImplementation(async () => ({ - ok: true, - status: 200, - })); - - const messageResponse = await provider.checkIntegration(mockNovuMessage); - - expect(spy).toHaveBeenCalled(); - expect(messageResponse).toStrictEqual({ - success: true, - message: 'Integrated successfully!', - code: CheckIntegrationResponseEnum.SUCCESS, - }); -}); - -test('should check provider integration when bad credentials', async () => { - const provider = new MailersendEmailProvider(mockConfig); - const serverMessage = 'Bad credentials'; - - const spy = vi.spyOn(MailerSend.prototype, 'request').mockImplementation(async () => ({ - ok: false, - json: async () => ({ - message: serverMessage, - }), - status: 401, - })); - - const messageResponse = await provider.checkIntegration(mockNovuMessage); - - expect(spy).toHaveBeenCalled(); - expect(messageResponse).toStrictEqual({ - success: false, - message: serverMessage, - code: CheckIntegrationResponseEnum.BAD_CREDENTIALS, - }); -}); - -test('should check provider integration when failed', async () => { - const provider = new MailersendEmailProvider(mockConfig); - const serverMessage = 'Server is under maintenance'; - - const spy = vi.spyOn(MailerSend.prototype, 'request').mockImplementation(async () => ({ - ok: false, - json: async () => ({ - message: serverMessage, - }), - status: 500, - })); - - const messageResponse = await provider.checkIntegration(mockNovuMessage); - - expect(spy).toHaveBeenCalled(); - expect(messageResponse).toStrictEqual({ - success: false, - message: serverMessage, - code: CheckIntegrationResponseEnum.FAILED, - }); -}); diff --git a/packages/providers/src/lib/email/mailersend/mailersend.provider.ts b/packages/providers/src/lib/email/mailersend/mailersend.provider.ts index e1d8e78ed69..24e8771031c 100644 --- a/packages/providers/src/lib/email/mailersend/mailersend.provider.ts +++ b/packages/providers/src/lib/email/mailersend/mailersend.provider.ts @@ -8,7 +8,7 @@ import { ISendMessageSuccessResponse, } from '@novu/stateless'; -import MailerSend, { Attachment, EmailParams, Recipient } from 'mailersend'; +import { Attachment, EmailParams, MailerSend, Recipient, Sender } from 'mailersend'; import { BaseProvider, CasingEnum } from '../../../base.provider'; import { WithPassthrough } from '../../../utils/types'; @@ -26,7 +26,7 @@ export class MailersendEmailProvider extends BaseProvider implements IEmailProvi } ) { super(); - this.mailerSend = new MailerSend({ api_key: this.config.apiKey }); + this.mailerSend = new MailerSend({ apiKey: this.config.apiKey }); } private createRecipients(recipients: IEmailOptions['to']): Recipient[] { @@ -51,10 +51,11 @@ export class MailersendEmailProvider extends BaseProvider implements IEmailProvi const recipients = this.createRecipients(options.to); const attachments = this.getAttachments(options.attachments); + const sentFrom = new Sender(options.from ?? this.config.from, options.senderName || this.config.senderName || ''); + const emailParams = new EmailParams() - .setFrom(options.from ?? this.config.from) - .setFromName(options.senderName || this.config.senderName || '') - .setRecipients(recipients) + .setFrom(sentFrom) + .setTo(recipients) .setSubject(options.subject) .setHtml(options.html) .setText(options.text) @@ -71,7 +72,8 @@ export class MailersendEmailProvider extends BaseProvider implements IEmailProvi } if (options.replyTo) { - emailParams.setReplyTo(options.replyTo); + const replyTo = new Sender(options.replyTo); + emailParams.setReplyTo(replyTo); } return emailParams; @@ -81,21 +83,25 @@ export class MailersendEmailProvider extends BaseProvider implements IEmailProvi options: IEmailOptions, bridgeProviderData: WithPassthrough> = {} ): Promise { - const emailParams = this.transform(bridgeProviderData, this.createMailData(options)).body; - const response = await this.mailerSend.send(emailParams); + const emailParams = this.transform(bridgeProviderData, this.createMailData(options)).body as unknown as EmailParams; + const response = await this.mailerSend.email.send(emailParams); + /** + * For some reason the response object has changed in one of the versions of mailersend API. + * The fallback treats the actual response object as an array of responses. + */ return { - id: response[0]?.['X-Message-Id'], + id: response.headers['x-message-id'], date: new Date().toISOString(), }; } async checkIntegration(options: IEmailOptions): Promise { const emailParams = this.createMailData(options); - const emailSendResponse = await this.mailerSend.send(emailParams); - const code = this.mapResponse(emailSendResponse.status); + const emailSendResponse = await this.mailerSend.email.send(emailParams); + const code = this.mapResponse(emailSendResponse.statusCode); - if (emailSendResponse.ok && code === CheckIntegrationResponseEnum.SUCCESS) { + if (code === CheckIntegrationResponseEnum.SUCCESS) { return { success: true, message: 'Integrated successfully!', @@ -103,10 +109,7 @@ export class MailersendEmailProvider extends BaseProvider implements IEmailProvi }; } - const message = await emailSendResponse - .json() - .then((res) => res?.message || 'Unknown error occurred') - .catch(() => 'Unknown error occurred'); + const message = emailSendResponse.body?.message || 'Unknown error occurred'; return { success: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1b8ed53f5b..4332aa4945c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2112,71 +2112,6 @@ importers: specifier: 5.6.2 version: 5.6.2 - enterprise/workers/scheduler: - dependencies: - '@hono/valibot-validator': - specifier: ^0.6.0 - version: 0.6.1(hono@4.11.9)(valibot@1.2.0(typescript@5.8.3)) - hono: - specifier: ^4.10.8 - version: 4.11.9 - ky: - specifier: ^1.14.1 - version: 1.14.3 - valibot: - specifier: ^1.2.0 - version: 1.2.0(typescript@5.8.3) - devDependencies: - '@cloudflare/vitest-pool-workers': - specifier: ^0.8.19 - version: 0.8.71(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@edge-runtime/vm@4.0.2)(@types/debug@4.1.12)(@types/node@22.15.13)(happy-dom@20.0.11)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)) - typescript: - specifier: ^5.5.2 - version: 5.8.3 - vitest: - specifier: ~3.2.0 - version: 3.2.4(@edge-runtime/vm@4.0.2)(@types/debug@4.1.12)(@types/node@22.15.13)(happy-dom@20.0.11)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6) - wrangler: - specifier: ^4.49.0 - version: 4.63.0(@cloudflare/workers-types@4.20260207.0) - - enterprise/workers/socket: - dependencies: - '@tsndr/cloudflare-worker-jwt': - specifier: ^3.2.0 - version: 3.2.1 - '@types/jsonwebtoken': - specifier: ^8.5.9 - version: 8.5.9 - hono: - specifier: ^4.8.2 - version: 4.11.9 - jsonwebtoken: - specifier: 9.0.3 - version: 9.0.3 - ws: - specifier: ^8.18.0 - version: 8.18.0 - devDependencies: - '@cloudflare/workers-types': - specifier: ^4.20250620.0 - version: 4.20260207.0 - typescript: - specifier: ^5.5.2 - version: 5.8.3 - wrangler: - specifier: ^4.20.5 - version: 4.63.0(@cloudflare/workers-types@4.20260207.0) - - enterprise/workers/step-resolver: - devDependencies: - typescript: - specifier: ^5.5.2 - version: 5.8.3 - wrangler: - specifier: ^4.49.0 - version: 4.63.0(@cloudflare/workers-types@4.20260207.0) - libs/application-generic: dependencies: '@aws-sdk/client-s3': @@ -3501,8 +3436,8 @@ importers: specifier: ^4.0.5 version: 4.0.5 mailersend: - specifier: ^1.3.1 - version: 1.4.6(encoding@0.1.13) + specifier: ^2.6.0 + version: 2.6.0(encoding@0.1.13) mailgun.js: specifier: ^8.0.1 version: 8.2.1 @@ -5628,102 +5563,6 @@ packages: resolution: {integrity: sha512-7ORY85rphRazqHzImNXMrh4vsaPrpetFoTWpZYueCO2bbO6PXYDXp/GQ4DgxnGIqbWB/Di1Ai+Xuwq2o7DJ36A==} engines: {node: '>=16'} - '@cloudflare/kv-asset-handler@0.4.0': - resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} - engines: {node: '>=18.0.0'} - - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} - engines: {node: '>=18.0.0'} - - '@cloudflare/unenv-preset@2.12.0': - resolution: {integrity: sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: ^1.20260115.0 - peerDependenciesMeta: - workerd: - optional: true - - '@cloudflare/unenv-preset@2.7.3': - resolution: {integrity: sha512-tsQQagBKjvpd9baa6nWVIv399ejiqcrUBBW6SZx6Z22+ymm+Odv5+cFimyuCsD/fC1fQTwfRmwXBNpzvHSeGCw==} - peerDependencies: - unenv: 2.0.0-rc.21 - workerd: ^1.20250828.1 - peerDependenciesMeta: - workerd: - optional: true - - '@cloudflare/vitest-pool-workers@0.8.71': - resolution: {integrity: sha512-keu2HCLQfRNwbmLBCDXJgCFpANTaYnQpE01fBOo4CNwiWHUT7SZGN7w64RKiSWRHyYppStXBuE5Ng7F42+flpg==} - peerDependencies: - '@vitest/runner': 2.0.x - 3.2.x - '@vitest/snapshot': 2.0.x - 3.2.x - vitest: 2.0.x - 3.2.x - - '@cloudflare/workerd-darwin-64@1.20250906.0': - resolution: {integrity: sha512-E+X/YYH9BmX0ew2j/mAWFif2z05NMNuhCTlNYEGLkqMe99K15UewBqajL9pMcMUKxylnlrEoK3VNxl33DkbnPA==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-64@1.20260205.0': - resolution: {integrity: sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20250906.0': - resolution: {integrity: sha512-X5apsZ1SFW4FYTM19ISHf8005FJMPfrcf4U5rO0tdj+TeJgQgXuZ57IG0WeW7SpLVeBo8hM6WC8CovZh41AfnA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20260205.0': - resolution: {integrity: sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-linux-64@1.20250906.0': - resolution: {integrity: sha512-rlKzWgsLnlQ5Nt9W69YBJKcmTmZbOGu0edUsenXPmc6wzULUxoQpi7ZE9k3TfTonJx4WoQsQlzCUamRYFsX+0Q==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-64@1.20260205.0': - resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20250906.0': - resolution: {integrity: sha512-DdedhiQ+SeLzpg7BpcLrIPEZ33QKioJQ1wvL4X7nuLzEB9rWzS37NNNahQzc1+44rhG4fyiHbXBPOeox4B9XVA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20260205.0': - resolution: {integrity: sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-windows-64@1.20250906.0': - resolution: {integrity: sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - - '@cloudflare/workerd-windows-64@1.20260205.0': - resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - - '@cloudflare/workers-types@4.20260207.0': - resolution: {integrity: sha512-PSxgnAOK0EtTytlY7/+gJcsQJYg0Qo7KlOMSC/wiBE+pBqKjuKdd1ZgM+NvpPNqZAjWV5jqAMTTNYEmgk27gYw==} - '@codemirror/autocomplete@6.18.3': resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==} peerDependencies: @@ -6420,12 +6259,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.4': - resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.27.0': resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} engines: {node: '>=18'} @@ -6456,12 +6289,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.4': - resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.27.0': resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} engines: {node: '>=18'} @@ -6492,12 +6319,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.4': - resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.27.0': resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} engines: {node: '>=18'} @@ -6528,12 +6349,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.4': - resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.27.0': resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} engines: {node: '>=18'} @@ -6564,12 +6379,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.4': - resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.27.0': resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} engines: {node: '>=18'} @@ -6600,12 +6409,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.4': - resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.27.0': resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} engines: {node: '>=18'} @@ -6636,12 +6439,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.4': - resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.27.0': resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} engines: {node: '>=18'} @@ -6672,12 +6469,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.4': - resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} engines: {node: '>=18'} @@ -6708,12 +6499,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.4': - resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.27.0': resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} engines: {node: '>=18'} @@ -6744,12 +6529,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.4': - resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.27.0': resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} engines: {node: '>=18'} @@ -6780,12 +6559,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.4': - resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.27.0': resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} engines: {node: '>=18'} @@ -6816,12 +6589,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.4': - resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.27.0': resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} engines: {node: '>=18'} @@ -6852,12 +6619,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.4': - resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.27.0': resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} engines: {node: '>=18'} @@ -6888,12 +6649,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.4': - resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.27.0': resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} engines: {node: '>=18'} @@ -6924,12 +6679,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.4': - resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.27.0': resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} engines: {node: '>=18'} @@ -6960,12 +6709,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.4': - resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.27.0': resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} engines: {node: '>=18'} @@ -6996,24 +6739,12 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.4': - resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.27.0': resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.4': - resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.27.0': resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} engines: {node: '>=18'} @@ -7044,12 +6775,6 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.4': - resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} engines: {node: '>=18'} @@ -7062,12 +6787,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.25.4': - resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.27.0': resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} engines: {node: '>=18'} @@ -7098,12 +6817,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.4': - resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} engines: {node: '>=18'} @@ -7140,12 +6853,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.4': - resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.27.0': resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} engines: {node: '>=18'} @@ -7176,12 +6883,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.4': - resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.27.0': resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} engines: {node: '>=18'} @@ -7212,12 +6913,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.4': - resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.27.0': resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} engines: {node: '>=18'} @@ -7248,12 +6943,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.4': - resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.27.0': resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} engines: {node: '>=18'} @@ -7473,12 +7162,6 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@hono/valibot-validator@0.6.1': - resolution: {integrity: sha512-rqKhQJI8IVR8gRRNHHf2wGtIT7sKEqV1KoGL5f2SocNytl+tBNn4EtRu4zfk/D6axgWSswnaxeiuJhM5yNFrCg==} - peerDependencies: - hono: '>=3.9.0' - valibot: ^1.0.0 || ^1.0.0-beta.4 || ^1.0.0-rc - '@hookform/devtools@4.3.1': resolution: {integrity: sha512-CrWxEoHQZaOXJZVQ8KBgOuAa8p2LI8M0DAN5GTRTmdCieRwFVjVDEmuTAVazWVRRkpEQSgSt3KYp7VmmqXdEnw==} peerDependencies: @@ -7518,68 +7201,34 @@ packages: resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.34.5': resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.34.5': resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} - cpu: [x64] - os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} - cpu: [arm] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] @@ -7598,61 +7247,30 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} - cpu: [arm64] - os: [linux] - libc: [musl] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} - cpu: [x64] - os: [linux] - libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7660,13 +7278,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7688,13 +7299,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7702,13 +7306,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7716,13 +7313,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7730,13 +7320,6 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7744,11 +7327,6 @@ packages: os: [linux] libc: [musl] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -7760,24 +7338,12 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.34.5': resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.34.5': resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -10247,15 +9813,6 @@ packages: '@popperjs/core@2.11.7': resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==} - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - - '@poppinss/dumper@0.6.5': - resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@prisma/instrumentation@5.19.1': resolution: {integrity: sha512-VLnzMQq7CWroL5AeaW0Py2huiNKeoMfCH3SUxstdzPrlWQi6UQ9UrfcbUkNHlVFqOMacqy8X/8YtE0kuKDpD9w==} @@ -12118,10 +11675,6 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} - '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -12834,9 +12387,6 @@ packages: peerDependencies: solid-js: ^1.6.12 - '@speed-highlight/core@1.2.14': - resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} - '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} @@ -13553,9 +13103,6 @@ packages: '@tsconfig/node16@1.0.3': resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} - '@tsndr/cloudflare-worker-jwt@3.2.1': - resolution: {integrity: sha512-1AfDEgu7DTbU0sXDSOKh4D5t7+RJ0dIRRaDmoX3LFeW0AxQdunuqTQ7AVGwNJqOnxrbxiruyvX01cf9HHqEvXQ==} - '@tufjs/canonical-json@2.0.0': resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -13614,9 +13161,6 @@ packages: '@types/chai@4.3.4': resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cheerio@0.22.35': resolution: {integrity: sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==} @@ -13702,9 +13246,6 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/es-aggregate-error@1.0.6': resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==} @@ -14485,9 +14026,6 @@ packages: '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/mocker@2.1.9': resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: @@ -14499,59 +14037,33 @@ packages: vite: optional: true - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.4.21 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} '@vitest/runner@2.1.9': resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} '@vitest/snapshot@2.1.9': resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} '@vitest/spy@2.1.9': resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vonage/accounts@1.9.0': resolution: {integrity: sha512-4cW/tfYpL53uHR3YjTbLL/kn23/RllPmFkFf3LAhdvratwtnDSYiOy/nZooATjmon3fzdOYLW0kYGAvoeWlHUg==} @@ -15679,15 +15191,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - birpc@0.2.14: - resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} - bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - blessed@0.1.81: resolution: {integrity: sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==} engines: {node: '>= 0.8.0'} @@ -16264,10 +15770,6 @@ packages: color@3.2.1: resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -17236,9 +16738,6 @@ packages: devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} - devalue@5.6.2: - resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -17543,9 +17042,6 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - error@7.0.2: resolution: {integrity: sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw==} @@ -17766,11 +17262,6 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.25.4: - resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.27.0: resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} engines: {node: '>=18'} @@ -18012,9 +17503,6 @@ packages: resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==} engines: {node: '>= 18'} - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - ext-list@2.2.2: resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} engines: {node: '>=0.10.0'} @@ -19047,10 +18535,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.11.9: - resolution: {integrity: sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==} - engines: {node: '>=16.9.0'} - hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -19804,8 +19288,8 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - isomorphic-unfetch@3.1.0: - resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} + isomorphic-unfetch@4.0.2: + resolution: {integrity: sha512-1Yd+CF/7al18/N2BDbsLBcp6RO3tucSW+jcLq24dqdX5MNbCNTw1z4BsGsp4zNmjr/Izm2cs/cEqZPp4kvWSCA==} isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} @@ -20586,10 +20070,6 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - ky@1.14.3: - resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} - engines: {node: '>=18'} - kysely@0.28.9: resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} engines: {node: '>=20.0.0'} @@ -21043,9 +20523,6 @@ packages: loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lowdb@1.0.0: resolution: {integrity: sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==} engines: {node: '>=4'} @@ -21149,8 +20626,8 @@ packages: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} - mailersend@1.4.6: - resolution: {integrity: sha512-H01TEAQIS2xciqow401yNjvFeooCH0BHF6DuSl7H9wsiP2GEvjeuiMaeSdUOGNd2N45t+qyKCuBeyxu7J0xWEg==} + mailersend@2.6.0: + resolution: {integrity: sha512-YX2Gyc6Wyw4Q4IsJ4np2Reof8nFWQ2OP/yXZTZcGCz4B7B1BOAYs71Kjb1uNRTTfDChP7rzzWBoOaX6iNlvPAg==} mailgun.js@8.2.1: resolution: {integrity: sha512-iKHCMehdUcWzBAp8KU2idLP7AbsTxQ8DjJev4Gvm430Dujul+ZkzKPgn40uYpb9BXGL5l8/w5jpf2pvw51df/w==} @@ -21580,16 +21057,6 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - miniflare@4.20250906.0: - resolution: {integrity: sha512-T/RWn1sa0ien80s6NjU+Un/tj12gR6wqScZoiLeMJDD4/fK0UXfnbWXJDubnUED8Xjm7RPQ5ESYdE+mhPmMtuQ==} - engines: {node: '>=18.0.0'} - hasBin: true - - miniflare@4.20260205.0: - resolution: {integrity: sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==} - engines: {node: '>=18.0.0'} - hasBin: true - minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -22362,9 +21829,6 @@ packages: ohash@1.1.3: resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - ohm-js@16.6.0: resolution: {integrity: sha512-X9P4koSGa7swgVQ0gt71UCYtkAQGOjciJPJAz74kDxWt8nXbH5HrDOQG6qBDH7SR40ktNv4x61BwpTDE9q4lRA==} engines: {node: '>=0.12.1'} @@ -22757,9 +22221,6 @@ packages: path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} @@ -24675,10 +24136,6 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -25265,9 +24722,6 @@ packages: strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strip-outer@2.0.0: resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -25408,10 +24862,6 @@ packages: engines: {node: '>=14.18.0'} deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} engines: {node: '>=0.8.0'} @@ -25714,18 +25164,10 @@ packages: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - tinyspy@2.2.1: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} @@ -25734,10 +25176,6 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} - tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} @@ -26270,10 +25708,6 @@ packages: resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} engines: {node: '>=18.17'} - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} - engines: {node: '>=20.18.1'} - undici@7.21.0: resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} engines: {node: '>=20.18.1'} @@ -26281,12 +25715,6 @@ packages: unenv@1.9.0: resolution: {integrity: sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g==} - unenv@2.0.0-rc.21: - resolution: {integrity: sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==} - - unenv@2.0.0-rc.24: - resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - unescape-js@1.1.4: resolution: {integrity: sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g==} @@ -26300,6 +25728,9 @@ packages: unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} + unfetch@5.0.0: + resolution: {integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==} + unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -26580,14 +26011,6 @@ packages: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} - valibot@1.2.0: - resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} - peerDependencies: - typescript: '>=5' - peerDependenciesMeta: - typescript: - optional: true - valid-data-url@3.0.1: resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} engines: {node: '>=10'} @@ -26690,11 +26113,6 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - vite-plugin-ejs@1.7.0: resolution: {integrity: sha512-JNP3zQDC4mSbfoJ3G73s5mmZITD8NGjUmLkq4swxyahy/W0xuokK9U9IJGXw7KCggq6UucT6hJ0p+tQrNtqTZw==} peerDependencies: @@ -26823,34 +26241,6 @@ packages: jsdom: optional: true - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vizion@2.2.1: resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==} engines: {node: '>=4.0'} @@ -27117,42 +26507,12 @@ packages: resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} engines: {node: '>=8.0.0'} - workerd@1.20250906.0: - resolution: {integrity: sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw==} - engines: {node: '>=16'} - hasBin: true - - workerd@1.20260205.0: - resolution: {integrity: sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==} - engines: {node: '>=16'} - hasBin: true - workerpool@6.1.0: resolution: {integrity: sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==} workerpool@6.2.1: resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} - wrangler@4.35.0: - resolution: {integrity: sha512-HbyXtbrh4Fi3mU8ussY85tVdQ74qpVS1vctUgaPc+bPrXBTqfDLkZ6VRtHAVF/eBhz4SFmhJtCQpN1caY2Ak8A==} - engines: {node: '>=18.0.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20250906.0 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - - wrangler@4.63.0: - resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} - engines: {node: '>=20.0.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260205.0 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - wrap-ansi@3.0.1: resolution: {integrity: sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==} engines: {node: '>=4'} @@ -27396,12 +26756,6 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} - youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - - youch@4.1.0-beta.10: - resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - yup@0.32.11: resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} engines: {node: '>=10'} @@ -27799,8 +27153,8 @@ snapshots: '@aws-crypto/sha1-browser': 3.0.0 '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.575.0 - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/core': 3.575.0 '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-bucket-endpoint': 3.575.0 @@ -28028,11 +27382,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.575.0': + '@aws-sdk/client-sso-oidc@3.575.0(@aws-sdk/client-sts@3.575.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/core': 3.575.0 '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-host-header': 3.575.0 @@ -28071,6 +27425,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: + - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)': @@ -28500,11 +27855,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)': + '@aws-sdk/client-sts@3.575.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.575.0 + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) '@aws-sdk/core': 3.575.0 '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-host-header': 3.575.0 @@ -28543,7 +27898,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.637.0': @@ -28793,7 +28147,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0)': dependencies: - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/credential-provider-env': 3.575.0 '@aws-sdk/credential-provider-process': 3.575.0 '@aws-sdk/credential-provider-sso': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) @@ -29273,7 +28627,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.575.0(@aws-sdk/client-sts@3.575.0)': dependencies: - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/types': 3.575.0 '@smithy/property-provider': 3.1.3 '@smithy/types': 3.3.0 @@ -29719,7 +29073,7 @@ snapshots: '@aws-sdk/token-providers@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.575.0 + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) '@aws-sdk/types': 3.575.0 '@smithy/property-provider': 3.1.3 '@smithy/shared-ini-file-loader': 3.1.4 @@ -29728,7 +29082,7 @@ snapshots: '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.575.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.575.0 + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) '@aws-sdk/types': 3.609.0 '@smithy/property-provider': 3.1.3 '@smithy/shared-ini-file-loader': 3.1.4 @@ -31354,73 +30708,6 @@ snapshots: dependencies: '@clickhouse/client-common': 1.12.1 - '@cloudflare/kv-asset-handler@0.4.0': - dependencies: - mime: 3.0.0 - - '@cloudflare/kv-asset-handler@0.4.2': {} - - '@cloudflare/unenv-preset@2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260205.0 - - '@cloudflare/unenv-preset@2.7.3(unenv@2.0.0-rc.21)(workerd@1.20250906.0)': - dependencies: - unenv: 2.0.0-rc.21 - optionalDependencies: - workerd: 1.20250906.0 - - '@cloudflare/vitest-pool-workers@0.8.71(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@edge-runtime/vm@4.0.2)(@types/debug@4.1.12)(@types/node@22.15.13)(happy-dom@20.0.11)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6))': - dependencies: - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - birpc: 0.2.14 - cjs-module-lexer: 1.4.1 - devalue: 5.6.2 - miniflare: 4.20250906.0 - semver: 7.7.3 - vitest: 3.2.4(@edge-runtime/vm@4.0.2)(@types/debug@4.1.12)(@types/node@22.15.13)(happy-dom@20.0.11)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6) - wrangler: 4.35.0(@cloudflare/workers-types@4.20260207.0) - zod: 3.25.20 - transitivePeerDependencies: - - '@cloudflare/workers-types' - - bufferutil - - utf-8-validate - - '@cloudflare/workerd-darwin-64@1.20250906.0': - optional: true - - '@cloudflare/workerd-darwin-64@1.20260205.0': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20250906.0': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260205.0': - optional: true - - '@cloudflare/workerd-linux-64@1.20250906.0': - optional: true - - '@cloudflare/workerd-linux-64@1.20260205.0': - optional: true - - '@cloudflare/workerd-linux-arm64@1.20250906.0': - optional: true - - '@cloudflare/workerd-linux-arm64@1.20260205.0': - optional: true - - '@cloudflare/workerd-windows-64@1.20250906.0': - optional: true - - '@cloudflare/workerd-windows-64@1.20260205.0': - optional: true - - '@cloudflare/workers-types@4.20260207.0': {} - '@codemirror/autocomplete@6.18.3(@codemirror/language@6.11.1)(@codemirror/state@6.4.1)(@codemirror/view@6.34.3)(@lezer/common@1.2.3)': dependencies: '@codemirror/language': 6.11.1 @@ -32348,9 +31635,6 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true - '@esbuild/aix-ppc64@0.25.4': - optional: true - '@esbuild/aix-ppc64@0.27.0': optional: true @@ -32366,9 +31650,6 @@ snapshots: '@esbuild/android-arm64@0.23.1': optional: true - '@esbuild/android-arm64@0.25.4': - optional: true - '@esbuild/android-arm64@0.27.0': optional: true @@ -32384,9 +31665,6 @@ snapshots: '@esbuild/android-arm@0.23.1': optional: true - '@esbuild/android-arm@0.25.4': - optional: true - '@esbuild/android-arm@0.27.0': optional: true @@ -32402,9 +31680,6 @@ snapshots: '@esbuild/android-x64@0.23.1': optional: true - '@esbuild/android-x64@0.25.4': - optional: true - '@esbuild/android-x64@0.27.0': optional: true @@ -32420,9 +31695,6 @@ snapshots: '@esbuild/darwin-arm64@0.23.1': optional: true - '@esbuild/darwin-arm64@0.25.4': - optional: true - '@esbuild/darwin-arm64@0.27.0': optional: true @@ -32438,9 +31710,6 @@ snapshots: '@esbuild/darwin-x64@0.23.1': optional: true - '@esbuild/darwin-x64@0.25.4': - optional: true - '@esbuild/darwin-x64@0.27.0': optional: true @@ -32456,9 +31725,6 @@ snapshots: '@esbuild/freebsd-arm64@0.23.1': optional: true - '@esbuild/freebsd-arm64@0.25.4': - optional: true - '@esbuild/freebsd-arm64@0.27.0': optional: true @@ -32474,9 +31740,6 @@ snapshots: '@esbuild/freebsd-x64@0.23.1': optional: true - '@esbuild/freebsd-x64@0.25.4': - optional: true - '@esbuild/freebsd-x64@0.27.0': optional: true @@ -32492,9 +31755,6 @@ snapshots: '@esbuild/linux-arm64@0.23.1': optional: true - '@esbuild/linux-arm64@0.25.4': - optional: true - '@esbuild/linux-arm64@0.27.0': optional: true @@ -32510,9 +31770,6 @@ snapshots: '@esbuild/linux-arm@0.23.1': optional: true - '@esbuild/linux-arm@0.25.4': - optional: true - '@esbuild/linux-arm@0.27.0': optional: true @@ -32528,9 +31785,6 @@ snapshots: '@esbuild/linux-ia32@0.23.1': optional: true - '@esbuild/linux-ia32@0.25.4': - optional: true - '@esbuild/linux-ia32@0.27.0': optional: true @@ -32546,9 +31800,6 @@ snapshots: '@esbuild/linux-loong64@0.23.1': optional: true - '@esbuild/linux-loong64@0.25.4': - optional: true - '@esbuild/linux-loong64@0.27.0': optional: true @@ -32564,9 +31815,6 @@ snapshots: '@esbuild/linux-mips64el@0.23.1': optional: true - '@esbuild/linux-mips64el@0.25.4': - optional: true - '@esbuild/linux-mips64el@0.27.0': optional: true @@ -32582,9 +31830,6 @@ snapshots: '@esbuild/linux-ppc64@0.23.1': optional: true - '@esbuild/linux-ppc64@0.25.4': - optional: true - '@esbuild/linux-ppc64@0.27.0': optional: true @@ -32600,9 +31845,6 @@ snapshots: '@esbuild/linux-riscv64@0.23.1': optional: true - '@esbuild/linux-riscv64@0.25.4': - optional: true - '@esbuild/linux-riscv64@0.27.0': optional: true @@ -32618,9 +31860,6 @@ snapshots: '@esbuild/linux-s390x@0.23.1': optional: true - '@esbuild/linux-s390x@0.25.4': - optional: true - '@esbuild/linux-s390x@0.27.0': optional: true @@ -32636,15 +31875,9 @@ snapshots: '@esbuild/linux-x64@0.23.1': optional: true - '@esbuild/linux-x64@0.25.4': - optional: true - '@esbuild/linux-x64@0.27.0': optional: true - '@esbuild/netbsd-arm64@0.25.4': - optional: true - '@esbuild/netbsd-arm64@0.27.0': optional: true @@ -32660,18 +31893,12 @@ snapshots: '@esbuild/netbsd-x64@0.23.1': optional: true - '@esbuild/netbsd-x64@0.25.4': - optional: true - '@esbuild/netbsd-x64@0.27.0': optional: true '@esbuild/openbsd-arm64@0.23.1': optional: true - '@esbuild/openbsd-arm64@0.25.4': - optional: true - '@esbuild/openbsd-arm64@0.27.0': optional: true @@ -32687,9 +31914,6 @@ snapshots: '@esbuild/openbsd-x64@0.23.1': optional: true - '@esbuild/openbsd-x64@0.25.4': - optional: true - '@esbuild/openbsd-x64@0.27.0': optional: true @@ -32708,9 +31932,6 @@ snapshots: '@esbuild/sunos-x64@0.23.1': optional: true - '@esbuild/sunos-x64@0.25.4': - optional: true - '@esbuild/sunos-x64@0.27.0': optional: true @@ -32726,9 +31947,6 @@ snapshots: '@esbuild/win32-arm64@0.23.1': optional: true - '@esbuild/win32-arm64@0.25.4': - optional: true - '@esbuild/win32-arm64@0.27.0': optional: true @@ -32744,9 +31962,6 @@ snapshots: '@esbuild/win32-ia32@0.23.1': optional: true - '@esbuild/win32-ia32@0.25.4': - optional: true - '@esbuild/win32-ia32@0.27.0': optional: true @@ -32762,9 +31977,6 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true - '@esbuild/win32-x64@0.25.4': - optional: true - '@esbuild/win32-x64@0.27.0': optional: true @@ -33047,11 +32259,6 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@hono/valibot-validator@0.6.1(hono@4.11.9)(valibot@1.2.0(typescript@5.8.3))': - dependencies: - hono: 4.11.9 - valibot: 1.2.0(typescript@5.8.3) - '@hookform/devtools@4.3.1(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@emotion/react': 11.10.6(@types/react@19.2.8)(react@19.2.3) @@ -33087,11 +32294,7 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} - '@img/colour@1.0.0': {} - - '@img/sharp-darwin-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/colour@1.0.0': optional: true '@img/sharp-darwin-arm64@0.34.5': @@ -33099,37 +32302,20 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - optional: true - '@img/sharp-darwin-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': - optional: true - '@img/sharp-libvips-linux-arm@1.2.4': optional: true @@ -33139,45 +32325,23 @@ snapshots: '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': - optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': - optional: true - '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - optional: true - '@img/sharp-linux-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - optional: true - '@img/sharp-linux-arm@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.4 @@ -33193,51 +32357,26 @@ snapshots: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 - optional: true - '@img/sharp-linux-s390x@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - optional: true - '@img/sharp-linux-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.33.5': - dependencies: - '@emnapi/runtime': 1.7.1 - optional: true - '@img/sharp-wasm32@0.34.5': dependencies: '@emnapi/runtime': 1.7.1 @@ -33246,15 +32385,9 @@ snapshots: '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.33.5': - optional: true - '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.33.5': - optional: true - '@img/sharp-win32-x64@0.34.5': optional: true @@ -36919,18 +36052,6 @@ snapshots: '@popperjs/core@2.11.7': {} - '@poppinss/colors@4.1.6': - dependencies: - kleur: 4.1.5 - - '@poppinss/dumper@0.6.5': - dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 - supports-color: 10.2.2 - - '@poppinss/exception@1.2.3': {} - '@prisma/instrumentation@5.19.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -39640,8 +38761,6 @@ snapshots: '@sindresorhus/is@4.6.0': {} - '@sindresorhus/is@7.2.0': {} - '@sindresorhus/merge-streams@4.0.0': {} '@sinonjs/commons@1.8.6': @@ -40739,8 +39858,6 @@ snapshots: dependencies: solid-js: 1.9.6 - '@speed-highlight/core@1.2.14': {} - '@stablelib/base64@1.0.1': {} '@standard-schema/spec@1.0.0': {} @@ -41632,8 +40749,6 @@ snapshots: '@tsconfig/node16@1.0.3': {} - '@tsndr/cloudflare-worker-jwt@3.2.1': {} - '@tufjs/canonical-json@2.0.0': {} '@tybys/wasm-util@0.10.0': @@ -41712,11 +40827,6 @@ snapshots: '@types/chai@4.3.4': {} - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - '@types/cheerio@0.22.35': dependencies: '@types/node': 20.19.10 @@ -41800,8 +40910,6 @@ snapshots: dependencies: '@types/ms': 0.7.31 - '@types/deep-eql@4.0.2': {} - '@types/es-aggregate-error@1.0.6': dependencies: '@types/node': 20.19.10 @@ -42799,14 +41907,6 @@ snapshots: chai: 5.2.0 tinyrainbow: 1.2.0 - '@vitest/expect@3.2.4': - dependencies: - '@types/chai': 5.2.3 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.0 - tinyrainbow: 2.0.0 - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.10)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6))': dependencies: '@vitest/spy': 2.1.9 @@ -42823,22 +41923,10 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6) - '@vitest/mocker@3.2.4(vite@5.4.21(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 5.4.21(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6) - '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 - '@vitest/pretty-format@3.2.4': - dependencies: - tinyrainbow: 2.0.0 - '@vitest/runner@1.6.1': dependencies: '@vitest/utils': 1.6.1 @@ -42850,12 +41938,6 @@ snapshots: '@vitest/utils': 2.1.9 pathe: 1.1.2 - '@vitest/runner@3.2.4': - dependencies: - '@vitest/utils': 3.2.4 - pathe: 2.0.3 - strip-literal: 3.1.0 - '@vitest/snapshot@1.6.1': dependencies: magic-string: 0.30.17 @@ -42868,12 +41950,6 @@ snapshots: magic-string: 0.30.17 pathe: 1.1.2 - '@vitest/snapshot@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.21 - pathe: 2.0.3 - '@vitest/spy@1.6.1': dependencies: tinyspy: 2.2.1 @@ -42882,10 +41958,6 @@ snapshots: dependencies: tinyspy: 3.0.2 - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 - '@vitest/utils@1.6.1': dependencies: diff-sequences: 29.6.3 @@ -42899,12 +41971,6 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - '@vitest/utils@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 - '@vonage/accounts@1.9.0(encoding@0.1.13)': dependencies: '@vonage/server-client': 1.9.0(encoding@0.1.13) @@ -44268,16 +43334,12 @@ snapshots: file-uri-to-path: 1.0.0 optional: true - birpc@0.2.14: {} - bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - blake3-wasm@2.1.5: {} - blessed@0.1.81: {} blob-util@2.0.2: @@ -45036,11 +44098,6 @@ snapshots: color-convert: 1.9.3 color-string: 1.9.1 - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - colord@2.9.3: {} colorette@2.0.20: {} @@ -45841,7 +44898,6 @@ snapshots: ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 - optional: true debug@4.3.1(supports-color@8.1.1): dependencies: @@ -46083,8 +45139,6 @@ snapshots: devalue@4.3.3: {} - devalue@5.6.2: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -46407,8 +45461,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - error-stack-parser-es@1.0.5: {} - error@7.0.2: dependencies: string-template: 0.2.1 @@ -46732,34 +45784,6 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 - esbuild@0.25.4: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.4 - '@esbuild/android-arm': 0.25.4 - '@esbuild/android-arm64': 0.25.4 - '@esbuild/android-x64': 0.25.4 - '@esbuild/darwin-arm64': 0.25.4 - '@esbuild/darwin-x64': 0.25.4 - '@esbuild/freebsd-arm64': 0.25.4 - '@esbuild/freebsd-x64': 0.25.4 - '@esbuild/linux-arm': 0.25.4 - '@esbuild/linux-arm64': 0.25.4 - '@esbuild/linux-ia32': 0.25.4 - '@esbuild/linux-loong64': 0.25.4 - '@esbuild/linux-mips64el': 0.25.4 - '@esbuild/linux-ppc64': 0.25.4 - '@esbuild/linux-riscv64': 0.25.4 - '@esbuild/linux-s390x': 0.25.4 - '@esbuild/linux-x64': 0.25.4 - '@esbuild/netbsd-arm64': 0.25.4 - '@esbuild/netbsd-x64': 0.25.4 - '@esbuild/openbsd-arm64': 0.25.4 - '@esbuild/openbsd-x64': 0.25.4 - '@esbuild/sunos-x64': 0.25.4 - '@esbuild/win32-arm64': 0.25.4 - '@esbuild/win32-ia32': 0.25.4 - '@esbuild/win32-x64': 0.25.4 - esbuild@0.27.0: optionalDependencies: '@esbuild/aix-ppc64': 0.27.0 @@ -47215,8 +46239,6 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.0.8: {} - ext-list@2.2.2: dependencies: mime-db: 1.53.0 @@ -48545,8 +47567,6 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.11.9: {} - hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: @@ -49319,12 +48339,10 @@ snapshots: isobject@3.0.1: {} - isomorphic-unfetch@3.1.0(encoding@0.1.13): + isomorphic-unfetch@4.0.2: dependencies: - node-fetch: 2.7.0(encoding@0.1.13) - unfetch: 4.2.0 - transitivePeerDependencies: - - encoding + node-fetch: 3.3.2 + unfetch: 5.0.0 isomorphic.js@0.2.5: {} @@ -50910,8 +49928,6 @@ snapshots: kuler@2.0.0: {} - ky@1.14.3: {} - kysely@0.28.9: {} languagedetect@1.3.0: {} @@ -51346,8 +50362,6 @@ snapshots: loupe@3.1.3: {} - loupe@3.2.1: {} - lowdb@1.0.0: dependencies: graceful-fs: 4.2.11 @@ -51466,11 +50480,14 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - mailersend@1.4.6(encoding@0.1.13): + mailersend@2.6.0(encoding@0.1.13): dependencies: - isomorphic-unfetch: 3.1.0(encoding@0.1.13) + gaxios: 6.1.1(encoding@0.1.13) + isomorphic-unfetch: 4.0.2 + qs: 6.14.0 transitivePeerDependencies: - encoding + - supports-color mailgun.js@8.2.1: dependencies: @@ -52150,36 +51167,6 @@ snapshots: min-indent@1.0.1: {} - miniflare@4.20250906.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - acorn: 8.14.0 - acorn-walk: 8.3.2 - exit-hook: 2.2.1 - glob-to-regexp: 0.4.1 - sharp: 0.33.5 - stoppable: 1.1.0 - undici: 7.21.0 - workerd: 1.20250906.0 - ws: 8.18.0 - youch: 4.1.0-beta.10 - zod: 3.22.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - miniflare@4.20260205.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.18.2 - workerd: 1.20260205.0 - ws: 8.18.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - minimalistic-assert@1.0.1: {} minimatch@10.0.1: @@ -52620,7 +51607,7 @@ snapshots: needle@2.4.0: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) iconv-lite: 0.4.24 sax: 1.4.1 transitivePeerDependencies: @@ -52628,7 +51615,7 @@ snapshots: needle@3.2.0: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) iconv-lite: 0.6.3 sax: 1.4.1 transitivePeerDependencies: @@ -53262,8 +52249,6 @@ snapshots: ohash@1.1.3: {} - ohash@2.0.11: {} - ohm-js@16.6.0: {} on-exit-leak-free@0.2.0: {} @@ -53689,8 +52674,6 @@ snapshots: path-to-regexp@6.2.1: {} - path-to-regexp@6.3.0: {} - path-to-regexp@8.2.0: {} path-type@3.0.0: @@ -54010,7 +52993,7 @@ snapshots: portfinder@1.0.32: dependencies: async: 2.6.4 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) mkdirp: 0.5.6 transitivePeerDependencies: - supports-color @@ -56025,32 +55008,6 @@ snapshots: shallowequal@1.1.0: {} - sharp@0.33.5: - dependencies: - color: 4.2.3 - detect-libc: 2.1.2 - semver: 7.7.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -56081,6 +55038,7 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + optional: true shebang-command@1.2.0: dependencies: @@ -56734,10 +55692,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - strip-outer@2.0.0: {} stripe-event-types@3.1.0(stripe@11.18.0): @@ -56917,8 +55871,6 @@ snapshots: transitivePeerDependencies: - supports-color - supports-color@10.2.2: {} - supports-color@2.0.0: {} supports-color@5.5.0: @@ -57389,18 +56341,12 @@ snapshots: tinypool@1.0.2: {} - tinypool@1.1.1: {} - tinyrainbow@1.2.0: {} - tinyrainbow@2.0.0: {} - tinyspy@2.2.1: {} tinyspy@3.0.2: {} - tinyspy@4.0.4: {} - tippy.js@6.3.7: dependencies: '@popperjs/core': 2.11.7 @@ -58122,8 +57068,6 @@ snapshots: undici@6.19.8: {} - undici@7.18.2: {} - undici@7.21.0: {} unenv@1.9.0: @@ -58134,18 +57078,6 @@ snapshots: node-fetch-native: 1.6.4 pathe: 1.1.2 - unenv@2.0.0-rc.21: - dependencies: - defu: 6.1.4 - exsolve: 1.0.8 - ohash: 2.0.11 - pathe: 2.0.3 - ufo: 1.6.1 - - unenv@2.0.0-rc.24: - dependencies: - pathe: 2.0.3 - unescape-js@1.1.4: dependencies: string.fromcodepoint: 0.2.1 @@ -58158,6 +57090,8 @@ snapshots: unfetch@4.2.0: {} + unfetch@5.0.0: {} + unicode-canonical-property-names-ecmascript@2.0.0: {} unicode-emoji-modifier-base@1.0.0: {} @@ -58494,10 +57428,6 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 - valibot@1.2.0(typescript@5.8.3): - optionalDependencies: - typescript: 5.8.3 - valid-data-url@3.0.1: {} validate-html-nesting@1.2.2: {} @@ -58735,24 +57665,6 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6): - dependencies: - cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 5.4.21(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - vite-plugin-ejs@1.7.0(vite@5.4.21(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)): dependencies: ejs: 3.1.10 @@ -58963,48 +57875,6 @@ snapshots: - supports-color - terser - vitest@3.2.4(@edge-runtime/vm@4.0.2)(@types/debug@4.1.12)(@types/node@22.15.13)(happy-dom@20.0.11)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.0 - debug: 4.4.3(supports-color@8.1.1) - expect-type: 1.2.1 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.2 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6) - vite-node: 3.2.4(@types/node@22.15.13)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6) - why-is-node-running: 2.3.0 - optionalDependencies: - '@edge-runtime/vm': 4.0.2 - '@types/debug': 4.1.12 - '@types/node': 22.15.13 - happy-dom: 20.0.11 - jsdom: 25.0.0 - transitivePeerDependencies: - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - vizion@2.2.1: dependencies: async: 2.6.4 @@ -59352,60 +58222,10 @@ snapshots: reduce-flatten: 2.0.0 typical: 5.2.0 - workerd@1.20250906.0: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20250906.0 - '@cloudflare/workerd-darwin-arm64': 1.20250906.0 - '@cloudflare/workerd-linux-64': 1.20250906.0 - '@cloudflare/workerd-linux-arm64': 1.20250906.0 - '@cloudflare/workerd-windows-64': 1.20250906.0 - - workerd@1.20260205.0: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260205.0 - '@cloudflare/workerd-darwin-arm64': 1.20260205.0 - '@cloudflare/workerd-linux-64': 1.20260205.0 - '@cloudflare/workerd-linux-arm64': 1.20260205.0 - '@cloudflare/workerd-windows-64': 1.20260205.0 - workerpool@6.1.0: {} workerpool@6.2.1: {} - wrangler@4.35.0(@cloudflare/workers-types@4.20260207.0): - dependencies: - '@cloudflare/kv-asset-handler': 0.4.0 - '@cloudflare/unenv-preset': 2.7.3(unenv@2.0.0-rc.21)(workerd@1.20250906.0) - blake3-wasm: 2.1.5 - esbuild: 0.25.4 - miniflare: 4.20250906.0 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.21 - workerd: 1.20250906.0 - optionalDependencies: - '@cloudflare/workers-types': 4.20260207.0 - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0): - dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0) - blake3-wasm: 2.1.5 - esbuild: 0.27.0 - miniflare: 4.20260205.0 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260205.0 - optionalDependencies: - '@cloudflare/workers-types': 4.20260207.0 - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - wrap-ansi@3.0.1: dependencies: string-width: 2.1.1 @@ -59625,19 +58445,6 @@ snapshots: yoctocolors@2.1.1: {} - youch-core@0.3.3: - dependencies: - '@poppinss/exception': 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0-beta.10: - dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.14 - cookie: 1.0.2 - youch-core: 0.3.3 - yup@0.32.11: dependencies: '@babel/runtime': 7.28.3