diff --git a/.changeset/quiet-yaks-wink.md b/.changeset/quiet-yaks-wink.md new file mode 100644 index 000000000..1aeb79a7e --- /dev/null +++ b/.changeset/quiet-yaks-wink.md @@ -0,0 +1,5 @@ +--- +"@logto/connector-mailgun": minor +--- + +add Mailgun connector diff --git a/.changeset/spicy-maps-warn.md b/.changeset/spicy-maps-warn.md new file mode 100644 index 000000000..ab49445e3 --- /dev/null +++ b/.changeset/spicy-maps-warn.md @@ -0,0 +1,5 @@ +--- +"@logto/connector-sendgrid-email": patch +--- + +improve content diff --git a/.vscode/settings.json b/.vscode/settings.json index dfc202bfe..131a4bbb9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,8 @@ "stylelint", "timestamptz", "topbar", - "withtyped" + "withtyped", + "sendgrid", + "mailgun", ] } diff --git a/packages/cli/src/connector/utils.ts b/packages/cli/src/connector/utils.ts index 9e0e21222..0a48d2513 100644 --- a/packages/cli/src/connector/utils.ts +++ b/packages/cli/src/connector/utils.ts @@ -10,6 +10,8 @@ import type { } from '@logto/connector-kit'; import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit'; +import { consoleLog } from '../utils.js'; + import { notImplemented } from './consts.js'; import type { ConnectorFactory } from './types.js'; @@ -29,10 +31,18 @@ export function validateConnectorModule( } } +const supportedImageTypes = Object.freeze({ + '.svg': 'image/svg+xml', + '.png': 'image/png', +}); + +const isSupportedImageType = (extension: string): extension is keyof typeof supportedImageTypes => + Object.keys(supportedImageTypes).includes(extension); + export const readUrl = async ( url: string, baseUrl: string, - type: 'text' | 'svg' + type: 'text' | 'image' ): Promise => { if (!url) { return url; @@ -46,10 +56,20 @@ export const readUrl = async ( return url; } - if (type === 'svg') { - const data = await readFile(path.join(baseUrl, url)); + if (type === 'image') { + const filePath = path.join(baseUrl, url); + const extension = path.extname(filePath); - return `data:image/svg+xml;base64,${data.toString('base64')}`; + if (!isSupportedImageType(extension)) { + consoleLog.warn( + `[readUrl] unexpected image type: ${filePath}, only support ".svg" and ".png". Falling back to empty string.` + ); + return ''; + } + + const data = await readFile(filePath); + + return `data:${supportedImageTypes[extension]};base64,${data.toString('base64')}`; } return readFile(path.join(baseUrl, url), 'utf8'); @@ -61,8 +81,8 @@ export const parseMetadata = async ( ): Promise => { return { ...metadata, - logo: await readUrl(metadata.logo, packagePath, 'svg'), - logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')), + logo: await readUrl(metadata.logo, packagePath, 'image'), + logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'image')), readme: await readUrl(metadata.readme, packagePath, 'text'), configTemplate: metadata.configTemplate && (await readUrl(metadata.configTemplate, packagePath, 'text')), diff --git a/packages/connectors/connector-mailgun/CHANGELOG.md b/packages/connectors/connector-mailgun/CHANGELOG.md new file mode 100644 index 000000000..af7c03015 --- /dev/null +++ b/packages/connectors/connector-mailgun/CHANGELOG.md @@ -0,0 +1 @@ +# @logto/connector-mailgun diff --git a/packages/connectors/connector-mailgun/README.md b/packages/connectors/connector-mailgun/README.md new file mode 100644 index 000000000..e24e859b6 --- /dev/null +++ b/packages/connectors/connector-mailgun/README.md @@ -0,0 +1,101 @@ +# Mailgun email connector + +The official Logto connector for Mailgun email service. + +**Table of contents** + +- [Mailgun email connector](#mailgun-email-connector) + - [Prerequisites](#prerequisites) + - [Basic configuration](#basic-configuration) + - [Deliveries](#deliveries) + - [Config object](#config-object) + - [Usage types](#usage-types) + - [Content config](#content-config) + - [Example](#example) + - [Test Mailgun email connector](#test-mailgun-email-connector) + +## Prerequisites + +- A [Mailgun](https://www.mailgun.com/) account +- An API key from your Mailgun account, requires the permission to send messages (emails). See [Where Can I Find My API Key and SMTP Credentials?](https://help.mailgun.com/hc/en-us/articles/203380100-Where-Can-I-Find-My-API-Key-and-SMTP-Credentials-) for more information. + +## Basic configuration + +- Fill out the `domain` field with the domain you have registered in your Mailgun account. This value can be found in the **Domains** section of the Mailgun dashboard. The domain should be in the format `example.com`, without the `https://` or `http://` prefix. +- Fill out the `apiKey` field with the API key you have generated in your Mailgun account. +- Fill out the `from` field with the email address you want to send emails from. This email address must be registered in your Mailgun account. The email address should be in the format `Sender Name `. + +## Deliveries + +### Config object + +The "Deliveries" section allows you to configure the content of the emails to be sent in different scenarios. It is a JSON key-value map where the key is the usage type and the value is an object containing the content config for the email to be sent. + +```json +{ + "": { + // ... + } +} +``` + +### Usage types + +The following usage types are supported: + +- `Register`: The email to be sent when a user is registering. +- `SignIn`: The email to be sent when a user is signing in. +- `ForgotPassword`: The email to be sent when a user is resetting their password. +- `Generic`: The email to be sent when a user is performing a generic action, for example, testing the email connector. + +> **Note** +> If the usage type is not specified in the deliveries config, the generic email will be sent. If the generic email is not specified, the connector will return an error. + +### Content config + +The connector supports both direct HTML content and Mailgun template. You can use one of them for each usage type. + +In both subject and content, you can use the `{{code}}` placeholder to insert the verification code. + +To use direct HTML content, fill out the following fields: + +- `subject`: The subject of the email to be sent. +- `replyTo`: The email address to be used as the reply-to address. +- `html`: (Required) The HTML content of the email to be sent. +- `text`: The plain text version of the email to be sent. + +To use Mailgun template, fill out the following fields: + +- `subject`: The subject of the email to be sent. +- `replyTo`: The email address to be used as the reply-to address. +- `template`: (Required) The name of the Mailgun template to be used. +- `variables`: The variables to be passed to the Mailgun template. Should be a JSON key-value map since it will be stringified before sending to Mailgun. Note there's no need to include the `code` variable since it will be automatically added by the connector. + +### Example + +The following is an example of the deliveries config: + +```json +{ + "Register": { + "subject": "{{code}} is your verification code", + "replyTo": "Foo ", + "html": "

Welcome to Logto

Your verification code is {{code}}.

", + "text": "Welcome to Logto. Your verification code is {{code}}." + }, + "SignIn": { + "subject": "Welcome back to Logto", + "replyTo": "Foo ", + "template": "logto-sign-in", + "variables": { + "bar": "baz" + } + } +} +``` + +## Test Mailgun email connector + +You can type in an email address and click on "Send" to see whether the settings can work before "Save and Done". + +That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in) diff --git a/packages/connectors/connector-mailgun/logo.png b/packages/connectors/connector-mailgun/logo.png new file mode 100644 index 000000000..2ea7c397c Binary files /dev/null and b/packages/connectors/connector-mailgun/logo.png differ diff --git a/packages/connectors/connector-mailgun/package.json b/packages/connectors/connector-mailgun/package.json new file mode 100644 index 000000000..9169f241d --- /dev/null +++ b/packages/connectors/connector-mailgun/package.json @@ -0,0 +1,51 @@ +{ + "name": "@logto/connector-mailgun", + "version": "1.0.0", + "description": "Mailgun connector for Logto.", + "author": "Silverhand Inc. ", + "dependencies": { + "@logto/connector-kit": "workspace:^1.1.0" + }, + "main": "./lib/index.js", + "module": "./lib/index.js", + "exports": "./lib/index.js", + "license": "MPL-2.0", + "type": "module", + "files": [ + "lib", + "docs", + "logo.svg", + "logo-dark.svg" + ], + "scripts": { + "precommit": "lint-staged", + "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", + "build": "rm -rf lib/ && tsc -p tsconfig.build.json --noEmit && rollup -c", + "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", + "lint": "eslint --ext .ts src", + "lint:report": "pnpm lint --format json --output-file report.json", + "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "pnpm build:test && pnpm test:only", + "test:ci": "pnpm test:only --silent --coverage", + "prepublishOnly": "pnpm build" + }, + "engines": { + "node": "^18.12.0" + }, + "eslintConfig": { + "extends": "@silverhand", + "settings": { + "import/core-modules": [ + "@silverhand/essentials", + "got", + "nock", + "snakecase-keys", + "zod" + ] + } + }, + "prettier": "@silverhand/eslint-config/.prettierrc", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/connectors/connector-mailgun/src/constant.ts b/packages/connectors/connector-mailgun/src/constant.ts new file mode 100644 index 000000000..bc3100944 --- /dev/null +++ b/packages/connectors/connector-mailgun/src/constant.ts @@ -0,0 +1,66 @@ +import type { ConnectorMetadata } from '@logto/connector-kit'; +import { ConnectorConfigFormItemType } from '@logto/connector-kit'; + +export const endpoint = 'https://api.sendgrid.com/v3/mail/send'; + +export const defaultMetadata: ConnectorMetadata = { + id: 'mailgun-email', + target: 'mailgun-email', + platform: null, + name: { + en: 'Mailgun', + }, + logo: './logo.png', + logoDark: null, + description: { + en: 'Mailgun is an email delivery service for sending, receiving, and tracking emails.', + }, + readme: './README.md', + formItems: [ + { + key: 'domain', + label: 'Domain', + type: ConnectorConfigFormItemType.Text, + required: true, + placeholder: 'https://your-mailgun-domain.com', + }, + { + key: 'apiKey', + label: 'API Key', + type: ConnectorConfigFormItemType.Text, + required: true, + placeholder: '', + }, + { + key: 'from', + label: 'Email address to send from', + type: ConnectorConfigFormItemType.Text, + required: true, + placeholder: 'Sender Name ', + }, + { + key: 'deliveries', + label: 'Deliveries', + type: ConnectorConfigFormItemType.Json, + required: true, + defaultValue: { + SignIn: { + subject: 'Logto sign-in template {{code}}', + html: 'Your Logto sign-in verification code is {{code}}. The code will remain active for 10 minutes.', + }, + Register: { + subject: 'Logto sign-up template {{code}}', + html: 'Your Logto sign-up verification code is {{code}}. The code will remain active for 10 minutes.', + }, + ForgotPassword: { + subject: 'Logto reset password template {{code}}', + html: 'Your Logto reset password verification code is {{code}}. The code will remain active for 10 minutes.', + }, + Generic: { + subject: 'Logto generic template {{code}}', + html: 'Your Logto generic verification code is {{code}}. The code will remain active for 10 minutes.', + }, + }, + }, + ], +}; diff --git a/packages/connectors/connector-mailgun/src/index.test.ts b/packages/connectors/connector-mailgun/src/index.test.ts new file mode 100644 index 000000000..563c1fa16 --- /dev/null +++ b/packages/connectors/connector-mailgun/src/index.test.ts @@ -0,0 +1,190 @@ +import nock from 'nock'; + +import { VerificationCodeType } from '@logto/connector-kit'; + +import createMailgunConnector from './index.js'; +import { type MailgunConfig } from './types.js'; + +const { jest } = import.meta; + +const getConfig = jest.fn(); + +const domain = 'example.com'; +const apiKey = 'apiKey'; +const connector = await createMailgunConnector({ + getConfig, +}); +const baseConfig: Partial = { + domain: 'example.com', + apiKey: 'apiKey', + from: 'foo@example.com', +}; + +/** + * Nock helper to assert request auth and body. + * + * @param expectation - The expected request body. + */ +const nockMessages = (expectation: Record) => + nock('https://api.mailgun.net') + .post(`/v3/${domain}/messages`) + .basicAuth({ user: 'api', pass: apiKey }) + .reply((_, body, callback) => { + const params = new URLSearchParams(body); + + for (const [key, value] of Object.entries(expectation)) { + if (Array.isArray(value)) { + expect(value).toEqual(params.getAll(key)); + } else { + expect(params.get(key)).toBe(value); + } + } + + callback(null, [200, 'OK']); + }); + +describe('Maligun connector', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + it('should send email with raw data', async () => { + nockMessages({ + from: baseConfig.from, + to: 'bar@example.com', + subject: 'Verification code is 123456', + html: '

Your verification code is 123456

', + 'h:Reply-To': 'baz@example.com', + }); + + getConfig.mockResolvedValue({ + ...baseConfig, + deliveries: { + [VerificationCodeType.Generic]: { + subject: 'Verification code is {{code}}', + html: '

Your verification code is {{code}}

', + replyTo: 'baz@example.com', + }, + }, + }); + + await connector.sendMessage({ + to: 'bar@example.com', + type: VerificationCodeType.Generic, + payload: { code: '123456' }, + }); + }); + + it('should send email with template', async () => { + nockMessages({ + from: 'foo@example.com', + to: 'bar@example.com', + subject: 'Verification code is 123456', + template: 'template', + 'h:X-Mailgun-Variables': JSON.stringify({ foo: 'bar', code: '123456' }), + }); + + getConfig.mockResolvedValue({ + ...baseConfig, + deliveries: { + [VerificationCodeType.Generic]: { + template: 'template', + variables: { foo: 'bar' }, + subject: 'Verification code is {{code}}', + }, + }, + }); + + await connector.sendMessage({ + to: 'bar@example.com', + type: VerificationCodeType.Generic, + payload: { + code: '123456', + }, + }); + }); + + it('should fall back to generic template if type not found', async () => { + nockMessages({ + from: 'foo@example.com', + to: 'bar@example.com', + subject: 'Verification code is 123456', + template: 'template', + 'h:X-Mailgun-Variables': JSON.stringify({ foo: 'bar', code: '123456' }), + }); + + getConfig.mockResolvedValue({ + ...baseConfig, + deliveries: { + [VerificationCodeType.Generic]: { + template: 'template', + variables: { foo: 'bar' }, + subject: 'Verification code is {{code}}', + }, + }, + }); + + await connector.sendMessage({ + to: 'bar@example.com', + type: VerificationCodeType.ForgotPassword, + payload: { + code: '123456', + }, + }); + }); + + it('should throw error if template not found or type not supported', async () => { + getConfig.mockResolvedValue({ + ...baseConfig, + deliveries: {}, + }); + + await expect( + connector.sendMessage({ + to: '', + type: VerificationCodeType.Generic, + payload: { + code: '123456', + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot('"ConnectorError: template_not_found"'); + + await expect( + connector.sendMessage({ + to: '', + // @ts-expect-error Invalid type + type: 'foo', + payload: { + code: '123456', + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot('"ConnectorError: template_not_supported"'); + }); + + it('should throw error if mailgun returns error', async () => { + getConfig.mockResolvedValue({ + ...baseConfig, + deliveries: { + [VerificationCodeType.Generic]: { + template: 'template', + variables: { foo: 'bar' }, + subject: 'Verification code is {{code}}', + }, + }, + }); + + nock('https://api.mailgun.net').post(`/v3/${domain}/messages`).reply(400, { message: 'error' }); + + await expect( + connector.sendMessage({ + to: '', + type: VerificationCodeType.Generic, + payload: { + code: '123456', + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + '"ConnectorError: {"statusCode":400,"body":"{\\"message\\":\\"error\\"}"}"' + ); + }); +}); diff --git a/packages/connectors/connector-mailgun/src/index.ts b/packages/connectors/connector-mailgun/src/index.ts new file mode 100644 index 000000000..4ceaae82e --- /dev/null +++ b/packages/connectors/connector-mailgun/src/index.ts @@ -0,0 +1,102 @@ +import { got, HTTPError } from 'got'; + +import type { + GetConnectorConfig, + SendMessageFunction, + CreateConnector, + EmailConnector, +} from '@logto/connector-kit'; +import { + ConnectorError, + ConnectorErrorCodes, + ConnectorType, + validateConfig, + VerificationCodeType, +} from '@logto/connector-kit'; + +import { defaultMetadata } from './constant.js'; +import { + type DeliveryConfig, + mailgunConfigGuard, + supportTemplateGuard, + type MailgunConfig, +} from './types.js'; + +const removeUndefinedKeys = (object: Record) => + Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined)); + +const getDataFromDeliveryConfig = ( + { subject, replyTo, ...rest }: DeliveryConfig, + code: string +): Record => { + const commonData = { + subject: subject?.replaceAll('{{code}}', code), + 'h:Reply-To': replyTo, + }; + + if ('template' in rest) { + return { + ...commonData, + template: rest.template, + 'h:X-Mailgun-Variables': JSON.stringify({ ...rest.variables, code }), + }; + } + + return { + ...commonData, + html: rest.html.replaceAll('{{code}}', code), + text: rest.text?.replaceAll('{{code}}', code), + }; +}; + +const sendMessage = (getConfig: GetConnectorConfig): SendMessageFunction => { + return async ({ to, type: typeInput, payload: { code } }, inputConfig) => { + const config = inputConfig ?? (await getConfig(defaultMetadata.id)); + validateConfig(config, mailgunConfigGuard); + + const { domain, apiKey, from, deliveries } = config; + const type = supportTemplateGuard.safeParse(typeInput); + + if (!type.success) { + throw new ConnectorError(ConnectorErrorCodes.TemplateNotSupported); + } + + const template = deliveries[type.data] ?? deliveries[VerificationCodeType.Generic]; + + if (!template) { + throw new ConnectorError(ConnectorErrorCodes.TemplateNotFound); + } + + try { + return await got.post(`https://api.mailgun.net/v3/${domain}/messages`, { + username: 'api', + password: apiKey, + form: { + from, + to, + ...removeUndefinedKeys(getDataFromDeliveryConfig(template, code)), + }, + }); + } catch (error) { + if (error instanceof HTTPError) { + const { + response: { body, statusCode }, + } = error; + + throw new ConnectorError(ConnectorErrorCodes.General, { statusCode, body }); + } + throw new ConnectorError(ConnectorErrorCodes.General, error); + } + }; +}; + +const createSendGridMailConnector: CreateConnector = async ({ getConfig }) => { + return { + metadata: defaultMetadata, + type: ConnectorType.Email, + configGuard: mailgunConfigGuard, + sendMessage: sendMessage(getConfig), + }; +}; + +export default createSendGridMailConnector; diff --git a/packages/connectors/connector-mailgun/src/type.test.ts b/packages/connectors/connector-mailgun/src/type.test.ts new file mode 100644 index 000000000..f39620705 --- /dev/null +++ b/packages/connectors/connector-mailgun/src/type.test.ts @@ -0,0 +1,65 @@ +import { VerificationCodeType } from '@logto/connector-kit'; + +import { mailgunConfigGuard } from './types.js'; + +describe('Mailgun config guard', () => { + it('should pass with valid config', () => { + const validConfig = { + domain: 'example.com', + apiKey: 'key', + from: 'from', + deliveries: { + [VerificationCodeType.SignIn]: { + html: 'html', + subject: 'subject', + }, + [VerificationCodeType.Register]: { + template: 'template', + variables: {}, + subject: 'subject', + }, + [VerificationCodeType.ForgotPassword]: { + html: 'html', + text: 'text', + subject: 'subject', + }, + [VerificationCodeType.Generic]: { + template: 'template', + variables: {}, + subject: 'subject', + }, + }, + }; + expect(() => mailgunConfigGuard.parse(validConfig)).not.toThrow(); + }); + + it('should allow partial template config', () => { + const validConfig = { + domain: 'example.com', + apiKey: 'key', + from: 'from', + deliveries: { + [VerificationCodeType.SignIn]: { + html: 'html', + subject: 'subject', + }, + }, + }; + expect(() => mailgunConfigGuard.parse(validConfig)).not.toThrow(); + }); + + it('should fail with invalid config', () => { + const invalidConfig = { + domain: 'example.com', + apiKey: 'key', + from: 'from', + deliveries: { + [VerificationCodeType.ForgotPassword]: { + text: 'text', + subject: 'subject', + }, + }, + }; + expect(() => mailgunConfigGuard.parse(invalidConfig)).toThrow(); + }); +}); diff --git a/packages/connectors/connector-mailgun/src/types.ts b/packages/connectors/connector-mailgun/src/types.ts new file mode 100644 index 000000000..6b965a078 --- /dev/null +++ b/packages/connectors/connector-mailgun/src/types.ts @@ -0,0 +1,76 @@ +import { z } from 'zod'; + +import { VerificationCodeType } from '@logto/connector-kit'; + +export const supportTemplateGuard = z.enum([ + VerificationCodeType.SignIn, + VerificationCodeType.Register, + VerificationCodeType.ForgotPassword, + VerificationCodeType.Generic, +]); + +type SupportTemplate = z.infer; + +type CommonEmailConfig = { + /** Subject of the message. */ + subject?: string; + /** The email address for recipients to reply to. */ + replyTo?: string; +}; + +/** The data to send a regular message (email). */ +type RawEmailConfig = CommonEmailConfig & { + /** HTML version of the message. */ + html: string; + /** Text version of the message. */ + text?: string; +}; + +/** The data to send a template message (email). */ +type TemplateEmailConfig = CommonEmailConfig & { + /** The template name. */ + template: string; + /** The template variables. */ + variables?: Record; +}; + +/** Config object fot a specific template type. */ +export type DeliveryConfig = RawEmailConfig | TemplateEmailConfig; + +const templateConfigGuard = z.union([ + z.object({ + html: z.string(), + text: z.string().optional(), + subject: z.string().optional(), + replyTo: z.string().optional(), + }), + z.object({ + template: z.string(), + variables: z.record(z.unknown()).optional(), + subject: z.string().optional(), + replyTo: z.string().optional(), + }), +]) satisfies z.ZodType; + +export type MailgunConfig = { + /** Mailgun domain. */ + domain: string; + /** Mailgun API key. */ + apiKey: string; + /** The sender of the email, in the form `Sender Name `. */ + from: string; + /** + * The template config object for each template type, while the key is the template type + * and the value is the config object. + */ + deliveries: Partial>; +}; + +export const mailgunConfigGuard = z.object({ + domain: z.string(), + apiKey: z.string(), + from: z.string(), + // Although the type it's expected, this guard should infer required keys. Looks like a mis-implemented in zod. + // See https://github.com/colinhacks/zod/issues/2623 + deliveries: z.record(supportTemplateGuard, templateConfigGuard), +}) satisfies z.ZodType; diff --git a/packages/connectors/connector-sendgrid-email/README.md b/packages/connectors/connector-sendgrid-email/README.md index c6e2abe95..ad73293dd 100644 --- a/packages/connectors/connector-sendgrid-email/README.md +++ b/packages/connectors/connector-sendgrid-email/README.md @@ -1,10 +1,10 @@ -# SendGrid mail connector +# SendGrid email connector The official Logto connector for SendGrid email service. **Table of contents** -- [SendGrid mail connector](#sendgrid-mail-connector) +- [SendGrid email connector](#sendgrid-email-connector) - [Get started](#get-started) - [Register SendGrid account](#register-sendgrid-account) - [Verify senders](#verify-senders) diff --git a/packages/connectors/connector-sendgrid-email/src/constant.ts b/packages/connectors/connector-sendgrid-email/src/constant.ts index 49982af80..40d4ebc08 100644 --- a/packages/connectors/connector-sendgrid-email/src/constant.ts +++ b/packages/connectors/connector-sendgrid-email/src/constant.ts @@ -8,10 +8,8 @@ export const defaultMetadata: ConnectorMetadata = { target: 'sendgrid-mail', platform: null, name: { - en: 'SendGrid Mail Service', - 'zh-CN': 'SendGrid 邮件服务', - 'tr-TR': 'SendGrid EMail Servisi', - ko: 'SendGrid 메일 서비스', + en: 'SendGrid Email', + 'zh-CN': 'SendGrid 邮件', }, logo: './logo.svg', logoDark: null, @@ -28,21 +26,21 @@ export const defaultMetadata: ConnectorMetadata = { label: 'API Key', type: ConnectorConfigFormItemType.Text, required: true, - placeholder: '', + placeholder: '', }, { key: 'fromEmail', label: 'From Email', type: ConnectorConfigFormItemType.Text, required: true, - placeholder: '', + placeholder: 'foo@example.com', }, { key: 'fromName', label: 'From Name', type: ConnectorConfigFormItemType.Text, required: false, - placeholder: '', + placeholder: 'Logto', }, { key: 'templates', diff --git a/packages/connectors/connector-sendgrid-email/src/index.ts b/packages/connectors/connector-sendgrid-email/src/index.ts index 7f278ec64..a3a19985d 100644 --- a/packages/connectors/connector-sendgrid-email/src/index.ts +++ b/packages/connectors/connector-sendgrid-email/src/index.ts @@ -15,14 +15,14 @@ import { } from '@logto/connector-kit'; import { defaultMetadata, endpoint } from './constant.js'; +import { sendGridMailConfigGuard } from './types.js'; import type { - SendGridMailConfig, EmailData, Personalization, Content, PublicParameters, + SendGridMailConfig, } from './types.js'; -import { sendGridMailConfigGuard } from './types.js'; const sendMessage = (getConfig: GetConnectorConfig): SendMessageFunction => @@ -87,7 +87,7 @@ const sendMessage = throw new ConnectorError(ConnectorErrorCodes.General, rawBody); } - throw error; + throw new ConnectorError(ConnectorErrorCodes.General, error); } }; diff --git a/packages/core/src/errors/RequestError/index.ts b/packages/core/src/errors/RequestError/index.ts index 84a8e0b23..aaf0b3066 100644 --- a/packages/core/src/errors/RequestError/index.ts +++ b/packages/core/src/errors/RequestError/index.ts @@ -29,7 +29,13 @@ export default class RequestError extends Error { expose = true, ...interpolation } = typeof input === 'string' ? { code: input } : input; - const message = i18next.t(`errors:${code}`, interpolation); + const message = i18next.t(`errors:${code}`, { + ...interpolation, + interpolation: { + // Disable i18next escape value since it's for API response, we can show HTML tags. + escapeValue: false, + }, + }); super(message); diff --git a/packages/core/src/middleware/koa-connector-error-handler.ts b/packages/core/src/middleware/koa-connector-error-handler.ts index 5bad6e65a..5e7642dcc 100644 --- a/packages/core/src/middleware/koa-connector-error-handler.ts +++ b/packages/core/src/middleware/koa-connector-error-handler.ts @@ -19,7 +19,8 @@ export default function koaConnectorErrorHandler(): Middleware const { code, data } = error; const errorDescriptionGuard = z.object({ errorDescription: z.string() }); - const message = trySafe(() => errorDescriptionGuard.parse(data))?.errorDescription; + const message = + trySafe(() => errorDescriptionGuard.parse(data))?.errorDescription ?? JSON.stringify(data); switch (code) { case ConnectorErrorCodes.InvalidMetadata: diff --git a/packages/phrases/src/locales/de/errors/connector.ts b/packages/phrases/src/locales/de/errors/connector.ts index e80c2d070..f6a0e05db 100644 --- a/packages/phrases/src/locales/de/errors/connector.ts +++ b/packages/phrases/src/locales/de/errors/connector.ts @@ -12,6 +12,7 @@ const connector = { invalid_response: 'Die Antwort des Connectors ist ungültig.', template_not_found: 'Die richtige Vorlage in der Connector-Konfiguration konnte nicht gefunden werden.', + template_not_supported: 'Der Connector unterstützt diesen Vorlagentyp nicht.', rate_limit_exceeded: 'Auslöser-Rate-Limit. Bitte versuchen Sie es später erneut.', not_implemented: '{{method}}: wurde noch nicht implementiert.', social_invalid_access_token: 'Der Access Token des Connectors ist ungültig.', diff --git a/packages/phrases/src/locales/en/errors/connector.ts b/packages/phrases/src/locales/en/errors/connector.ts index 120fa943d..e75fd27f2 100644 --- a/packages/phrases/src/locales/en/errors/connector.ts +++ b/packages/phrases/src/locales/en/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: "The connector's config is invalid.", invalid_response: "The connector's response is invalid.", template_not_found: 'Unable to find correct template in connector config.', + template_not_supported: 'The connector does not support this template type.', rate_limit_exceeded: 'Trigger rate limit. Please try again later.', not_implemented: '{{method}}: has not been implemented yet.', social_invalid_access_token: "The connector's access token is invalid.", diff --git a/packages/phrases/src/locales/es/errors/connector.ts b/packages/phrases/src/locales/es/errors/connector.ts index ebd766013..edd3cc3a8 100644 --- a/packages/phrases/src/locales/es/errors/connector.ts +++ b/packages/phrases/src/locales/es/errors/connector.ts @@ -11,6 +11,7 @@ const connector = { invalid_response: 'La respuesta del conector es inválida.', template_not_found: 'No se puede encontrar la plantilla correcta en la configuración del conector.', + template_not_supported: 'El conector no admite este tipo de plantilla.', rate_limit_exceeded: 'Límite de frecuencia activado. Por favor, inténtalo de nuevo más tarde.', not_implemented: '{{method}}: aún no se ha implementado.', social_invalid_access_token: 'El token de acceso del conector es inválido.', diff --git a/packages/phrases/src/locales/fr/errors/connector.ts b/packages/phrases/src/locales/fr/errors/connector.ts index f76281bd3..42c8a4f12 100644 --- a/packages/phrases/src/locales/fr/errors/connector.ts +++ b/packages/phrases/src/locales/fr/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: "La configuration du connecteur n'est pas valide.", invalid_response: "La réponse du connecteur n'est pas valide.", template_not_found: 'Impossible de trouver le bon modèle dans la configuration du connecteur.', + template_not_supported: 'Le connecteur ne prend pas en charge ce type de modèle.', rate_limit_exceeded: 'Limite de taux déclenchée. Veuillez réessayer plus tard.', not_implemented: "{{method}} : n'a pas encore été mis en œuvre.", social_invalid_access_token: "Le jeton d'accès du connecteur n'est pas valide.", diff --git a/packages/phrases/src/locales/it/errors/connector.ts b/packages/phrases/src/locales/it/errors/connector.ts index ca08ba824..774c3ee2b 100644 --- a/packages/phrases/src/locales/it/errors/connector.ts +++ b/packages/phrases/src/locales/it/errors/connector.ts @@ -11,6 +11,7 @@ const connector = { invalid_response: 'La risposta del connettore non è valida.', template_not_found: 'Impossibile trovare il modello corretto nella configurazione del connettore.', + template_not_supported: 'Il connettore non supporta questo tipo di modello.', rate_limit_exceeded: 'Limite di frequenza attivata. Riprova più tardi.', not_implemented: '{{method}}: non è stato ancora implementato.', social_invalid_access_token: 'Il token di accesso del connettore non è valido.', diff --git a/packages/phrases/src/locales/ja/errors/connector.ts b/packages/phrases/src/locales/ja/errors/connector.ts index 84fa1f459..3f43a75af 100644 --- a/packages/phrases/src/locales/ja/errors/connector.ts +++ b/packages/phrases/src/locales/ja/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: 'コネクタの設定が無効です。', invalid_response: 'コネクタのレスポンスが無効です。', template_not_found: 'コネクタ構成から正しいテンプレートを見つけることができませんでした。', + template_not_supported: 'コネクタはこのテンプレートタイプをサポートしていません。', rate_limit_exceeded: 'トリガーレート制限。後でもう一度お試しください。', not_implemented: '{{method}}:まだ実装されていません。', social_invalid_access_token: 'コネクタのアクセストークンが無効です。', diff --git a/packages/phrases/src/locales/ko/errors/connector.ts b/packages/phrases/src/locales/ko/errors/connector.ts index f756e6d1b..bff0b9af1 100644 --- a/packages/phrases/src/locales/ko/errors/connector.ts +++ b/packages/phrases/src/locales/ko/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: '연동 설정이 유효하지 않아요.', invalid_response: '연동 응답이 유효하지 않아요.', template_not_found: '연동 예제 설정을 찾을 수 없어요.', + template_not_supported: '연동이 이 템플릿 타입을 지원하지 않아요.', rate_limit_exceeded: '트리거 주기 제한. 나중에 다시 시도하세요.', not_implemented: '{{method}}은 아직 구현되지 않았어요.', social_invalid_access_token: '연동 서비스의 Access 토큰이 유효하지 않아요.', diff --git a/packages/phrases/src/locales/pl-pl/errors/connector.ts b/packages/phrases/src/locales/pl-pl/errors/connector.ts index dee651fe4..1437522ad 100644 --- a/packages/phrases/src/locales/pl-pl/errors/connector.ts +++ b/packages/phrases/src/locales/pl-pl/errors/connector.ts @@ -11,6 +11,7 @@ const connector = { invalid_config: 'Konfiguracja łącznika jest nieprawidłowa.', invalid_response: 'Odpowiedź łącznika jest nieprawidłowa.', template_not_found: 'Nie można znaleźć poprawnego szablonu w konfiguracji łącznika.', + template_not_supported: 'Łącznik nie obsługuje tego typu szablonu.', rate_limit_exceeded: 'Ograniczenie szybkości wywołań. Spróbuj ponownie później.', not_implemented: '{{method}}: jeszcze nie zaimplementowano.', social_invalid_access_token: 'Token dostępu łącznika jest nieprawidłowy.', diff --git a/packages/phrases/src/locales/pt-br/errors/connector.ts b/packages/phrases/src/locales/pt-br/errors/connector.ts index 82fdc50a8..2b181ff9a 100644 --- a/packages/phrases/src/locales/pt-br/errors/connector.ts +++ b/packages/phrases/src/locales/pt-br/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: 'A configuração do conector é inválida.', invalid_response: 'A resposta do conector é inválida.', template_not_found: 'Não foi possível encontrar o modelo correto na configuração do conector.', + template_not_supported: 'O conector não suporta esse tipo de modelo.', rate_limit_exceeded: 'Limite de taxa de acionamento. Tente novamente mais tarde.', not_implemented: '{{method}}: ainda não foi implementado.', social_invalid_access_token: 'O token de acesso do conector é inválido.', diff --git a/packages/phrases/src/locales/pt-pt/errors/connector.ts b/packages/phrases/src/locales/pt-pt/errors/connector.ts index b88ba4c87..d17a1a3b1 100644 --- a/packages/phrases/src/locales/pt-pt/errors/connector.ts +++ b/packages/phrases/src/locales/pt-pt/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: 'A configuração do conector é inválida.', invalid_response: 'A resposta do conector é inválida.', template_not_found: 'Não foi possível encontrar o modelo correto na configuração do conector.', + template_not_supported: 'O conector não suporta este tipo de modelo.', rate_limit_exceeded: 'Limite de taxa de ativação. Por favor, tente novamente mais tarde.', not_implemented: '{{method}}: ainda não foi implementado.', social_invalid_access_token: 'O token de acesso do conector é inválido.', diff --git a/packages/phrases/src/locales/ru/errors/connector.ts b/packages/phrases/src/locales/ru/errors/connector.ts index 54c29aa4d..d05fbc7ed 100644 --- a/packages/phrases/src/locales/ru/errors/connector.ts +++ b/packages/phrases/src/locales/ru/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: 'Конфигурация коннектора недействительна.', invalid_response: 'Ответ коннектора недействителен.', template_not_found: 'Невозможно найти правильный шаблон в конфигурации коннектора.', + template_not_supported: 'Коннектор не поддерживает этот тип шаблона.', rate_limit_exceeded: 'Превышен лимит запросов. Пожалуйста, попробуйте позже.', not_implemented: '{{method}}: еще не реализован.', social_invalid_access_token: 'Токен доступа коннектора недействителен.', diff --git a/packages/phrases/src/locales/tr-tr/errors/connector.ts b/packages/phrases/src/locales/tr-tr/errors/connector.ts index 69dd90db3..71a645b8e 100644 --- a/packages/phrases/src/locales/tr-tr/errors/connector.ts +++ b/packages/phrases/src/locales/tr-tr/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: 'Bağlayıcının ayarları geçersiz.', invalid_response: 'Bağlayıcının yanıtı geçersiz.', template_not_found: 'Bağlayıcı yapılandırmasında doğru şablon bulunamıyor.', + template_not_supported: 'Bağlayıcı bu şablon türünü desteklemiyor.', rate_limit_exceeded: 'Tetikleyici oran sınırına ulaşıldı. Lütfen daha sonra tekrar deneyin.', not_implemented: '{{method}}: henüz uygulanmadı.', social_invalid_access_token: 'Bağlayıcının erişim tokenı geçersiz.', diff --git a/packages/phrases/src/locales/zh-cn/errors/connector.ts b/packages/phrases/src/locales/zh-cn/errors/connector.ts index e739dd1be..df2096808 100644 --- a/packages/phrases/src/locales/zh-cn/errors/connector.ts +++ b/packages/phrases/src/locales/zh-cn/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: '连接器配置错误', invalid_response: '连接器错误响应', template_not_found: '无法从连接器配置中找到对应的模板', + template_not_supported: '连接器不支持此模板类型。', rate_limit_exceeded: '触发速率限制。请稍后再试。', not_implemented: '方法 {{method}} 尚未实现', social_invalid_access_token: '当前连接器的 access_token 无效', diff --git a/packages/phrases/src/locales/zh-hk/errors/connector.ts b/packages/phrases/src/locales/zh-hk/errors/connector.ts index ccc63b4b1..fcf0568a3 100644 --- a/packages/phrases/src/locales/zh-hk/errors/connector.ts +++ b/packages/phrases/src/locales/zh-hk/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: '連接器配置錯誤', invalid_response: '連接器錯誤響應', template_not_found: '無法從連接器配置中找到對應的模板', + template_not_supported: '連接器不支援此模板類型。', rate_limit_exceeded: '觸發速率限制。請稍後再試。', not_implemented: '方法 {{method}} 尚未實現', social_invalid_access_token: '當前連接器的 access_token 無效', diff --git a/packages/phrases/src/locales/zh-tw/errors/connector.ts b/packages/phrases/src/locales/zh-tw/errors/connector.ts index f524c1e77..d7cda1757 100644 --- a/packages/phrases/src/locales/zh-tw/errors/connector.ts +++ b/packages/phrases/src/locales/zh-tw/errors/connector.ts @@ -10,6 +10,7 @@ const connector = { invalid_config: '連接器配置錯誤', invalid_response: '連接器錯誤響應', template_not_found: '無法從連接器配置中找到對應的模板', + template_not_supported: '連接器不支援此模板類型', rate_limit_exceeded: '觸發速率限制。請稍後再試。', not_implemented: '方法 {{method}} 尚未實現', social_invalid_access_token: '當前連接器的 access_token 無效', diff --git a/packages/toolkit/connector-kit/src/types.ts b/packages/toolkit/connector-kit/src/types.ts index 27329c8b5..615af6cd5 100644 --- a/packages/toolkit/connector-kit/src/types.ts +++ b/packages/toolkit/connector-kit/src/types.ts @@ -48,7 +48,10 @@ export enum ConnectorErrorCodes { InsufficientRequestParameters = 'insufficient_request_parameters', InvalidConfig = 'invalid_config', InvalidResponse = 'invalid_response', + /** The template is not found for the given type. */ TemplateNotFound = 'template_not_found', + /** The template type is not supported by the connector. */ + TemplateNotSupported = 'template_not_supported', RateLimitExceeded = 'rate_limit_exceeded', NotImplemented = 'not_implemented', SocialAuthCodeInvalid = 'social_auth_code_invalid', @@ -62,7 +65,7 @@ export class ConnectorError extends Error { public data: unknown; constructor(code: ConnectorErrorCodes, data?: unknown) { - const message = typeof data === 'string' ? data : 'Connector error occurred.'; + const message = `ConnectorError: ${data ? JSON.stringify(data) : code}`; super(message); this.code = code; this.data = typeof data === 'string' ? { message: data } : data; @@ -233,6 +236,19 @@ export const emailServiceBrandingGuard = z export type EmailServiceBranding = z.infer; +export type SendMessagePayload = { + to: string; + type: VerificationCodeType; + payload: { + /** + * The dynamic verification code to send. + * + * @example '123456' + */ + code: string; + } & EmailServiceBranding; +}; + export const sendMessagePayloadGuard = z.object({ to: z.string(), type: verificationCodeTypeGuard, @@ -241,9 +257,7 @@ export const sendMessagePayloadGuard = z.object({ code: z.string(), }) .merge(emailServiceBrandingGuard), -}); - -export type SendMessagePayload = z.infer; +}) satisfies z.ZodType; export type SendMessageFunction = (data: SendMessagePayload, config?: unknown) => Promise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43ce4c0f9..7763536f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1534,6 +1534,85 @@ importers: specifier: ^5.0.0 version: 5.0.2 + packages/connectors/connector-mailgun: + dependencies: + '@logto/connector-kit': + specifier: workspace:^1.1.0 + version: link:../../toolkit/connector-kit + '@silverhand/essentials': + specifier: ^2.5.0 + version: 2.7.0 + got: + specifier: ^13.0.0 + version: 13.0.0 + snakecase-keys: + specifier: ^5.4.4 + version: 5.4.4 + zod: + specifier: ^3.20.2 + version: 3.20.2 + devDependencies: + '@jest/types': + specifier: ^29.5.0 + version: 29.5.0 + '@rollup/plugin-commonjs': + specifier: ^25.0.0 + version: 25.0.0(rollup@3.8.0) + '@rollup/plugin-json': + specifier: ^6.0.0 + version: 6.0.0(rollup@3.8.0) + '@rollup/plugin-node-resolve': + specifier: ^15.0.1 + version: 15.0.1(rollup@3.8.0) + '@rollup/plugin-typescript': + specifier: ^11.0.0 + version: 11.0.0(rollup@3.8.0)(typescript@5.0.2) + '@silverhand/eslint-config': + specifier: 4.0.1 + version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2) + '@silverhand/ts-config': + specifier: 4.0.0 + version: 4.0.0(typescript@5.0.2) + '@types/jest': + specifier: ^29.4.0 + version: 29.4.0 + '@types/node': + specifier: ^18.11.18 + version: 18.11.18 + '@types/supertest': + specifier: ^2.0.11 + version: 2.0.11 + eslint: + specifier: ^8.44.0 + version: 8.44.0 + jest: + specifier: ^29.5.0 + version: 29.5.0(@types/node@18.11.18) + jest-matcher-specific-error: + specifier: ^1.0.0 + version: 1.0.0 + lint-staged: + specifier: ^13.0.0 + version: 13.0.0 + nock: + specifier: ^13.2.2 + version: 13.3.1 + prettier: + specifier: ^3.0.0 + version: 3.0.0 + rollup: + specifier: ^3.8.0 + version: 3.8.0 + rollup-plugin-summary: + specifier: ^2.0.0 + version: 2.0.0(rollup@3.8.0) + supertest: + specifier: ^6.2.2 + version: 6.2.2 + typescript: + specifier: ^5.0.0 + version: 5.0.2 + packages/connectors/connector-mock-email: dependencies: '@logto/connector-kit': @@ -2906,7 +2985,7 @@ importers: version: 3.0.0 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@20.4.2)(ts-node@10.9.1) + version: 29.5.0(@types/node@18.11.18)(ts-node@10.9.1) jest-environment-jsdom: specifier: ^29.0.0 version: 29.2.2 @@ -2966,7 +3045,7 @@ importers: version: 2.19.3(react@18.2.0) react-dnd: specifier: ^16.0.0 - version: 16.0.0(@types/node@20.4.2)(@types/react@18.0.31)(react@18.2.0) + version: 16.0.0(@types/node@18.11.18)(@types/react@18.0.31)(react@18.2.0) react-dnd-html5-backend: specifier: ^16.0.0 version: 16.0.0 @@ -3933,7 +4012,7 @@ importers: version: 3.0.0 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@20.4.2)(ts-node@10.9.1) + version: 29.5.0(@types/node@18.11.18)(ts-node@10.9.1) jest-environment-jsdom: specifier: ^29.0.0 version: 29.2.2 @@ -6528,15 +6607,15 @@ packages: '@commitlint/execute-rule': 17.4.0 '@commitlint/resolve-extends': 17.4.4 '@commitlint/types': 17.4.4 - '@types/node': 20.4.2 + '@types/node': 18.11.18 chalk: 4.1.2 cosmiconfig: 8.2.0 - cosmiconfig-typescript-loader: 4.3.0(@types/node@20.4.2)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.0.2) + cosmiconfig-typescript-loader: 4.3.0(@types/node@18.11.18)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.0.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@20.4.2)(typescript@5.0.2) + ts-node: 10.9.1(@types/node@18.11.18)(typescript@5.0.2) typescript: 5.0.2 transitivePeerDependencies: - '@swc/core' @@ -6794,6 +6873,48 @@ packages: slash: 3.0.0 dev: true + /@jest/core@29.5.0: + resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.11.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.5.0 + jest-config: 29.5.0(@types/node@18.11.18) + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 + micromatch: 4.0.5 + pretty-format: 29.5.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /@jest/core@29.5.0(ts-node@10.9.1): resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7251,7 +7372,7 @@ packages: /@logto/js@2.1.1: resolution: {integrity: sha512-PHikheavVK+l4ivgtzi14p184hEPgXjqQEAom1Gme1MZoopx+WlwxvHSEQBsmyvVqRtI0oiojhoU5tgYi1FKJw==} dependencies: - '@silverhand/essentials': 2.6.2 + '@silverhand/essentials': 2.7.0 camelcase-keys: 7.0.2 jose: 4.14.2 dev: true @@ -7260,7 +7381,7 @@ packages: resolution: {integrity: sha512-joSzzAqaRKeEquRenoFrIXXkNxkJci5zSkk4afywz1P8tTcTysnV4eXaBmwXNpmDfQdtHBwRdSACZPLgeF8JiQ==} dependencies: '@logto/client': 2.1.0 - '@silverhand/essentials': 2.6.2 + '@silverhand/essentials': 2.7.0 js-base64: 3.7.5 node-fetch: 2.6.7 transitivePeerDependencies: @@ -7273,7 +7394,7 @@ packages: react: '>=16.8.0 || ^18.0.0' dependencies: '@logto/browser': 2.1.0 - '@silverhand/essentials': 2.6.2 + '@silverhand/essentials': 2.7.0 react: 18.2.0 dev: true @@ -8867,11 +8988,6 @@ packages: resolution: {integrity: sha512-8GgVFAmbo6S0EgsjYXH4aH8a69O7SzEtPFPDpVZmJuGEt8e3ODVx0F2V4rXyC3/SzFbcb2md2gRbA+Z6aTad6g==} engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^7} - /@silverhand/essentials@2.6.2: - resolution: {integrity: sha512-1b5u2BGEa14V3o8XzaE7eL+nuwmQe8c1wqSMcGvq+KAusPPZo9tV4glbfF16Xi/ohv37vUpBGJ2DNf4CfuxBLw==} - engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0} - dev: true - /@silverhand/essentials@2.7.0: resolution: {integrity: sha512-F5Qo5ZNnERUURK/9F1ZIi4FBDM22aeD59Zv0VtkgIhUL9tYK9svA2Jz88NNdYBwqCPrh8ExZlpFNi+pNmXKNlQ==} engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0} @@ -9602,10 +9718,6 @@ packages: /@types/node@18.11.18: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} - /@types/node@20.4.2: - resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} - dev: true - /@types/nodemailer@6.4.7: resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==} dependencies: @@ -11144,7 +11256,7 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true - /cosmiconfig-typescript-loader@4.3.0(@types/node@20.4.2)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.0.2): + /cosmiconfig-typescript-loader@4.3.0(@types/node@18.11.18)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.0.2): resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -11153,9 +11265,9 @@ packages: ts-node: '>=10' typescript: '>=3' dependencies: - '@types/node': 20.4.2 + '@types/node': 18.11.18 cosmiconfig: 8.2.0 - ts-node: 10.9.1(@types/node@20.4.2)(typescript@5.0.2) + ts-node: 10.9.1(@types/node@18.11.18)(typescript@5.0.2) typescript: 5.0.2 dev: true @@ -11452,6 +11564,17 @@ packages: /dayjs@1.11.6: resolution: {integrity: sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==} + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + /debug@3.2.7(supports-color@5.5.0): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -12003,9 +12126,9 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7(supports-color@5.5.0) - is-core-module: 2.11.0 - resolve: 1.22.1 + debug: 3.2.7 + is-core-module: 2.12.1 + resolve: 1.22.2 transitivePeerDependencies: - supports-color dev: true @@ -12024,7 +12147,7 @@ packages: eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.61.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.44.0) get-tsconfig: 4.5.0 globby: 13.1.3 - is-core-module: 2.11.0 + is-core-module: 2.12.1 is-glob: 4.0.3 synckit: 0.8.5 transitivePeerDependencies: @@ -12056,7 +12179,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.61.0(eslint@8.44.0)(typescript@5.0.2) - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7 eslint: 8.44.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.61.0)(eslint-plugin-import@2.27.5)(eslint@8.44.0) @@ -12108,18 +12231,18 @@ packages: array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7 doctrine: 2.1.0 eslint: 8.44.0 eslint-import-resolver-node: 0.3.7 eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.61.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.44.0) has: 1.0.3 - is-core-module: 2.11.0 + is-core-module: 2.12.1 is-glob: 4.0.3 minimatch: 3.1.2 object.values: 1.1.6 - resolve: 1.22.1 - semver: 6.3.0 + resolve: 1.22.2 + semver: 6.3.1 tsconfig-paths: 3.14.1 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -12173,8 +12296,8 @@ packages: eslint-utils: 2.1.0 ignore: 5.2.4 minimatch: 3.1.2 - resolve: 1.22.1 - semver: 6.3.0 + resolve: 1.22.2 + semver: 6.3.1 dev: true /eslint-plugin-prettier@5.0.0-alpha.2(eslint-config-prettier@8.8.0)(eslint@8.44.0)(prettier@3.0.0): @@ -13801,12 +13924,6 @@ packages: ci-info: 3.8.0 dev: true - /is-core-module@2.11.0: - resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} - dependencies: - has: 1.0.3 - dev: true - /is-core-module@2.12.1: resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: @@ -14150,6 +14267,34 @@ packages: - supports-color dev: true + /jest-cli@29.5.0(@types/node@18.11.18): + resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.1.0 + jest-config: 29.5.0(@types/node@18.11.18) + jest-util: 29.5.0 + jest-validate: 29.5.0 + prompts: 2.4.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + /jest-cli@29.5.0(@types/node@18.11.18)(ts-node@10.9.1): resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -14178,32 +14323,43 @@ packages: - ts-node dev: true - /jest-cli@29.5.0(@types/node@20.4.2)(ts-node@10.9.1): - resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} + /jest-config@29.5.0(@types/node@18.11.18): + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + '@types/node': '*' + ts-node: '>=9.0.0' peerDependenciesMeta: - node-notifier: + '@types/node': + optional: true + ts-node: optional: true dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1) - '@jest/test-result': 29.5.0 + '@babel/core': 7.20.2 + '@jest/test-sequencer': 29.5.0 '@jest/types': 29.5.0 + '@types/node': 18.11.18 + babel-jest: 29.5.0(@babel/core@7.20.2) chalk: 4.1.2 - exit: 0.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 graceful-fs: 4.2.11 - import-local: 3.1.0 - jest-config: 29.5.0(@types/node@20.4.2)(ts-node@10.9.1) + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 jest-util: 29.5.0 jest-validate: 29.5.0 - prompts: 2.4.2 - yargs: 17.7.2 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.5.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 transitivePeerDependencies: - - '@types/node' - supports-color - - ts-node dev: true /jest-config@29.5.0(@types/node@18.11.18)(ts-node@10.9.1): @@ -14241,47 +14397,7 @@ packages: pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@20.4.2)(typescript@5.0.2) - transitivePeerDependencies: - - supports-color - dev: true - - /jest-config@29.5.0(@types/node@20.4.2)(ts-node@10.9.1): - resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.20.2 - '@jest/test-sequencer': 29.5.0 - '@jest/types': 29.5.0 - '@types/node': 20.4.2 - babel-jest: 29.5.0(@babel/core@7.20.2) - chalk: 4.1.2 - ci-info: 3.8.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.5.0 - jest-environment-node: 29.5.0 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.5.0 - jest-runner: 29.5.0 - jest-util: 29.5.0 - jest-validate: 29.5.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.5.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@20.4.2)(typescript@5.0.2) + ts-node: 10.9.1(@types/node@18.11.18)(typescript@5.0.2) transitivePeerDependencies: - supports-color dev: true @@ -14640,7 +14756,7 @@ packages: jest: ^28.1.0 || ^29.1.2 react: ^17.0.0 || ^18.0.0 dependencies: - jest: 29.5.0(@types/node@20.4.2)(ts-node@10.9.1) + jest: 29.5.0(@types/node@18.11.18)(ts-node@10.9.1) react: 18.2.0 dev: true @@ -14704,6 +14820,26 @@ packages: supports-color: 8.1.1 dev: true + /jest@29.5.0(@types/node@18.11.18): + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.5.0 + '@jest/types': 29.5.0 + import-local: 3.1.0 + jest-cli: 29.5.0(@types/node@18.11.18) + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + /jest@29.5.0(@types/node@18.11.18)(ts-node@10.9.1): resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -14724,26 +14860,6 @@ packages: - ts-node dev: true - /jest@29.5.0(@types/node@20.4.2)(ts-node@10.9.1): - resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1) - '@jest/types': 29.5.0 - import-local: 3.1.0 - jest-cli: 29.5.0(@types/node@20.4.2)(ts-node@10.9.1) - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /joi@17.7.0: resolution: {integrity: sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==} dependencies: @@ -15538,7 +15654,7 @@ packages: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} dependencies: - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 dev: true /make-dir@3.1.0: @@ -17732,7 +17848,7 @@ packages: dnd-core: 16.0.0 dev: true - /react-dnd@16.0.0(@types/node@20.4.2)(@types/react@18.0.31)(react@18.2.0): + /react-dnd@16.0.0(@types/node@18.11.18)(@types/react@18.0.31)(react@18.2.0): resolution: {integrity: sha512-RCoeWRWhuwSoqdLaJV8N/weARLyXqsf43OC3QiBWPORIIGGovF/EqI8ckf14ca3bl6oZNI/igtxX49+IDmNDeQ==} peerDependencies: '@types/hoist-non-react-statics': '>= 3.3.1' @@ -17749,7 +17865,7 @@ packages: dependencies: '@react-dnd/invariant': 4.0.0 '@react-dnd/shallowequal': 4.0.0 - '@types/node': 20.4.2 + '@types/node': 18.11.18 '@types/react': 18.0.31 dnd-core: 16.0.0 fast-deep-equal: 3.1.3 @@ -19819,7 +19935,7 @@ packages: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: true - /ts-node@10.9.1(@types/node@20.4.2)(typescript@5.0.2): + /ts-node@10.9.1(@types/node@18.11.18)(typescript@5.0.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -19838,7 +19954,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.4.2 + '@types/node': 18.11.18 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3