From b22870011fce9add3e9d82ae0f0ab4fa8840b42b Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Wed, 20 Aug 2025 10:24:52 -0700 Subject: [PATCH 01/12] Create two endpoints for the email service --- app/api/service/emails/render/route.js | 58 +++++++++++++++++++++ app/api/service/emails/route.js | 13 +++++ emails/forgot-password.jsx | 72 +++++++++++++++++++------- emails/index.ts | 1 + util/server-only/email-service.js | 32 ++++++++++++ 5 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 app/api/service/emails/render/route.js create mode 100644 app/api/service/emails/route.js create mode 100644 util/server-only/email-service.js diff --git a/app/api/service/emails/render/route.js b/app/api/service/emails/render/route.js new file mode 100644 index 0000000..0ab5f7f --- /dev/null +++ b/app/api/service/emails/render/route.js @@ -0,0 +1,58 @@ +import { NextResponse } from 'next/server' +import { render } from '@react-email/render' +import React from 'react' +import * as BaseEmailComponents from 'emails' +import { ValidationError, ValidatedEmail } from 'emails/utils/validation' +import { checkEmailServiceAuth } from 'util/server-only/email-service' + +const EmailComponents = Object.values(BaseEmailComponents).reduce( + (acc, Component) => ({ + [`${Component.definition.name}:${Component.definition.version || ''}`]: + ValidatedEmail(Component, Component.definition.schema), + ...acc, + }), + {} +) + +// interface RequestBody { +// name: string; +// id?: string; +// data?: Record; +// } + +export async function POST(req) { + const authError = checkEmailServiceAuth(req) + if (authError) return authError + + try { + const body = await req.json() + const { name, id = '', data = {} } = body + + const identifier = `${name}:${id}` + + const EmailComponent = EmailComponents[identifier] + + if (!EmailComponent) { + return NextResponse.json( + { error: `Email template "${name}" not found` }, + { status: 404 } + ) + } + + const html = await render(React.createElement(EmailComponent, data)) + return NextResponse.json({ html }) + } catch (error) { + console.error('Email render error:', error) + return NextResponse.json( + error instanceof ValidationError + ? { + error: error.message, + data: error.data, + } + : { + error: error instanceof Error ? error.message : 'Invalid request', + }, + { status: 400 } + ) + } +} diff --git a/app/api/service/emails/route.js b/app/api/service/emails/route.js new file mode 100644 index 0000000..e84d102 --- /dev/null +++ b/app/api/service/emails/route.js @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server' +import * as EmailComponents from 'emails' +import { checkEmailServiceAuth } from 'util/server-only/email-service' + +export async function GET(request) { + const authError = checkEmailServiceAuth(request) + if (authError) return authError + const emails = Object.values(EmailComponents).map((Component) => { + return Component.definition + }) + + return NextResponse.json({ results: emails, count: emails.length }) +} diff --git a/emails/forgot-password.jsx b/emails/forgot-password.jsx index ee71eb7..1984026 100644 --- a/emails/forgot-password.jsx +++ b/emails/forgot-password.jsx @@ -1,37 +1,66 @@ -import { Button, Html, Text, Container, Section, Head, Heading, Body } from '@react-email/components' -import { variables } from 'styles/variables' -import { useTranslation } from 'react-i18next' -import './_util/i18n' +import { + Button, + Html, + Text, + Container, + Section, + Head, + Heading, + Body, +} from '@react-email/components' import * as React from 'react' -export const ForgotPassword = ({ theme = 'dark', language = 'fr', resetLink = 'http://example.com/reset' }) => { - const { t, i18n } = useTranslation('email') - - i18n.changeLanguage(language) +const definition = { + name: 'Forgot Password', + description: + 'Sends a password reset link when a user submits forgot password form.', + schema: { + type: 'object', + required: ['resetLink'], + properties: { + resetLink: { + type: 'string', + default: 'https://example.com/reset', + }, + }, + }, +} + +const schema = { + type: 'object', + required: ['resetLink'], + properties: { + resetLink: { + type: 'string', + default: 'https://example.com/reset', + }, + }, +} +export const ForgotPassword = ({ + resetLink = schema.properties.resetLink.default, +}) => { return ( - + - {t('forgot-password.heading')} + Reset Your Password - {t('forgot-password.body')} + We received a request to reset your password. Click the button below + to set a new password:
-
- {t('forgot-password.disclaimer')} - - - {t('forgot-password.footer')} + If you didn't request a password reset, please ignore this email. + © 2025 YourApp. All rights reserved.
@@ -76,4 +105,7 @@ const footer = { textAlign: 'center', } +ForgotPassword.schema = schema +ForgotPassword.definition = definition + export default ForgotPassword diff --git a/emails/index.ts b/emails/index.ts index e69de29..1f16357 100644 --- a/emails/index.ts +++ b/emails/index.ts @@ -0,0 +1 @@ +export * from './forgot-password' diff --git a/util/server-only/email-service.js b/util/server-only/email-service.js new file mode 100644 index 0000000..7296ae7 --- /dev/null +++ b/util/server-only/email-service.js @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server' + +export function checkEmailServiceAuth(request) { + const authHeader = request.headers.get('authorization') + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return NextResponse.json( + { error: 'Missing or invalid authorization header' }, + { status: 401 } + ) + } + + const token = authHeader.substring(7) // Remove 'Bearer ' prefix + const expectedToken = process.env.EMAIL_PROVIDER_API_KEY + + if (!expectedToken) { + console.error('EMAIL_PROVIDER_API_KEY environment variable not set') + return NextResponse.json( + { error: 'Server configuration error' }, + { status: 500 } + ) + } + + if (token !== expectedToken) { + return NextResponse.json( + { error: 'Invalid authorization token' }, + { status: 401 } + ) + } + + return null // Auth passed +} From 3a909cfac36102ca516f8d08a5466b75d2dafaa3 Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Wed, 20 Aug 2025 14:57:49 -0700 Subject: [PATCH 02/12] Add two endpoints --- app/api/service/emails/render/route.js | 15 +- app/api/service/emails/render/route.test.js | 171 ++++++++++++++ app/api/service/emails/render/route.test.mjs | 94 ++++++++ architecture/openapi.yaml | 127 ++++++++++ emails/_util/validation.js | 59 +++++ emails/forgot-password.jsx | 42 +++- package-lock.json | 229 ++++++++++--------- package.json | 2 + 8 files changed, 623 insertions(+), 116 deletions(-) create mode 100644 app/api/service/emails/render/route.test.js create mode 100644 app/api/service/emails/render/route.test.mjs create mode 100644 architecture/openapi.yaml create mode 100644 emails/_util/validation.js diff --git a/app/api/service/emails/render/route.js b/app/api/service/emails/render/route.js index 0ab5f7f..a0bb59d 100644 --- a/app/api/service/emails/render/route.js +++ b/app/api/service/emails/render/route.js @@ -2,12 +2,12 @@ import { NextResponse } from 'next/server' import { render } from '@react-email/render' import React from 'react' import * as BaseEmailComponents from 'emails' -import { ValidationError, ValidatedEmail } from 'emails/utils/validation' +import { ValidationError, ValidatedEmail } from 'emails/_util/validation' import { checkEmailServiceAuth } from 'util/server-only/email-service' const EmailComponents = Object.values(BaseEmailComponents).reduce( (acc, Component) => ({ - [`${Component.definition.name}:${Component.definition.version || ''}`]: + [Component.definition.version || Component.definition.name]: ValidatedEmail(Component, Component.definition.schema), ...acc, }), @@ -15,8 +15,7 @@ const EmailComponents = Object.values(BaseEmailComponents).reduce( ) // interface RequestBody { -// name: string; -// id?: string; +// template_id: string; // data?: Record; // } @@ -26,15 +25,13 @@ export async function POST(req) { try { const body = await req.json() - const { name, id = '', data = {} } = body + const { template_id, data = {} } = body - const identifier = `${name}:${id}` - - const EmailComponent = EmailComponents[identifier] + const EmailComponent = EmailComponents[template_id] if (!EmailComponent) { return NextResponse.json( - { error: `Email template "${name}" not found` }, + { error: `Email template "${template_id}" not found` }, { status: 404 } ) } diff --git a/app/api/service/emails/render/route.test.js b/app/api/service/emails/render/route.test.js new file mode 100644 index 0000000..05863a0 --- /dev/null +++ b/app/api/service/emails/render/route.test.js @@ -0,0 +1,171 @@ +import { NextRequest } from 'next/server' +import { POST } from './route' + +// Mock the dependencies +jest.mock('@react-email/render', () => ({ + render: jest.fn() +})) + +jest.mock('emails', () => ({ + ForgotPassword: { + definition: { + name: 'Forgot Password', + version: 'forgot-password-v1', + schema: { + type: 'object', + required: ['resetLink'], + properties: { + subject: { type: 'string', default: 'Reset your password' }, + resetLink: { type: 'string', format: 'uri', default: 'https://example.com/reset' }, + languages: { type: 'array', items: { type: 'string' }, default: ['en'] }, + theme: { type: 'string', default: 'default' } + } + } + } + } +})) + +jest.mock('emails/utils/validation', () => ({ + ValidationError: class ValidationError extends Error { + constructor(message, data) { + super(message) + this.data = data + } + }, + ValidatedEmail: jest.fn((Component) => Component) +})) + +jest.mock('util/server-only/email-service', () => ({ + checkEmailServiceAuth: jest.fn(() => null) // Return null for successful auth +})) + +describe('/api/service/emails/render', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should render forgot-password email template successfully', async () => { + const { render } = require('@react-email/render') + render.mockResolvedValue('Rendered email content') + + const requestBody = { + template_id: 'forgot-password-v1', + data: { + subject: 'Reset your password', + resetLink: 'https://example.com/reset?token=abc123', + languages: ['en'], + theme: 'default' + } + } + + const request = new NextRequest('http://localhost:3000/api/service/emails/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + const response = await POST(request) + const responseData = await response.json() + + expect(response.status).toBe(200) + expect(responseData).toEqual({ + html: 'Rendered email content' + }) + expect(render).toHaveBeenCalledTimes(1) + }) + + it('should return 404 for non-existent template', async () => { + const requestBody = { + template_id: 'non-existent-template', + data: {} + } + + const request = new NextRequest('http://localhost:3000/api/service/emails/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + const response = await POST(request) + const responseData = await response.json() + + expect(response.status).toBe(404) + expect(responseData).toEqual({ + error: 'Email template "non-existent-template" not found' + }) + }) + + it('should handle validation errors', async () => { + const { render } = require('@react-email/render') + const { ValidationError } = require('emails/utils/validation') + + render.mockRejectedValue(new ValidationError('Invalid data', { field: 'resetLink' })) + + const requestBody = { + template_id: 'forgot-password-v1', + data: { + resetLink: 'invalid-url' + } + } + + const request = new NextRequest('http://localhost:3000/api/service/emails/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + const response = await POST(request) + const responseData = await response.json() + + expect(response.status).toBe(400) + expect(responseData).toEqual({ + error: 'Invalid data', + data: { field: 'resetLink' } + }) + }) + + it('should handle authentication errors', async () => { + const { checkEmailServiceAuth } = require('util/server-only/email-service') + const authErrorResponse = new Response('Unauthorized', { status: 401 }) + checkEmailServiceAuth.mockReturnValue(authErrorResponse) + + const requestBody = { + template_id: 'forgot-password-v1', + data: {} + } + + const request = new NextRequest('http://localhost:3000/api/service/emails/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + const response = await POST(request) + + expect(response.status).toBe(401) + }) + + it('should handle malformed JSON', async () => { + const request = new NextRequest('http://localhost:3000/api/service/emails/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: 'invalid json' + }) + + const response = await POST(request) + const responseData = await response.json() + + expect(response.status).toBe(400) + expect(responseData.error).toBeDefined() + }) +}) \ No newline at end of file diff --git a/app/api/service/emails/render/route.test.mjs b/app/api/service/emails/render/route.test.mjs new file mode 100644 index 0000000..932965a --- /dev/null +++ b/app/api/service/emails/render/route.test.mjs @@ -0,0 +1,94 @@ +import { test, describe } from 'node:test' +import assert from 'node:assert' + +// Simple test for the POST endpoint using fetch +describe('POST /api/service/emails/render', () => { + test('should render forgot-password email template successfully', async () => { + const payload = { + template_id: 'forgot-password-v1', + data: { + subject: 'Reset your password', + resetLink: 'https://example.com/reset?token=abc123', + languages: ['en'], + theme: 'default' + } + } + + // Note: This test assumes the server is running on localhost:3000 + // You may need to adjust the URL based on your development setup + try { + const response = await fetch('http://localhost:3000/api/service/emails/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer test-token' + }, + body: JSON.stringify(payload) + }) + + const data = await response.json() + + // Test that the response is successful + assert.strictEqual(response.status, 200, `Expected status 200, got ${response.status}`) + + // Test that the response contains HTML + assert.ok(data.html, 'Response should contain html field') + assert.strictEqual(typeof data.html, 'string', 'HTML should be a string') + + // Test that the HTML contains expected content + assert.ok(data.html.includes('Reset your password'), 'HTML should contain the subject') + assert.ok(data.html.includes('https://example.com/reset?token=abc123'), 'HTML should contain the reset link') + + console.log('✅ Test passed: Email template rendered successfully') + console.log('Response data:', JSON.stringify(data, null, 2)) + + } catch (error) { + if (error.code === 'ECONNREFUSED') { + console.log('⚠️ Server not running. Start the dev server with: npm run dev') + console.log(' Then run this test again') + return + } + throw error + } + }) + + test('should return 404 for non-existent template', async () => { + const payload = { + template_id: 'non-existent-template', + data: {} + } + + try { + const response = await fetch('http://localhost:3000/api/service/emails/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer test-token' + }, + body: JSON.stringify(payload) + }) + + const data = await response.json() + + assert.strictEqual(response.status, 404, `Expected status 404, got ${response.status}`) + assert.ok(data.error, 'Response should contain error field') + assert.ok(data.error.includes('not found'), 'Error should mention template not found') + + console.log('✅ Test passed: 404 error for non-existent template') + + } catch (error) { + if (error.code === 'ECONNREFUSED') { + console.log('⚠️ Server not running. Start the dev server with: npm run dev') + return + } + throw error + } + }) +}) + +// Manual test runner if you want to run this directly +if (import.meta.url === `file://${process.argv[1]}`) { + console.log('🧪 Running email render API tests...') + console.log('Make sure your development server is running (npm run dev)') + console.log('') +} \ No newline at end of file diff --git a/architecture/openapi.yaml b/architecture/openapi.yaml new file mode 100644 index 0000000..236bf25 --- /dev/null +++ b/architecture/openapi.yaml @@ -0,0 +1,127 @@ +openapi: 3.1.0 +info: + title: Email Specifications + description: Defines the structure and data requirements for emails sent by the backend + version: 1.0.0 + +# Email template provider configuration +x-email-template-providers: + endpoint: "POST /api/service/emails/render" + description: "Renders email templates with provided data and returns HTML" + +# Use vendor extensions for email-specific metadata +x-email-templates: + forgot-password: + name: "Forgot Password" + description: "Email that is sent when a user submits the forgot password form." + trigger: "user_forgot_password" + subject: "Reset your password" + template-id: "forgot-password-v1" + +components: + schemas: + # Common schemas that can be reused + Language: + type: string + enum: [en, es, fr] + default: en + + Theme: + type: string + enum: [default, dark, minimal] + default: default + + # Email-specific schemas (must end with "Email") + ForgotPasswordEmail: + type: object + description: Data required for forgot password email template + properties: + subject: + type: string + default: "Reset your password" + example: "Reset your password" + resetLink: + type: string + format: uri + default: "https://example.com/reset" + example: "https://example.com/reset?token=abc123" + languages: + type: array + items: + $ref: '#/components/schemas/Language' + default: ["en"] + theme: + $ref: '#/components/schemas/Theme' + required: + - resetLink + +paths: + /api/service/emails/render: + post: + summary: Render email template + description: Renders an email template with provided data and returns HTML + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + template_id: + type: string + description: Template ID/version identifier + example: "forgot-password-v1" + data: + type: object + description: Template data + additionalProperties: true + example: + subject: "Reset your password" + resetLink: "https://example.com/reset?token=abc123" + languages: ["en"] + theme: "default" + required: + - template_id + examples: + forgot-password: + summary: Forgot Password Email + value: + template_id: "forgot-password-v1" + data: + subject: "Reset your password" + resetLink: "https://example.com/reset?token=abc123" + languages: ["en"] + theme: "default" + responses: + '200': + description: Successfully rendered email template + content: + application/json: + schema: + type: object + properties: + html: + type: string + description: Rendered HTML email content + '400': + description: Bad request - validation error or invalid data + content: + application/json: + schema: + type: object + properties: + error: + type: string + data: + type: object + description: Additional error data for validation errors + '404': + description: Email template not found + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Email template "Template Name" not found' \ No newline at end of file diff --git a/emails/_util/validation.js b/emails/_util/validation.js new file mode 100644 index 0000000..158e771 --- /dev/null +++ b/emails/_util/validation.js @@ -0,0 +1,59 @@ +import React from 'react' + +export class ValidationError extends Error { + constructor(message, data) { + super(message) + this.name = 'ValidationError' + this.data = data + } +} + +export const ValidatedEmail = (Component, schema) => (props) => { + // Basic validation - check required fields + if (schema.required) { + for (const field of schema.required) { + if (props[field] === undefined || props[field] === null) { + throw new ValidationError( + `Missing required field: ${field}`, + { field, value: props[field] } + ) + } + } + } + + // Basic type and format validation + if (schema.properties) { + for (const [field, fieldSchema] of Object.entries(schema.properties)) { + const value = props[field] + if (value !== undefined) { + // Type validation + if (fieldSchema.type === 'string' && typeof value !== 'string') { + throw new ValidationError( + `Field ${field} must be a string`, + { field, value, expectedType: 'string' } + ) + } + if (fieldSchema.type === 'array' && !Array.isArray(value)) { + throw new ValidationError( + `Field ${field} must be an array`, + { field, value, expectedType: 'array' } + ) + } + + // URI format validation + if (fieldSchema.format === 'uri' && typeof value === 'string') { + try { + new URL(value) + } catch { + throw new ValidationError( + `Field ${field} must be a valid URI`, + { field, value, expectedFormat: 'uri' } + ) + } + } + } + } + } + + return +} diff --git a/emails/forgot-password.jsx b/emails/forgot-password.jsx index 1984026..1863063 100644 --- a/emails/forgot-password.jsx +++ b/emails/forgot-password.jsx @@ -14,14 +14,33 @@ const definition = { name: 'Forgot Password', description: 'Sends a password reset link when a user submits forgot password form.', + version: 'forgot-password-v1', schema: { type: 'object', required: ['resetLink'], properties: { + subject: { + type: 'string', + default: 'Reset your password', + }, resetLink: { type: 'string', + format: 'uri', default: 'https://example.com/reset', }, + languages: { + type: 'array', + items: { + type: 'string', + enum: ['en', 'es', 'fr'], + }, + default: ['en'], + }, + theme: { + type: 'string', + enum: ['default', 'dark', 'minimal'], + default: 'default', + }, }, }, } @@ -30,15 +49,36 @@ const schema = { type: 'object', required: ['resetLink'], properties: { + subject: { + type: 'string', + default: 'Reset your password', + }, resetLink: { type: 'string', + format: 'uri', default: 'https://example.com/reset', }, + languages: { + type: 'array', + items: { + type: 'string', + enum: ['en', 'es', 'fr'], + }, + default: ['en'], + }, + theme: { + type: 'string', + enum: ['default', 'dark', 'minimal'], + default: 'default', + }, }, } export const ForgotPassword = ({ + subject = schema.properties.subject.default, resetLink = schema.properties.resetLink.default, + languages = schema.properties.languages.default, + theme = schema.properties.theme.default, }) => { return ( @@ -47,7 +87,7 @@ export const ForgotPassword = ({ style={{ fontFamily: 'Arial, sans-serif', backgroundColor: '#f4f4f4' }} > - Reset Your Password + {subject} We received a request to reset your password. Click the button below to set a new password: diff --git a/package-lock.json b/package-lock.json index 3c8a932..fc4b7cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2311,6 +2311,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2326,6 +2343,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -3885,21 +3909,22 @@ } } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "ajv": "^8.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-keywords": { @@ -3915,13 +3940,6 @@ "ajv": "^8.8.2" } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -7713,39 +7731,6 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", @@ -7762,21 +7747,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-html": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", @@ -8077,21 +8047,22 @@ "webpack": ">=5" } }, - "node_modules/babel-loader/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/babel-loader/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "ajv": "^8.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/babel-loader/node_modules/ajv-keywords": { @@ -8141,13 +8112,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-loader/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/babel-loader/node_modules/locate-path": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", @@ -10250,6 +10214,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -10321,6 +10302,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -10473,7 +10461,8 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -12064,9 +12053,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -14905,6 +14896,37 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", @@ -16356,6 +16378,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -16596,21 +16619,22 @@ } } }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/webpack-dev-middleware/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "ajv": "^8.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { @@ -16626,13 +16650,6 @@ "ajv": "^8.8.2" } }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", diff --git a/package.json b/package.json index a8e94a5..60b5dd8 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", + "test": "node --test **/*.test.mjs", + "test:email-render": "node app/api/service/emails/render/route.test.mjs", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "email": "email dev" From cbd89ddfbee9a73ad3b2318e3209f080b79a57d9 Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 13:58:23 -0700 Subject: [PATCH 03/12] Remove redundant OpenAPI spec file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The email service now provides schema information dynamically via /api/service/emails endpoint, making the static YAML specification file unnecessary and potentially outdated. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/service/emails/render/route.js | 10 +- architecture/openapi.yaml | 127 ----------- emails/index.ts | 1 + emails/purchase.jsx | 292 +++++++++++++++++++++++++ 4 files changed, 301 insertions(+), 129 deletions(-) delete mode 100644 architecture/openapi.yaml create mode 100644 emails/purchase.jsx diff --git a/app/api/service/emails/render/route.js b/app/api/service/emails/render/route.js index a0bb59d..6e68d65 100644 --- a/app/api/service/emails/render/route.js +++ b/app/api/service/emails/render/route.js @@ -7,8 +7,10 @@ import { checkEmailServiceAuth } from 'util/server-only/email-service' const EmailComponents = Object.values(BaseEmailComponents).reduce( (acc, Component) => ({ - [Component.definition.version || Component.definition.name]: - ValidatedEmail(Component, Component.definition.schema), + [Component.definition.version || Component.definition.name]: ValidatedEmail( + Component, + Component.definition.schema + ), ...acc, }), {} @@ -21,14 +23,17 @@ const EmailComponents = Object.values(BaseEmailComponents).reduce( export async function POST(req) { const authError = checkEmailServiceAuth(req) + console.log('>>> authError', authError) if (authError) return authError try { const body = await req.json() + console.log('>>> body', JSON.stringify(body, null, 2)) const { template_id, data = {} } = body const EmailComponent = EmailComponents[template_id] + console.log('>>> EmailComponent', EmailComponent) if (!EmailComponent) { return NextResponse.json( { error: `Email template "${template_id}" not found` }, @@ -39,6 +44,7 @@ export async function POST(req) { const html = await render(React.createElement(EmailComponent, data)) return NextResponse.json({ html }) } catch (error) { + console.log('>>> error', error) console.error('Email render error:', error) return NextResponse.json( error instanceof ValidationError diff --git a/architecture/openapi.yaml b/architecture/openapi.yaml deleted file mode 100644 index 236bf25..0000000 --- a/architecture/openapi.yaml +++ /dev/null @@ -1,127 +0,0 @@ -openapi: 3.1.0 -info: - title: Email Specifications - description: Defines the structure and data requirements for emails sent by the backend - version: 1.0.0 - -# Email template provider configuration -x-email-template-providers: - endpoint: "POST /api/service/emails/render" - description: "Renders email templates with provided data and returns HTML" - -# Use vendor extensions for email-specific metadata -x-email-templates: - forgot-password: - name: "Forgot Password" - description: "Email that is sent when a user submits the forgot password form." - trigger: "user_forgot_password" - subject: "Reset your password" - template-id: "forgot-password-v1" - -components: - schemas: - # Common schemas that can be reused - Language: - type: string - enum: [en, es, fr] - default: en - - Theme: - type: string - enum: [default, dark, minimal] - default: default - - # Email-specific schemas (must end with "Email") - ForgotPasswordEmail: - type: object - description: Data required for forgot password email template - properties: - subject: - type: string - default: "Reset your password" - example: "Reset your password" - resetLink: - type: string - format: uri - default: "https://example.com/reset" - example: "https://example.com/reset?token=abc123" - languages: - type: array - items: - $ref: '#/components/schemas/Language' - default: ["en"] - theme: - $ref: '#/components/schemas/Theme' - required: - - resetLink - -paths: - /api/service/emails/render: - post: - summary: Render email template - description: Renders an email template with provided data and returns HTML - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - template_id: - type: string - description: Template ID/version identifier - example: "forgot-password-v1" - data: - type: object - description: Template data - additionalProperties: true - example: - subject: "Reset your password" - resetLink: "https://example.com/reset?token=abc123" - languages: ["en"] - theme: "default" - required: - - template_id - examples: - forgot-password: - summary: Forgot Password Email - value: - template_id: "forgot-password-v1" - data: - subject: "Reset your password" - resetLink: "https://example.com/reset?token=abc123" - languages: ["en"] - theme: "default" - responses: - '200': - description: Successfully rendered email template - content: - application/json: - schema: - type: object - properties: - html: - type: string - description: Rendered HTML email content - '400': - description: Bad request - validation error or invalid data - content: - application/json: - schema: - type: object - properties: - error: - type: string - data: - type: object - description: Additional error data for validation errors - '404': - description: Email template not found - content: - application/json: - schema: - type: object - properties: - error: - type: string - example: 'Email template "Template Name" not found' \ No newline at end of file diff --git a/emails/index.ts b/emails/index.ts index 1f16357..57f4159 100644 --- a/emails/index.ts +++ b/emails/index.ts @@ -1 +1,2 @@ export * from './forgot-password' +export * from './purchase' diff --git a/emails/purchase.jsx b/emails/purchase.jsx new file mode 100644 index 0000000..759a571 --- /dev/null +++ b/emails/purchase.jsx @@ -0,0 +1,292 @@ +import { + Button, + Html, + Head, + Body, + Container, + Section, + Text, + Img, + Hr, + Row, + Column, +} from '@react-email/components' +import * as React from 'react' + +const definition = { + name: 'Purchase', + description: 'Example email for a purchase with multiple items', + version: 'purchase-confirmation-v1', + schema: { + type: 'object', + required: ['items', 'order_number'], + properties: { + subject: { + type: 'string', + default: 'Purchase Confirmation', + }, + email: { + type: 'string', + format: 'email', + }, + order_number: { + type: 'number', + }, + total: { + type: 'number', + }, + languages: { + type: 'array', + items: { + type: 'string', + }, + default: ['en'], + }, + theme: { + type: 'string', + default: 'default', + }, + items: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + description: { + type: 'string', + }, + price: { + type: 'number', + }, + image: { + type: 'string', + }, + }, + required: ['name', 'details', 'price', 'image'], + }, + default: [ + { + name: 'Premium Subscription', + details: '1-year premium plan with all features included', + price: 99.99, + image: 'https://via.placeholder.com/60x60?text=Premium', + }, + ], + }, + }, + }, +} + +export const Purchase = ({ + subject = definition.schema.properties.subject.default, + email, + order_number, + total, + languages = definition.schema.properties.languages.default, + theme = definition.schema.properties.theme.default, + items = definition.schema.properties.items.default, +}) => { + const calculatedTotal = total || items.reduce((sum, item) => sum + item.price, 0) + + return ( + + + + + + {subject} + + + + Thank you for your purchase! Here's your order summary for order #{order_number}: + + +
+ {items.map((item, index) => ( +
+ + + {item.name} + + + + {item.name} + + + {item.details} + + + ${item.price.toFixed(2)} + + + + {index < items.length - 1 && ( +
+ )} +
+ ))} + +
+ + + + + Total: ${calculatedTotal.toFixed(2)} + + + +
+ + + + + If you have any questions about your order, please contact our + support team. + +
+ + + ) +} + +const schema = { + type: 'object', + required: ['items', 'order_number'], + properties: { + subject: { + type: 'string', + default: 'Purchase Confirmation', + }, + email: { + type: 'string', + format: 'email', + }, + order_number: { + type: 'number', + }, + total: { + type: 'number', + }, + languages: { + type: 'array', + items: { + type: 'string', + }, + default: ['en'], + }, + theme: { + type: 'string', + default: 'default', + }, + items: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + details: { + type: 'string', + }, + price: { + type: 'number', + }, + image: { + type: 'string', + }, + }, + required: ['name', 'details', 'price', 'image'], + }, + default: [ + { + name: 'Premium Subscription', + description: '1-year premium plan with all features included', + price: 99.99, + image: 'https://via.placeholder.com/60x60?text=Premium', + }, + ], + }, + }, +} + +Purchase.schema = schema +Purchase.definition = definition + +export default Purchase From 9cb227f60c464b83376e3aac165bf89413501a31 Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 14:04:29 -0700 Subject: [PATCH 04/12] Remove commented TypeScript interface from JS file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleaned up unused commented code that was not applicable in JavaScript context. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/service/emails/render/route.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/api/service/emails/render/route.js b/app/api/service/emails/render/route.js index 6e68d65..9b1f5c8 100644 --- a/app/api/service/emails/render/route.js +++ b/app/api/service/emails/render/route.js @@ -16,10 +16,6 @@ const EmailComponents = Object.values(BaseEmailComponents).reduce( {} ) -// interface RequestBody { -// template_id: string; -// data?: Record; -// } export async function POST(req) { const authError = checkEmailServiceAuth(req) From 62c0d235c421abf452be559ba09957fe6494ffa4 Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 14:06:45 -0700 Subject: [PATCH 05/12] Remove test scripts and test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleaned up unused test configuration and test files for email render route. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/service/emails/render/route.test.js | 171 ------------------- app/api/service/emails/render/route.test.mjs | 94 ---------- package.json | 2 - 3 files changed, 267 deletions(-) delete mode 100644 app/api/service/emails/render/route.test.js delete mode 100644 app/api/service/emails/render/route.test.mjs diff --git a/app/api/service/emails/render/route.test.js b/app/api/service/emails/render/route.test.js deleted file mode 100644 index 05863a0..0000000 --- a/app/api/service/emails/render/route.test.js +++ /dev/null @@ -1,171 +0,0 @@ -import { NextRequest } from 'next/server' -import { POST } from './route' - -// Mock the dependencies -jest.mock('@react-email/render', () => ({ - render: jest.fn() -})) - -jest.mock('emails', () => ({ - ForgotPassword: { - definition: { - name: 'Forgot Password', - version: 'forgot-password-v1', - schema: { - type: 'object', - required: ['resetLink'], - properties: { - subject: { type: 'string', default: 'Reset your password' }, - resetLink: { type: 'string', format: 'uri', default: 'https://example.com/reset' }, - languages: { type: 'array', items: { type: 'string' }, default: ['en'] }, - theme: { type: 'string', default: 'default' } - } - } - } - } -})) - -jest.mock('emails/utils/validation', () => ({ - ValidationError: class ValidationError extends Error { - constructor(message, data) { - super(message) - this.data = data - } - }, - ValidatedEmail: jest.fn((Component) => Component) -})) - -jest.mock('util/server-only/email-service', () => ({ - checkEmailServiceAuth: jest.fn(() => null) // Return null for successful auth -})) - -describe('/api/service/emails/render', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should render forgot-password email template successfully', async () => { - const { render } = require('@react-email/render') - render.mockResolvedValue('Rendered email content') - - const requestBody = { - template_id: 'forgot-password-v1', - data: { - subject: 'Reset your password', - resetLink: 'https://example.com/reset?token=abc123', - languages: ['en'], - theme: 'default' - } - } - - const request = new NextRequest('http://localhost:3000/api/service/emails/render', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody) - }) - - const response = await POST(request) - const responseData = await response.json() - - expect(response.status).toBe(200) - expect(responseData).toEqual({ - html: 'Rendered email content' - }) - expect(render).toHaveBeenCalledTimes(1) - }) - - it('should return 404 for non-existent template', async () => { - const requestBody = { - template_id: 'non-existent-template', - data: {} - } - - const request = new NextRequest('http://localhost:3000/api/service/emails/render', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody) - }) - - const response = await POST(request) - const responseData = await response.json() - - expect(response.status).toBe(404) - expect(responseData).toEqual({ - error: 'Email template "non-existent-template" not found' - }) - }) - - it('should handle validation errors', async () => { - const { render } = require('@react-email/render') - const { ValidationError } = require('emails/utils/validation') - - render.mockRejectedValue(new ValidationError('Invalid data', { field: 'resetLink' })) - - const requestBody = { - template_id: 'forgot-password-v1', - data: { - resetLink: 'invalid-url' - } - } - - const request = new NextRequest('http://localhost:3000/api/service/emails/render', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody) - }) - - const response = await POST(request) - const responseData = await response.json() - - expect(response.status).toBe(400) - expect(responseData).toEqual({ - error: 'Invalid data', - data: { field: 'resetLink' } - }) - }) - - it('should handle authentication errors', async () => { - const { checkEmailServiceAuth } = require('util/server-only/email-service') - const authErrorResponse = new Response('Unauthorized', { status: 401 }) - checkEmailServiceAuth.mockReturnValue(authErrorResponse) - - const requestBody = { - template_id: 'forgot-password-v1', - data: {} - } - - const request = new NextRequest('http://localhost:3000/api/service/emails/render', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody) - }) - - const response = await POST(request) - - expect(response.status).toBe(401) - }) - - it('should handle malformed JSON', async () => { - const request = new NextRequest('http://localhost:3000/api/service/emails/render', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: 'invalid json' - }) - - const response = await POST(request) - const responseData = await response.json() - - expect(response.status).toBe(400) - expect(responseData.error).toBeDefined() - }) -}) \ No newline at end of file diff --git a/app/api/service/emails/render/route.test.mjs b/app/api/service/emails/render/route.test.mjs deleted file mode 100644 index 932965a..0000000 --- a/app/api/service/emails/render/route.test.mjs +++ /dev/null @@ -1,94 +0,0 @@ -import { test, describe } from 'node:test' -import assert from 'node:assert' - -// Simple test for the POST endpoint using fetch -describe('POST /api/service/emails/render', () => { - test('should render forgot-password email template successfully', async () => { - const payload = { - template_id: 'forgot-password-v1', - data: { - subject: 'Reset your password', - resetLink: 'https://example.com/reset?token=abc123', - languages: ['en'], - theme: 'default' - } - } - - // Note: This test assumes the server is running on localhost:3000 - // You may need to adjust the URL based on your development setup - try { - const response = await fetch('http://localhost:3000/api/service/emails/render', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer test-token' - }, - body: JSON.stringify(payload) - }) - - const data = await response.json() - - // Test that the response is successful - assert.strictEqual(response.status, 200, `Expected status 200, got ${response.status}`) - - // Test that the response contains HTML - assert.ok(data.html, 'Response should contain html field') - assert.strictEqual(typeof data.html, 'string', 'HTML should be a string') - - // Test that the HTML contains expected content - assert.ok(data.html.includes('Reset your password'), 'HTML should contain the subject') - assert.ok(data.html.includes('https://example.com/reset?token=abc123'), 'HTML should contain the reset link') - - console.log('✅ Test passed: Email template rendered successfully') - console.log('Response data:', JSON.stringify(data, null, 2)) - - } catch (error) { - if (error.code === 'ECONNREFUSED') { - console.log('⚠️ Server not running. Start the dev server with: npm run dev') - console.log(' Then run this test again') - return - } - throw error - } - }) - - test('should return 404 for non-existent template', async () => { - const payload = { - template_id: 'non-existent-template', - data: {} - } - - try { - const response = await fetch('http://localhost:3000/api/service/emails/render', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer test-token' - }, - body: JSON.stringify(payload) - }) - - const data = await response.json() - - assert.strictEqual(response.status, 404, `Expected status 404, got ${response.status}`) - assert.ok(data.error, 'Response should contain error field') - assert.ok(data.error.includes('not found'), 'Error should mention template not found') - - console.log('✅ Test passed: 404 error for non-existent template') - - } catch (error) { - if (error.code === 'ECONNREFUSED') { - console.log('⚠️ Server not running. Start the dev server with: npm run dev') - return - } - throw error - } - }) -}) - -// Manual test runner if you want to run this directly -if (import.meta.url === `file://${process.argv[1]}`) { - console.log('🧪 Running email render API tests...') - console.log('Make sure your development server is running (npm run dev)') - console.log('') -} \ No newline at end of file diff --git a/package.json b/package.json index 60b5dd8..a8e94a5 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,6 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "test": "node --test **/*.test.mjs", - "test:email-render": "node app/api/service/emails/render/route.test.mjs", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "email": "email dev" From a2ad85fad6bae9d05dcfb25d05aa5f85a72c784a Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 14:45:49 -0700 Subject: [PATCH 06/12] npm install --- package-lock.json | 1650 +++++---------------------------------------- 1 file changed, 165 insertions(+), 1485 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc4b7cc..83b046f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1839,74 +1839,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", @@ -1924,937 +1856,239 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=18" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", - "dependencies": { - "@floating-ui/utils": "^0.2.1" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", - "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.1" + "node_modules/@floating-ui/dom": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", + "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.1" } }, "node_modules/@floating-ui/react": { "version": "0.26.28", "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", - "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" - }, - "node_modules/@hookform/resolvers": { - "version": "2.9.10", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.10.tgz", - "integrity": "sha512-JIL1DgJIlH9yuxcNGtyhsWX/PgNltz+5Gr6+8SX9fhXc/hPbEIk6wPI82nhgvp3uUb6ZfAM5mqg/x7KR7NAb+A==", - "peerDependencies": { - "react-hook-form": "^7.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", - "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.1.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", - "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.1.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", - "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", - "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", - "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", - "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", - "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", - "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", - "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", - "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", - "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.1.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", - "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.1.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", - "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.1.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", - "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.1.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", - "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", - "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@floating-ui/react-dom": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", + "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", - "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "2.9.10", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.10.tgz", + "integrity": "sha512-JIL1DgJIlH9yuxcNGtyhsWX/PgNltz+5Gr6+8SX9fhXc/hPbEIk6wPI82nhgvp3uUb6ZfAM5mqg/x7KR7NAb+A==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, "dependencies": { - "@emnapi/runtime": "^1.4.3" + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=10.10.0" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", - "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=12.22" }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/sharp-win32-ia32": { + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", - "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", "cpu": [ - "ia32" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", - "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", "cpu": [ - "x64" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "darwin" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } @@ -3059,118 +2293,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", - "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", - "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", - "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", - "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", - "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", - "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", - "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5866,114 +4988,12 @@ }, "node_modules/@sentry/cli-darwin": { "version": "2.39.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz", - "integrity": "sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ==", - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm": { - "version": "2.39.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz", - "integrity": "sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ==", - "cpu": [ - "arm" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm64": { - "version": "2.39.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz", - "integrity": "sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw==", - "cpu": [ - "arm64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-i686": { - "version": "2.39.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz", - "integrity": "sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-x64": { - "version": "2.39.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz", - "integrity": "sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA==", - "cpu": [ - "x64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-i686": { - "version": "2.39.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz", - "integrity": "sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-x64": { - "version": "2.39.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz", - "integrity": "sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw==", - "cpu": [ - "x64" - ], + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz", + "integrity": "sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ==", "license": "BSD-3-Clause", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": ">=10" @@ -15040,29 +14060,6 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/sharp/node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", @@ -15080,323 +14077,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/sharp/node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/sharp/node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/sharp/node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/sharp/node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/sharp/node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/sharp/node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/sharp/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", From 251d7e44100154dc2dcead4780d49e310910bf4c Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 15:01:10 -0700 Subject: [PATCH 07/12] Remove redundant schema variables in email templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Eliminate duplicate schema definitions in forgot-password.jsx and purchase.jsx - Reference definition.schema instead of maintaining separate schema variables - Reduces code duplication while maintaining same functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- emails/forgot-password.jsx | 38 +++-------------------- emails/purchase.jsx | 63 +------------------------------------- 2 files changed, 6 insertions(+), 95 deletions(-) diff --git a/emails/forgot-password.jsx b/emails/forgot-password.jsx index 1863063..52457a9 100644 --- a/emails/forgot-password.jsx +++ b/emails/forgot-password.jsx @@ -45,40 +45,12 @@ const definition = { }, } -const schema = { - type: 'object', - required: ['resetLink'], - properties: { - subject: { - type: 'string', - default: 'Reset your password', - }, - resetLink: { - type: 'string', - format: 'uri', - default: 'https://example.com/reset', - }, - languages: { - type: 'array', - items: { - type: 'string', - enum: ['en', 'es', 'fr'], - }, - default: ['en'], - }, - theme: { - type: 'string', - enum: ['default', 'dark', 'minimal'], - default: 'default', - }, - }, -} export const ForgotPassword = ({ - subject = schema.properties.subject.default, - resetLink = schema.properties.resetLink.default, - languages = schema.properties.languages.default, - theme = schema.properties.theme.default, + subject = definition.schema.properties.subject.default, + resetLink = definition.schema.properties.resetLink.default, + languages = definition.schema.properties.languages.default, + theme = definition.schema.properties.theme.default, }) => { return ( @@ -145,7 +117,7 @@ const footer = { textAlign: 'center', } -ForgotPassword.schema = schema +ForgotPassword.schema = definition.schema ForgotPassword.definition = definition export default ForgotPassword diff --git a/emails/purchase.jsx b/emails/purchase.jsx index 759a571..1515376 100644 --- a/emails/purchase.jsx +++ b/emails/purchase.jsx @@ -224,69 +224,8 @@ export const Purchase = ({ ) } -const schema = { - type: 'object', - required: ['items', 'order_number'], - properties: { - subject: { - type: 'string', - default: 'Purchase Confirmation', - }, - email: { - type: 'string', - format: 'email', - }, - order_number: { - type: 'number', - }, - total: { - type: 'number', - }, - languages: { - type: 'array', - items: { - type: 'string', - }, - default: ['en'], - }, - theme: { - type: 'string', - default: 'default', - }, - items: { - type: 'array', - minItems: 1, - items: { - type: 'object', - properties: { - name: { - type: 'string', - }, - details: { - type: 'string', - }, - price: { - type: 'number', - }, - image: { - type: 'string', - }, - }, - required: ['name', 'details', 'price', 'image'], - }, - default: [ - { - name: 'Premium Subscription', - description: '1-year premium plan with all features included', - price: 99.99, - image: 'https://via.placeholder.com/60x60?text=Premium', - }, - ], - }, - }, -} -Purchase.schema = schema +Purchase.schema = definition.schema Purchase.definition = definition export default Purchase From 1fa3a166b1ce534a83a75bbc95c5c47925b7ce61 Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 15:04:50 -0700 Subject: [PATCH 08/12] Remove debug console.log statements from email render route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove console.log('>>> authError', authError) - Remove console.log('>>> body', JSON.stringify(body, null, 2)) - Remove console.log('>>> EmailComponent', EmailComponent) - Remove console.log('>>> error', error) - Keep legitimate console.error for error handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/service/emails/render/route.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/api/service/emails/render/route.js b/app/api/service/emails/render/route.js index 9b1f5c8..a6a8375 100644 --- a/app/api/service/emails/render/route.js +++ b/app/api/service/emails/render/route.js @@ -19,17 +19,13 @@ const EmailComponents = Object.values(BaseEmailComponents).reduce( export async function POST(req) { const authError = checkEmailServiceAuth(req) - console.log('>>> authError', authError) if (authError) return authError try { const body = await req.json() - console.log('>>> body', JSON.stringify(body, null, 2)) const { template_id, data = {} } = body const EmailComponent = EmailComponents[template_id] - - console.log('>>> EmailComponent', EmailComponent) if (!EmailComponent) { return NextResponse.json( { error: `Email template "${template_id}" not found` }, @@ -40,7 +36,6 @@ export async function POST(req) { const html = await render(React.createElement(EmailComponent, data)) return NextResponse.json({ html }) } catch (error) { - console.log('>>> error', error) console.error('Email render error:', error) return NextResponse.json( error instanceof ValidationError From 1ee17133b878e3160f7cb4f6e60016bd8566ae25 Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 15:09:10 -0700 Subject: [PATCH 09/12] Remove purchase email template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- emails/index.ts | 1 - emails/purchase.jsx | 231 -------------------------------------------- 2 files changed, 232 deletions(-) delete mode 100644 emails/purchase.jsx diff --git a/emails/index.ts b/emails/index.ts index 57f4159..1f16357 100644 --- a/emails/index.ts +++ b/emails/index.ts @@ -1,2 +1 @@ export * from './forgot-password' -export * from './purchase' diff --git a/emails/purchase.jsx b/emails/purchase.jsx deleted file mode 100644 index 1515376..0000000 --- a/emails/purchase.jsx +++ /dev/null @@ -1,231 +0,0 @@ -import { - Button, - Html, - Head, - Body, - Container, - Section, - Text, - Img, - Hr, - Row, - Column, -} from '@react-email/components' -import * as React from 'react' - -const definition = { - name: 'Purchase', - description: 'Example email for a purchase with multiple items', - version: 'purchase-confirmation-v1', - schema: { - type: 'object', - required: ['items', 'order_number'], - properties: { - subject: { - type: 'string', - default: 'Purchase Confirmation', - }, - email: { - type: 'string', - format: 'email', - }, - order_number: { - type: 'number', - }, - total: { - type: 'number', - }, - languages: { - type: 'array', - items: { - type: 'string', - }, - default: ['en'], - }, - theme: { - type: 'string', - default: 'default', - }, - items: { - type: 'array', - minItems: 1, - items: { - type: 'object', - properties: { - name: { - type: 'string', - }, - description: { - type: 'string', - }, - price: { - type: 'number', - }, - image: { - type: 'string', - }, - }, - required: ['name', 'details', 'price', 'image'], - }, - default: [ - { - name: 'Premium Subscription', - details: '1-year premium plan with all features included', - price: 99.99, - image: 'https://via.placeholder.com/60x60?text=Premium', - }, - ], - }, - }, - }, -} - -export const Purchase = ({ - subject = definition.schema.properties.subject.default, - email, - order_number, - total, - languages = definition.schema.properties.languages.default, - theme = definition.schema.properties.theme.default, - items = definition.schema.properties.items.default, -}) => { - const calculatedTotal = total || items.reduce((sum, item) => sum + item.price, 0) - - return ( - - - - - - {subject} - - - - Thank you for your purchase! Here's your order summary for order #{order_number}: - - -
- {items.map((item, index) => ( -
- - - {item.name} - - - - {item.name} - - - {item.details} - - - ${item.price.toFixed(2)} - - - - {index < items.length - 1 && ( -
- )} -
- ))} - -
- - - - - Total: ${calculatedTotal.toFixed(2)} - - - -
- - - - - If you have any questions about your order, please contact our - support team. - -
- - - ) -} - - -Purchase.schema = definition.schema -Purchase.definition = definition - -export default Purchase From 2ccd77001a95be6db892108c28ddc5b51903adc2 Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Thu, 21 Aug 2025 15:20:02 -0700 Subject: [PATCH 10/12] Update lock file --- package-lock.json | 105 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/package-lock.json b/package-lock.json index 83b046f..0c1d1b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15663,6 +15663,111 @@ "optional": true } } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", + "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", + "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", + "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", + "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", + "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", + "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", + "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } From 7b38a80b5a68aabe16528527344e33ce8eafc80e Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Fri, 22 Aug 2025 14:21:36 -0700 Subject: [PATCH 11/12] Use OpenAPI schema --- emails.yaml | 51 +++++++ emails/_util/validation.js | 115 ++++++++++------ emails/forgot-password.jsx | 37 +----- package-lock.json | 266 ++++++++++++++++++++----------------- package.json | 3 + 5 files changed, 275 insertions(+), 197 deletions(-) create mode 100644 emails.yaml diff --git a/emails.yaml b/emails.yaml new file mode 100644 index 0000000..078e019 --- /dev/null +++ b/emails.yaml @@ -0,0 +1,51 @@ +openapi: 3.1.0 +info: + title: Email Specifications + description: Defines the structure and data requirements for emails sent by the backend + version: 1.0.0 + +# Email template provider configuration +x-email-template-providers: + endpoint: "https://example.com/api/service/emails/render" + +# Use vendor extensions for email-specific metadata +x-email-templates: + forgot-password: + name: "Forgot Password" + description: "Email that is sent when a user submits the forgot password form." + trigger: "user_forgot_password" + subject: "Reset your password" + template-id: "forgot-password-v1" + +components: + schemas: + # Common schemas that can be reused + Language: + type: string + enum: [en, es, fr] + default: en + + Theme: + type: string + enum: [default, dark, minimal] + default: default + + # Email-specific schemas (must end with "Email") + ForgotPasswordEmail: + type: object + description: Data required for forgot password email template + properties: + resetLink: + type: string + format: uri + default: "https://example.com/reset" + example: "https://example.com/reset?token=abc123" + languages: + type: array + items: + $ref: '#/components/schemas/Language' + default: ["en", "es", "fr"] + theme: + $ref: '#/components/schemas/Theme' + required: + - resetLink \ No newline at end of file diff --git a/emails/_util/validation.js b/emails/_util/validation.js index 158e771..d918696 100644 --- a/emails/_util/validation.js +++ b/emails/_util/validation.js @@ -1,4 +1,9 @@ import React from 'react' +import Ajv from 'ajv' +import addFormats from 'ajv-formats' +import fs from 'fs' +import path from 'path' +import yaml from 'yaml' export class ValidationError extends Error { constructor(message, data) { @@ -8,50 +13,80 @@ export class ValidationError extends Error { } } +// Cache for loaded OpenAPI spec +let openApiSpec = null + +// Load OpenAPI schema +const getOpenApiSpec = () => { + if (!openApiSpec) { + const specPath = path.join(process.cwd(), 'emails.yaml') + const specContent = fs.readFileSync(specPath, 'utf8') + openApiSpec = yaml.parse(specContent) + } + return openApiSpec +} + +// Get schema for a specific email type from OpenAPI spec +const getEmailSchema = (emailType) => { + const spec = getOpenApiSpec() + const schemaName = `${emailType.charAt(0).toUpperCase()}${emailType.slice(1)}Email` + + if (!spec.components?.schemas?.[schemaName]) { + throw new Error(`Email schema not found: ${schemaName}`) + } + + return { + ...spec.components.schemas[schemaName], + // Include referenced schemas in definitions for Ajv + definitions: spec.components.schemas + } +} + export const ValidatedEmail = (Component, schema) => (props) => { - // Basic validation - check required fields - if (schema.required) { - for (const field of schema.required) { - if (props[field] === undefined || props[field] === null) { - throw new ValidationError( - `Missing required field: ${field}`, - { field, value: props[field] } - ) - } + // If schema is provided inline, use the current validation + // Otherwise, try to get schema from OpenAPI spec + let validationSchema = schema + + if (!schema && Component.definition?.version) { + // Extract email type from template version (e.g., "forgot-password-v1" -> "forgotPassword") + const emailType = Component.definition.version + .split('-') + .slice(0, -1) // Remove version suffix + .map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)) + .join('') + + try { + validationSchema = getEmailSchema(emailType) + } catch (error) { + console.warn(`Could not load OpenAPI schema for ${emailType}:`, error.message) + // Fall back to inline schema if available + validationSchema = Component.schema } } - // Basic type and format validation - if (schema.properties) { - for (const [field, fieldSchema] of Object.entries(schema.properties)) { - const value = props[field] - if (value !== undefined) { - // Type validation - if (fieldSchema.type === 'string' && typeof value !== 'string') { - throw new ValidationError( - `Field ${field} must be a string`, - { field, value, expectedType: 'string' } - ) - } - if (fieldSchema.type === 'array' && !Array.isArray(value)) { - throw new ValidationError( - `Field ${field} must be an array`, - { field, value, expectedType: 'array' } - ) - } - - // URI format validation - if (fieldSchema.format === 'uri' && typeof value === 'string') { - try { - new URL(value) - } catch { - throw new ValidationError( - `Field ${field} must be a valid URI`, - { field, value, expectedFormat: 'uri' } - ) - } - } - } + if (validationSchema) { + // Create Ajv instance + const ajv = new Ajv({ allErrors: true }) + addFormats(ajv) + + // Compile the schema + const validate = ajv.compile(validationSchema) + + // Validate props + const valid = validate(props) + + if (!valid) { + const errors = validate.errors.map(error => ({ + field: error.instancePath?.replace('/', '') || error.params?.missingProperty, + message: error.message, + value: error.data, + schemaPath: error.schemaPath + })) + + throw new ValidationError( + `Validation failed: ${errors.map(e => `${e.field}: ${e.message}`).join(', ')}`, + { errors } + ) } } diff --git a/emails/forgot-password.jsx b/emails/forgot-password.jsx index 52457a9..1ba8af4 100644 --- a/emails/forgot-password.jsx +++ b/emails/forgot-password.jsx @@ -15,43 +15,15 @@ const definition = { description: 'Sends a password reset link when a user submits forgot password form.', version: 'forgot-password-v1', - schema: { - type: 'object', - required: ['resetLink'], - properties: { - subject: { - type: 'string', - default: 'Reset your password', - }, - resetLink: { - type: 'string', - format: 'uri', - default: 'https://example.com/reset', - }, - languages: { - type: 'array', - items: { - type: 'string', - enum: ['en', 'es', 'fr'], - }, - default: ['en'], - }, - theme: { - type: 'string', - enum: ['default', 'dark', 'minimal'], - default: 'default', - }, - }, - }, } export const ForgotPassword = ({ - subject = definition.schema.properties.subject.default, - resetLink = definition.schema.properties.resetLink.default, - languages = definition.schema.properties.languages.default, - theme = definition.schema.properties.theme.default, + resetLink = 'https://example.com/reset', + languages = ['en', 'es', 'fr'], + theme = 'default', }) => { + const subject = 'Reset your password' return ( @@ -117,7 +89,6 @@ const footer = { textAlign: 'center', } -ForgotPassword.schema = definition.schema ForgotPassword.definition = definition export default ForgotPassword diff --git a/package-lock.json b/package-lock.json index 0c1d1b8..1403bf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "@sentry/nextjs": "^8.30.0", "@tanstack/react-query": "^5.0.0", "@tanstack/react-query-devtools": "^5.0.3", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "axios": "^1.6.7", "case": "^1.6.3", "date-fns": "^3.6.0", @@ -35,6 +37,7 @@ "react-hook-form": "7.40.0", "react-i18next": "^15.5.2", "sass": "latest", + "yaml": "^2.8.1", "yup": "0.32.11", "zustand": "4.1.5" }, @@ -1829,16 +1832,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", @@ -2293,6 +2286,111 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", + "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", + "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", + "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", + "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", + "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", + "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", + "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6754,7 +6852,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6767,6 +6864,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-html": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", @@ -7945,6 +8059,16 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -9494,7 +9618,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, "funding": [ { "type": "github", @@ -11076,7 +11199,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -13641,7 +13763,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15591,13 +15712,15 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" } }, "node_modules/yocto-queue": { @@ -15663,111 +15786,6 @@ "optional": true } } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", - "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", - "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", - "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", - "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", - "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", - "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", - "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index a8e94a5..319a460 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@sentry/nextjs": "^8.30.0", "@tanstack/react-query": "^5.0.0", "@tanstack/react-query-devtools": "^5.0.3", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "axios": "^1.6.7", "case": "^1.6.3", "date-fns": "^3.6.0", @@ -38,6 +40,7 @@ "react-hook-form": "7.40.0", "react-i18next": "^15.5.2", "sass": "latest", + "yaml": "^2.8.1", "yup": "0.32.11", "zustand": "4.1.5" }, From f765e07d5b03a6b090ce391ba195d0008100d0ac Mon Sep 17 00:00:00 2001 From: Kotaro Fukuo Date: Fri, 22 Aug 2025 14:37:19 -0700 Subject: [PATCH 12/12] Make file name consistent --- emails.yaml => email-type-list.yaml | 0 emails/_util/validation.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename emails.yaml => email-type-list.yaml (100%) diff --git a/emails.yaml b/email-type-list.yaml similarity index 100% rename from emails.yaml rename to email-type-list.yaml diff --git a/emails/_util/validation.js b/emails/_util/validation.js index d918696..802bad4 100644 --- a/emails/_util/validation.js +++ b/emails/_util/validation.js @@ -19,7 +19,7 @@ let openApiSpec = null // Load OpenAPI schema const getOpenApiSpec = () => { if (!openApiSpec) { - const specPath = path.join(process.cwd(), 'emails.yaml') + const specPath = path.join(process.cwd(), 'email-type-list.yaml') const specContent = fs.readFileSync(specPath, 'utf8') openApiSpec = yaml.parse(specContent) }