From ca2186189ea64336c446a7ca5cb6ae0298171031 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 11 Jul 2024 14:43:50 +0800 Subject: [PATCH 001/135] refactor: fix experience branding fallback --- .../src/Providers/AppBoundary/AppMeta.tsx | 3 +- .../src/tests/experience/overrides.test.ts | 199 +++++++++++------- .../src/ui-helpers/expect-experience.ts | 12 ++ 3 files changed, 141 insertions(+), 73 deletions(-) diff --git a/packages/experience/src/Providers/AppBoundary/AppMeta.tsx b/packages/experience/src/Providers/AppBoundary/AppMeta.tsx index 20da833774e7..10aca13e0c0a 100644 --- a/packages/experience/src/Providers/AppBoundary/AppMeta.tsx +++ b/packages/experience/src/Providers/AppBoundary/AppMeta.tsx @@ -33,7 +33,8 @@ const themeToFavicon = Object.freeze({ const AppMeta = () => { const { experienceSettings, theme, platform, isPreview } = useContext(PageContext); - const favicon = experienceSettings?.branding[themeToFavicon[theme]]; + const favicon = + experienceSettings?.branding[themeToFavicon[theme]] ?? experienceSettings?.branding.favicon; return ( diff --git a/packages/integration-tests/src/tests/experience/overrides.test.ts b/packages/integration-tests/src/tests/experience/overrides.test.ts index 16d62c69ae4e..67d4790b465f 100644 --- a/packages/integration-tests/src/tests/experience/overrides.test.ts +++ b/packages/integration-tests/src/tests/experience/overrides.test.ts @@ -3,7 +3,8 @@ */ import { ConnectorType } from '@logto/connector-kit'; -import { ApplicationType, SignInIdentifier } from '@logto/schemas'; +import { ApplicationType, type Branding, type Color, SignInIdentifier } from '@logto/schemas'; +import { pick } from '@silverhand/essentials'; import { setApplicationSignInExperience } from '#src/api/application-sign-in-experience.js'; import { createApplication, deleteApplication } from '#src/api/application.js'; @@ -13,14 +14,37 @@ import { clearConnectorsByTypes } from '#src/helpers/connector.js'; import { OrganizationApiTest } from '#src/helpers/organization.js'; import ExpectExperience from '#src/ui-helpers/expect-experience.js'; -describe('override', () => { +describe('overrides', () => { const organizationApi = new OrganizationApiTest(); - const logoUrl = 'mock://fake-url-for-omni/logo.png'; - const darkLogoUrl = 'mock://fake-url-for-omni/dark-logo.png'; - const primaryColor = '#000'; - const darkPrimaryColor = '#fff'; - const favicon = 'mock://fake-url-for-omni/favicon.ico'; - const darkFavicon = 'mock://fake-url-for-omni/dark-favicon.ico'; + + const omniColor = Object.freeze({ + primaryColor: '#f00', + darkPrimaryColor: '#0f0', + isDarkModeEnabled: true, + } satisfies Color); + const omniBranding = Object.freeze({ + logoUrl: 'mock://fake-url-for-omni/logo.png', + darkLogoUrl: 'mock://fake-url-for-omni/dark-logo.png', + favicon: 'mock://fake-url-for-omni/favicon.ico', + darkFavicon: 'mock://fake-url-for-omni/dark-favicon.ico', + } satisfies Branding); + + const appColor = Object.freeze({ + primaryColor: '#00f', + darkPrimaryColor: '#f0f', + isDarkModeEnabled: true, + } satisfies Color); + const appBranding = Object.freeze({ + logoUrl: 'mock://fake-url-for-app/logo.png', + darkLogoUrl: 'mock://fake-url-for-app/dark-logo.png', + favicon: 'mock://fake-url-for-app/favicon.ico', + darkFavicon: 'mock://fake-url-for-app/dark-favicon.ico', + } satisfies Branding); + + const organizationBranding = Object.freeze({ + logoUrl: 'mock://fake-url-for-org/logo.png', + darkLogoUrl: 'mock://fake-url-for-org/dark-logo.png', + } satisfies Branding); afterEach(async () => { await organizationApi.cleanUp(); @@ -31,8 +55,8 @@ describe('override', () => { await updateSignInExperience({ termsOfUseUrl: null, privacyPolicyUrl: null, - color: { primaryColor, darkPrimaryColor, isDarkModeEnabled: true }, - branding: { logoUrl, darkLogoUrl, favicon, darkFavicon }, + color: omniColor, + branding: omniBranding, signUp: { identifiers: [], password: true, verify: false }, signIn: { methods: [ @@ -52,57 +76,39 @@ describe('override', () => { await experience.page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]); await experience.navigateTo(demoAppUrl.href); await experience.toMatchElement('body[class$="dark"]'); - await experience.toMatchElement(`img[src="${darkLogoUrl}"]`); + await experience.toMatchElement(`img[src="${omniBranding.darkLogoUrl}"]`); const button = await experience.toMatchElement('button[name="submit"]'); expect( await button.evaluate((element) => window.getComputedStyle(element).backgroundColor) - ).toBe('rgb(255, 255, 255)'); + ).toBe('rgb(0, 255, 0)'); - const foundFavicon = await experience.page.evaluate(() => { - return document.querySelector('link[rel="shortcut icon"]')?.getAttribute('href'); - }); - expect(foundFavicon).toBe(darkFavicon); + const { favicon: faviconElement, appleFavicon } = await experience.findFaviconUrls(); + expect(faviconElement).toBe(omniBranding.darkFavicon); + expect(appleFavicon).toBe(omniBranding.darkFavicon); - const faviconAppleTouch = await experience.page.evaluate(() => { - return document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href'); - }); - expect(faviconAppleTouch).toBe(darkFavicon); await experience.page.close(); }); - it('should show the overridden organization logos and favicons', async () => { - const logoUrl = 'mock://fake-url-for-organization/logo.png'; - const darkLogoUrl = 'mock://fake-url-for-organization/dark-logo.png'; - + it('should show the overridden organization logos', async () => { const organization = await organizationApi.create({ name: 'Sign-in experience override', - branding: { - logoUrl, - darkLogoUrl, - }, + branding: organizationBranding, }); const experience = new ExpectExperience(await browser.newPage()); await experience.page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'light' }]); await experience.navigateTo(demoAppUrl.href + `?organization_id=${organization.id}`); - await experience.toMatchElement(`img[src="${logoUrl}"]`); + await experience.toMatchElement(`img[src="${organizationBranding.logoUrl}"]`); await experience.page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]); await experience.navigateTo(demoAppUrl.href + `?organization_id=${organization.id}`); - await experience.toMatchElement(`img[src="${darkLogoUrl}"]`); + await experience.toMatchElement(`img[src="${organizationBranding.darkLogoUrl}"]`); await experience.page.close(); }); it('should show app-level logo, favicon, and color', async () => { - const logoUrl = 'mock://fake-url-for-app/logo.png'; - const darkLogoUrl = 'mock://fake-url-for-app/dark-logo.png'; - const primaryColor = '#f00'; - const darkPrimaryColor = '#0f0'; - const favicon = 'mock://fake-url-for-organization/favicon.ico'; - const darkFavicon = 'mock://fake-url-for-organization/dark-favicon.ico'; - const application = await createApplication( 'Sign-in experience override', ApplicationType.SPA, @@ -115,8 +121,8 @@ describe('override', () => { ); await setApplicationSignInExperience(application.id, { - color: { primaryColor, darkPrimaryColor }, - branding: { logoUrl, darkLogoUrl, favicon, darkFavicon }, + color: appColor, + branding: appBranding, }); const experience = new ExpectExperience(await browser.newPage()); @@ -134,39 +140,27 @@ describe('override', () => { await button.evaluate((element) => window.getComputedStyle(element).backgroundColor) ).toBe(primaryColor); - const foundFavicon = await experience.page.evaluate(() => { - return document.querySelector('link[rel="shortcut icon"]')?.getAttribute('href'); - }); - expect(foundFavicon).toBe(favicon); - - const faviconAppleTouch = await experience.page.evaluate(() => { - return document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href'); - }); - expect(faviconAppleTouch).toBe(favicon); + const { favicon: faviconElement, appleFavicon } = await experience.findFaviconUrls(); + expect(faviconElement).toBe(favicon); + expect(appleFavicon).toBe(favicon); }; - await expectMatchBranding('light', logoUrl, 'rgb(255, 0, 0)', favicon); - await expectMatchBranding('dark', darkLogoUrl, 'rgb(0, 255, 0)', darkFavicon); + await expectMatchBranding('light', appBranding.logoUrl, 'rgb(0, 0, 255)', appBranding.favicon); + await expectMatchBranding( + 'dark', + appBranding.darkLogoUrl, + 'rgb(255, 0, 255)', + appBranding.darkFavicon + ); await deleteApplication(application.id); await experience.page.close(); }); it('should combine app-level and organization-level branding', async () => { - const organizationLogoUrl = 'mock://fake-url-for-organization/logo.png'; - const organizationDarkLogoUrl = 'mock://fake-url-for-organization/dark-logo.png'; - - const appLogoUrl = 'mock://fake-url-for-app/logo.png'; - const appDarkLogoUrl = 'mock://fake-url-for-app/dark-logo.png'; - const appPrimaryColor = '#00f'; - const appDarkPrimaryColor = '#f0f'; - const organization = await organizationApi.create({ name: 'Sign-in experience override', - branding: { - logoUrl: organizationLogoUrl, - darkLogoUrl: organizationDarkLogoUrl, - }, + branding: organizationBranding, }); const application = await createApplication( @@ -181,14 +175,8 @@ describe('override', () => { ); await setApplicationSignInExperience(application.id, { - color: { - primaryColor: appPrimaryColor, - darkPrimaryColor: appDarkPrimaryColor, - }, - branding: { - logoUrl: appLogoUrl, - darkLogoUrl: appDarkLogoUrl, - }, + color: appColor, + branding: appBranding, }); const experience = new ExpectExperience(await browser.newPage()); @@ -204,8 +192,75 @@ describe('override', () => { ).toBe(primaryColor); }; - await expectMatchBranding('light', organizationLogoUrl, 'rgb(0, 0, 255)'); - await expectMatchBranding('dark', organizationDarkLogoUrl, 'rgb(255, 0, 255)'); + await expectMatchBranding('light', organizationBranding.logoUrl, 'rgb(0, 0, 255)'); + await expectMatchBranding('dark', organizationBranding.darkLogoUrl, 'rgb(255, 0, 255)'); + await experience.page.close(); + }); + + describe('override fallback', () => { + beforeAll(async () => { + await updateSignInExperience({ + color: omniColor, + branding: pick(omniBranding, 'logoUrl', 'favicon'), + }); + }); + + it('should fall back to light mode branding elements when dark mode is enabled but no dark mode branding elements are provided (omni)', async () => { + const experience = new ExpectExperience(await browser.newPage()); + await experience.page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]); + await experience.navigateTo(demoAppUrl.href); + await experience.toMatchElement('body[class$="dark"]'); + await experience.toMatchElement(`img[src="${omniBranding.logoUrl}"]`); + + const { favicon: faviconElement, appleFavicon } = await experience.findFaviconUrls(); + expect(faviconElement).toBe(omniBranding.favicon); + expect(appleFavicon).toBe(omniBranding.favicon); + await experience.page.close(); + }); + }); + + it('should fall back to light mode branding elements when dark mode is enabled but no dark mode branding elements are provided (organization)', async () => { + const organization = await organizationApi.create({ + name: 'Sign-in experience override', + branding: pick(organizationBranding, 'logoUrl'), + }); + + const experience = new ExpectExperience(await browser.newPage()); + await experience.page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]); + await experience.navigateTo(demoAppUrl.href + `?organization_id=${organization.id}`); + await experience.toMatchElement('body[class$="dark"]'); + await experience.toMatchElement(`img[src="${organizationBranding.logoUrl}"]`); + + await experience.page.close(); + }); + + it('should fall back to light mode branding elements when dark mode is enabled but no dark mode branding elements are provided (app)', async () => { + const application = await createApplication( + 'Sign-in experience override', + ApplicationType.SPA, + { + oidcClientMetadata: { + redirectUris: [demoAppRedirectUri], + postLogoutRedirectUris: [demoAppRedirectUri], + }, + } + ); + + await setApplicationSignInExperience(application.id, { + color: appColor, + branding: pick(appBranding, 'logoUrl', 'favicon'), + }); + + const experience = new ExpectExperience(await browser.newPage()); + await experience.page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]); + await experience.navigateTo(demoAppUrl.href + `?app_id=${application.id}`); + await experience.toMatchElement('body[class$="dark"]'); + await experience.toMatchElement(`img[src="${appBranding.logoUrl}"]`); + + const { favicon: faviconElement, appleFavicon } = await experience.findFaviconUrls(); + expect(faviconElement).toBe(appBranding.favicon); + expect(appleFavicon).toBe(appBranding.favicon); + await experience.page.close(); }); }); diff --git a/packages/integration-tests/src/ui-helpers/expect-experience.ts b/packages/integration-tests/src/ui-helpers/expect-experience.ts index c6588b4bda85..14c08fdd3cea 100644 --- a/packages/integration-tests/src/ui-helpers/expect-experience.ts +++ b/packages/integration-tests/src/ui-helpers/expect-experience.ts @@ -300,6 +300,18 @@ export default class ExpectExperience extends ExpectPage { return (await userIdSpan.evaluate((element) => element.textContent)) ?? ''; } + async findFaviconUrls() { + const [favicon, appleFavicon] = await Promise.all([ + this.page.evaluate(() => { + return document.querySelector('link[rel="shortcut icon"]')?.getAttribute('href'); + }), + this.page.evaluate(() => { + return document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href'); + }), + ]); + return { favicon, appleFavicon }; + } + /** Build a full experience URL from a pathname. */ protected buildExperienceUrl(pathname = '') { return appendPath(this.options.endpoint, pathname); From cee1c54dc04428f8bf9101e8123a22dfbc6c5bd6 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 11 Jul 2024 17:06:21 +0800 Subject: [PATCH 002/135] refactor(console): improve branding experience --- .../ConnectorForm/BasicForm/index.tsx | 6 +-- .../components/ImageInputs/LogoAndFavicon.tsx | 2 +- .../components/ImageInputs/index.module.scss | 15 +----- .../src/ds-components/Tip/TipBubble/utils.ts | 4 +- .../Uploader/FileUploader/index.module.scss | 3 +- .../Branding/index.module.scss | 8 +++ .../Branding/index.tsx | 50 ++++++++++++++++--- .../ApplicationDetailsContent/index.tsx | 14 +++--- .../Settings/JitSettings.tsx | 7 +-- .../Settings/index.module.scss | 6 +-- .../OrganizationDetails/Settings/index.tsx | 4 +- .../PlanComparisonTable/index.tsx | 2 +- .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/sign-in-exp/index.ts | 7 +++ .../admin-console/subscription/quota-table.ts | 2 +- .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../admin-console/subscription/quota-table.ts | 1 - .../schemas/src/seeds/sign-in-experience.ts | 2 +- 29 files changed, 86 insertions(+), 60 deletions(-) diff --git a/packages/console/src/components/ConnectorForm/BasicForm/index.tsx b/packages/console/src/components/ConnectorForm/BasicForm/index.tsx index 6fe7331827fb..fb872a83e6de 100644 --- a/packages/console/src/components/ConnectorForm/BasicForm/index.tsx +++ b/packages/console/src/components/ConnectorForm/BasicForm/index.tsx @@ -3,7 +3,7 @@ import { Controller, useFormContext } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; import Error from '@/assets/icons/toast-error.svg'; -import LogoInputs from '@/components/ImageInputs'; +import ImageInputs from '@/components/ImageInputs'; import UnnamedTrans from '@/components/UnnamedTrans'; import FormField from '@/ds-components/FormField'; import Select from '@/ds-components/Select'; @@ -57,7 +57,7 @@ function BasicForm({ isAllowEditTarget, isStandard, conflictConnectorName }: Pro {...register('name', { required: true })} /> - ({ name: themeToField[theme], error: errors[themeToField[theme]], - type: 'app_logo', + type: 'connector_logo', theme, }))} /> diff --git a/packages/console/src/components/ImageInputs/LogoAndFavicon.tsx b/packages/console/src/components/ImageInputs/LogoAndFavicon.tsx index fc070aec1524..b60af048eb68 100644 --- a/packages/console/src/components/ImageInputs/LogoAndFavicon.tsx +++ b/packages/console/src/components/ImageInputs/LogoAndFavicon.tsx @@ -45,7 +45,7 @@ function LogoAndFavicon({ uploadTitle={ <> {t(`sign_in_exp.branding.with_${theme}`, { - value: t('sign_in_exp.branding.app_logo_and_favicon'), + value: t(`sign_in_exp.branding.${type}_and_favicon`), })} } diff --git a/packages/console/src/components/ImageInputs/index.module.scss b/packages/console/src/components/ImageInputs/index.module.scss index b931a3b5c692..4e24be27ac43 100644 --- a/packages/console/src/components/ImageInputs/index.module.scss +++ b/packages/console/src/components/ImageInputs/index.module.scss @@ -2,24 +2,11 @@ .container { display: flex; + gap: _.unit(2); > * { flex: 1; - &:first-child { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &:last-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - - &:not(:first-child):not(:last-child) { - border-radius: 0; - } - &.dark { background-color: #111; } diff --git a/packages/console/src/ds-components/Tip/TipBubble/utils.ts b/packages/console/src/ds-components/Tip/TipBubble/utils.ts index 1dace20b11df..20071e5399d1 100644 --- a/packages/console/src/ds-components/Tip/TipBubble/utils.ts +++ b/packages/console/src/ds-components/Tip/TipBubble/utils.ts @@ -5,11 +5,11 @@ import type { TipBubblePlacement } from '.'; export const getVerticalOffset = (placement: TipBubblePlacement) => { switch (placement) { case 'top': { - return -16; + return -8; } case 'bottom': { - return 16; + return 8; } default: { diff --git a/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss b/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss index 0e15e5bbed67..77569e8821c2 100644 --- a/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss +++ b/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss @@ -28,7 +28,7 @@ .actionDescription { margin-top: _.unit(1); - font: var(--font-body-2); // With font height to be 20px. + font: var(--font-body-3); user-select: none; } } @@ -60,4 +60,3 @@ border-color: var(--color-error); } } - diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.module.scss b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.module.scss index f99c87282003..57e7fffb7141 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.module.scss +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.module.scss @@ -3,3 +3,11 @@ .colors { margin-top: _.unit(6); } + +.darkModeTip { + display: flex; + align-items: baseline; + font: var(--font-body-2); + color: var(--color-text-secondary); + margin-top: _.unit(1); +} diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.tsx index d048756849fe..7ff5d422e9d6 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Branding/index.tsx @@ -1,16 +1,23 @@ -import { Theme, type Application, type ApplicationSignInExperience } from '@logto/schemas'; -import { useCallback, useEffect } from 'react'; +import { generateDarkColor } from '@logto/core-kit'; +import { + Theme, + defaultPrimaryColor, + type Application, + type ApplicationSignInExperience, +} from '@logto/schemas'; +import { useCallback, useEffect, useMemo } from 'react'; import { useForm, FormProvider, Controller } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import DetailsForm from '@/components/DetailsForm'; import FormCard, { FormCardSkeleton } from '@/components/FormCard'; -import LogoInputs, { themeToLogoName } from '@/components/ImageInputs'; +import ImageInputs, { themeToLogoName } from '@/components/ImageInputs'; import LogoAndFavicon from '@/components/ImageInputs/LogoAndFavicon'; import RequestDataError from '@/components/RequestDataError'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import { appSpecificBrandingLink, logtoThirdPartyAppBrandingLink } from '@/consts'; +import Button from '@/ds-components/Button'; import ColorPicker from '@/ds-components/ColorPicker'; import FormField from '@/ds-components/FormField'; import Switch from '@/ds-components/Switch'; @@ -105,10 +112,22 @@ function Branding({ application, isActive }: Props) { // is valid; otherwise, directly save the form will be a no-op. useEffect(() => { if (isBrandingEnabled && Object.values(color).filter(Boolean).length === 0) { - setValue('color', { primaryColor: '#000000', darkPrimaryColor: '#000000' }); + setValue('color', { + primaryColor: defaultPrimaryColor, + darkPrimaryColor: generateDarkColor(defaultPrimaryColor), + }); } }, [color, isBrandingEnabled, setValue]); + const [primaryColor, darkPrimaryColor] = watch(['color.primaryColor', 'color.darkPrimaryColor']); + const calculatedDarkPrimaryColor = useMemo(() => { + return primaryColor && generateDarkColor(primaryColor); + }, [primaryColor]); + + const handleResetColor = useCallback(() => { + setValue('color.darkPrimaryColor', calculatedDarkPrimaryColor); + }, [calculatedDarkPrimaryColor, setValue]); + const NonThirdPartyBrandingForm = useCallback( () => ( <> @@ -153,10 +172,29 @@ function Branding({ application, isActive }: Props) { )} /> + {calculatedDarkPrimaryColor !== darkPrimaryColor && ( +
+ {t('sign_in_exp.color.dark_mode_reset_tip')} +
+ )} ), - [control, errors.branding, register] + [ + control, + errors.branding, + register, + calculatedDarkPrimaryColor, + darkPrimaryColor, + handleResetColor, + t, + ] ); if (isLoading) { @@ -193,7 +231,7 @@ function Branding({ application, isActive }: Props) { - { @@ -189,9 +195,7 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P {t('application_details.permissions.name')} )} - {[ApplicationType.Native, ApplicationType.SPA, ApplicationType.Traditional].includes( - data.type - ) && ( + {hasBranding && ( {t('application_details.branding.name')} @@ -266,9 +270,7 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P )} - {[ApplicationType.Native, ApplicationType.SPA, ApplicationType.Traditional].includes( - data.type - ) && ( + {hasBranding && ( {!allSsoConnectors?.length && ( - + }} @@ -100,7 +100,7 @@ function JitSettings({ form }: Props) { render={({ field: { onChange, value } }) => ( <> {value.length > 0 && ( -
+
{value.map((id) => { const connector = allSsoConnectors?.find( ({ id: connectorId }) => id === connectorId @@ -130,6 +130,7 @@ function JitSettings({ form }: Props) { {Boolean(filteredSsoConnectors?.length) && ( - Date: Fri, 12 Jul 2024 14:03:21 +0800 Subject: [PATCH 003/135] feat(core): handle dpop and client certificate for token exchange (#6199) --- .../src/oidc/grants/client-credentials.ts | 27 ++----- .../core/src/oidc/grants/refresh-token.ts | 57 ++----------- .../src/oidc/grants/token-exchange/index.ts | 5 +- packages/core/src/oidc/grants/utils.ts | 81 +++++++++++++++++++ 4 files changed, 95 insertions(+), 75 deletions(-) create mode 100644 packages/core/src/oidc/grants/utils.ts diff --git a/packages/core/src/oidc/grants/client-credentials.ts b/packages/core/src/oidc/grants/client-credentials.ts index 5bf06f889acb..8aafdeec2edb 100644 --- a/packages/core/src/oidc/grants/client-credentials.ts +++ b/packages/core/src/oidc/grants/client-credentials.ts @@ -23,8 +23,6 @@ import { buildOrganizationUrn } from '@logto/core-kit'; import { cond } from '@silverhand/essentials'; import type Provider from 'oidc-provider'; import { errors } from 'oidc-provider'; -import epochTime from 'oidc-provider/lib/helpers/epoch_time.js'; -import dpopValidate from 'oidc-provider/lib/helpers/validate_dpop.js'; import instance from 'oidc-provider/lib/helpers/weak_cache.js'; import checkResource from 'oidc-provider/lib/shared/check_resource.js'; @@ -34,6 +32,8 @@ import assertThat from '#src/utils/assert-that.js'; import { getSharedResourceServerData, reversedResourceAccessTokenTtl } from '../resource.js'; +import { handleClientCertificate, handleDPoP } from './utils.js'; + const { AccessDenied, InvalidClient, InvalidGrant, InvalidScope, InvalidTarget } = errors; /** @@ -51,7 +51,7 @@ export const buildHandler: ( // eslint-disable-next-line complexity ) => Parameters[1] = (envSet, queries) => async (ctx, next) => { const { client, params } = ctx.oidc; - const { ClientCredentials, ReplayDetection } = ctx.oidc.provider; + const { ClientCredentials } = ctx.oidc.provider; assertThat(client, new InvalidClient('client must be available')); @@ -62,8 +62,6 @@ export const buildHandler: ( scopes: statics, } = instance(ctx.oidc.provider).configuration(); - const dPoP = await dpopValidate(ctx); - /* === RFC 0006 === */ // The value type is `unknown`, which will swallow other type inferences. So we have to cast it // to `Boolean` first. @@ -166,23 +164,8 @@ export const buildHandler: ( token.setThumbprint('x5t', cert); } - if (dPoP) { - // @ts-expect-error -- code from oidc-provider - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const unique: unknown = await ReplayDetection.unique( - client.clientId, - dPoP.jti, - epochTime() + 300 - ); - - assertThat(unique, new InvalidGrant('DPoP proof JWT Replay detected')); - - // @ts-expect-error -- code from oidc-provider - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - token.setThumbprint('jkt', dPoP.thumbprint); - } else if (ctx.oidc.client?.dpopBoundAccessTokens) { - throw new InvalidGrant('DPoP proof JWT not provided'); - } + await handleDPoP(ctx, token); + await handleClientCertificate(ctx, token); ctx.oidc.entity('ClientCredentials', token); const value = await token.save(); diff --git a/packages/core/src/oidc/grants/refresh-token.ts b/packages/core/src/oidc/grants/refresh-token.ts index 87677f4f0345..f3fa05fff5b5 100644 --- a/packages/core/src/oidc/grants/refresh-token.ts +++ b/packages/core/src/oidc/grants/refresh-token.ts @@ -19,19 +19,14 @@ * The commit hash of the original file is `cf2069cbb31a6a855876e95157372d25dde2511c`. */ -import { type X509Certificate } from 'node:crypto'; - import { UserScope, buildOrganizationUrn } from '@logto/core-kit'; -import { type Optional, isKeyInObject, cond } from '@silverhand/essentials'; +import { isKeyInObject, cond } from '@silverhand/essentials'; import type Provider from 'oidc-provider'; import { errors } from 'oidc-provider'; import difference from 'oidc-provider/lib/helpers/_/difference.js'; -import certificateThumbprint from 'oidc-provider/lib/helpers/certificate_thumbprint.js'; -import epochTime from 'oidc-provider/lib/helpers/epoch_time.js'; import filterClaims from 'oidc-provider/lib/helpers/filter_claims.js'; import resolveResource from 'oidc-provider/lib/helpers/resolve_resource.js'; import revoke from 'oidc-provider/lib/helpers/revoke.js'; -import dpopValidate from 'oidc-provider/lib/helpers/validate_dpop.js'; import validatePresence from 'oidc-provider/lib/helpers/validate_presence.js'; import instance from 'oidc-provider/lib/helpers/weak_cache.js'; @@ -46,6 +41,8 @@ import { isOrganizationConsentedToApplication, } from '../resource.js'; +import { handleClientCertificate, handleDPoP } from './utils.js'; + const { InvalidClient, InvalidGrant, InvalidScope, InsufficientScope, AccessDenied } = errors; /** The grant type name. `gty` follows the name in oidc-provider. */ @@ -93,8 +90,6 @@ export const buildHandler: ( }, } = providerInstance.configuration(); - const dPoP = await dpopValidate(ctx); - // @gao: I believe the presence of the param is validated by required parameters of this grant. // Add `String` to make TS happy. let refreshTokenValue = String(params.refresh_token); @@ -112,23 +107,6 @@ export const buildHandler: ( throw new InvalidGrant('refresh token is expired'); } - let cert: Optional; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- the original code uses `||` - if (client.tlsClientCertificateBoundAccessTokens || refreshToken['x5t#S256']) { - cert = getCertificate(ctx); - if (!cert) { - throw new InvalidGrant('mutual TLS client certificate not provided'); - } - } - - if (!dPoP && client.dpopBoundAccessTokens) { - throw new InvalidGrant('DPoP proof JWT not provided'); - } - - if (refreshToken['x5t#S256'] && refreshToken['x5t#S256'] !== certificateThumbprint(cert!)) { - throw new InvalidGrant('failed x5t#S256 verification'); - } - /* === RFC 0001 === */ // The value type is `unknown`, which will swallow other type inferences. So we have to cast it // to `Boolean` first. @@ -177,22 +155,6 @@ export const buildHandler: ( } } - if (dPoP) { - // @ts-expect-error -- code from oidc-provider - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const unique: unknown = await ReplayDetection.unique( - client.clientId, - dPoP.jti, - epochTime() + 300 - ); - - assertThat(unique, new errors.InvalidGrant('DPoP proof JWT Replay detected')); - } - - if (refreshToken.jkt && (!dPoP || refreshToken.jkt !== dPoP.thumbprint)) { - throw new InvalidGrant('failed jkt verification'); - } - ctx.oidc.entity('RefreshToken', refreshToken); ctx.oidc.entity('Grant', grant); @@ -304,17 +266,8 @@ export const buildHandler: ( scope: undefined!, }); - if (client.tlsClientCertificateBoundAccessTokens) { - // @ts-expect-error -- code from oidc-provider - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - at.setThumbprint('x5t', cert); - } - - if (dPoP) { - // @ts-expect-error -- code from oidc-provider - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - at.setThumbprint('jkt', dPoP.thumbprint); - } + await handleDPoP(ctx, at, refreshToken); + await handleClientCertificate(ctx, at, refreshToken); if (at.gty && !at.gty.endsWith(gty)) { at.gty = `${at.gty} ${gty}`; diff --git a/packages/core/src/oidc/grants/token-exchange/index.ts b/packages/core/src/oidc/grants/token-exchange/index.ts index f672bad78e6e..b4758e7537eb 100644 --- a/packages/core/src/oidc/grants/token-exchange/index.ts +++ b/packages/core/src/oidc/grants/token-exchange/index.ts @@ -22,6 +22,7 @@ import { getSharedResourceServerData, reversedResourceAccessTokenTtl, } from '../../resource.js'; +import { handleClientCertificate, handleDPoP } from '../utils.js'; import { handleActorToken } from './actor-token.js'; import { TokenExchangeTokenType, type TokenExchangeAct } from './types.js'; @@ -93,7 +94,6 @@ export const buildHandler: ( throw new InvalidGrant('refresh token invalid (referenced account not found)'); } - // TODO: (LOG-9501) Implement general security checks like dPop ctx.oidc.entity('Account', account); /* === RFC 0001 === */ @@ -137,6 +137,9 @@ export const buildHandler: ( scope: undefined!, }); + await handleDPoP(ctx, accessToken); + await handleClientCertificate(ctx, accessToken); + /** The scopes requested by the client. If not provided, use the scopes from the refresh token. */ const scope = requestParamScopes; const resource = await resolveResource( diff --git a/packages/core/src/oidc/grants/utils.ts b/packages/core/src/oidc/grants/utils.ts new file mode 100644 index 000000000000..7720c67ef934 --- /dev/null +++ b/packages/core/src/oidc/grants/utils.ts @@ -0,0 +1,81 @@ +import type Provider from 'oidc-provider'; +import { errors, type KoaContextWithOIDC } from 'oidc-provider'; +import certificateThumbprint from 'oidc-provider/lib/helpers/certificate_thumbprint.js'; +import epochTime from 'oidc-provider/lib/helpers/epoch_time.js'; +import dpopValidate from 'oidc-provider/lib/helpers/validate_dpop.js'; +import instance from 'oidc-provider/lib/helpers/weak_cache.js'; + +import assertThat from '#src/utils/assert-that.js'; + +const { InvalidGrant, InvalidClient } = errors; + +/** + * Handle DPoP bound access tokens. + */ +export const handleDPoP = async ( + ctx: KoaContextWithOIDC, + token: InstanceType | InstanceType, + originalToken?: InstanceType +) => { + const { client } = ctx.oidc; + assertThat(client, new InvalidClient('client must be available')); + + const dPoP = await dpopValidate(ctx); + + if (dPoP) { + // @ts-expect-error -- code from oidc-provider + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const unique: unknown = await ReplayDetection.unique( + client.clientId, + dPoP.jti, + epochTime() + 300 + ); + + assertThat(unique, new InvalidGrant('DPoP proof JWT Replay detected')); + + // @ts-expect-error -- code from oidc-provider + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + token.setThumbprint('jkt', dPoP.thumbprint); + } else if (client.dpopBoundAccessTokens) { + throw new InvalidGrant('DPoP proof JWT not provided'); + } + + if (originalToken?.jkt && (!dPoP || originalToken.jkt !== dPoP.thumbprint)) { + throw new InvalidGrant('failed jkt verification'); + } +}; + +/** + * Handle client certificate bound access tokens. + */ +export const handleClientCertificate = async ( + ctx: KoaContextWithOIDC, + token: InstanceType | InstanceType, + originalToken?: InstanceType +) => { + const { client, provider } = ctx.oidc; + assertThat(client, new InvalidClient('client must be available')); + + const providerInstance = instance(provider); + const { + features: { + mTLS: { getCertificate }, + }, + } = providerInstance.configuration(); + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (client.tlsClientCertificateBoundAccessTokens || originalToken?.['x5t#S256']) { + const cert = getCertificate(ctx); + + if (!cert) { + throw new InvalidGrant('mutual TLS client certificate not provided'); + } + // @ts-expect-error -- code from oidc-provider + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + token.setThumbprint('x5t', cert); + + if (originalToken?.['x5t#S256'] && originalToken['x5t#S256'] !== certificateThumbprint(cert)) { + throw new InvalidGrant('failed x5t#S256 verification'); + } + } +}; From ba875b417c20fa22214a060dc7600a9e2d11e52a Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 12 Jul 2024 14:04:43 +0800 Subject: [PATCH 004/135] refactor: fix third-party app experience branding (#6223) --- .../src/components/ImageInputs/index.tsx | 5 +- .../src/libraries/sign-in-experience/index.ts | 12 ++++- .../queries/application-sign-in-experience.ts | 26 +++++++++-- .../src/tests/experience/overrides.test.ts | 46 +++++++++++++++++-- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/packages/console/src/components/ImageInputs/index.tsx b/packages/console/src/components/ImageInputs/index.tsx index bce230e7c2bf..a9f8d6f5e908 100644 --- a/packages/console/src/components/ImageInputs/index.tsx +++ b/packages/console/src/components/ImageInputs/index.tsx @@ -45,7 +45,10 @@ export type ImageField = { type Props = { /** The condensed title when user assets service is available. */ readonly uploadTitle: React.ComponentProps['title']; - /** The tooltip to show for all the fields. */ + /** + * When user assets service is available, the tip will be displayed for the `uploadTitle`; + * otherwise, it will be displayed for each text input. + */ readonly tip?: React.ComponentProps['tip']; readonly control: Control; readonly register: UseFormRegister; diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 5c7672f3b8fb..a98f80c63053 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -147,7 +147,7 @@ export const createSignInExperienceLibrary = ( return; } - return pick(found, 'branding', 'color'); + return pick(found, 'branding', 'color', 'type', 'isThirdParty'); }; const getFullSignInExperience = async ({ @@ -223,9 +223,17 @@ export const createSignInExperienceLibrary = ( }; }; + /** Get the branding and color from the app sign-in experience if it is not a third-party app. */ + const getAppSignInExperience = () => { + if (!appSignInExperience || appSignInExperience.isThirdParty) { + return {}; + } + return pick(appSignInExperience, 'branding', 'color'); + }; + return { ...deepmerge( - deepmerge(signInExperience, appSignInExperience ?? {}), + deepmerge(signInExperience, getAppSignInExperience()), organizationOverride ?? {} ), socialConnectors, diff --git a/packages/core/src/queries/application-sign-in-experience.ts b/packages/core/src/queries/application-sign-in-experience.ts index 3a9e0f157aee..84a891085c9f 100644 --- a/packages/core/src/queries/application-sign-in-experience.ts +++ b/packages/core/src/queries/application-sign-in-experience.ts @@ -1,4 +1,10 @@ -import { ApplicationSignInExperiences, type ApplicationSignInExperience } from '@logto/schemas'; +import { + type Application, + ApplicationSignInExperiences, + Applications, + type ApplicationSignInExperience, +} from '@logto/schemas'; +import { type Nullable } from '@silverhand/essentials'; import { sql, type CommonQueryMethods } from '@silverhand/slonik'; import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; @@ -10,12 +16,22 @@ const createApplicationSignInExperienceQueries = (pool: CommonQueryMethods) => { returning: true, }); - const safeFindSignInExperienceByApplicationId = async (applicationId: string) => { - const { table, fields } = convertToIdentifiers(ApplicationSignInExperiences); + type ApplicationSignInExperienceReturn = ApplicationSignInExperience & + Pick; - return pool.maybeOne(sql` - select ${sql.join(Object.values(fields), sql`, `)} + const safeFindSignInExperienceByApplicationId = async ( + applicationId: string + ): Promise> => { + const applications = convertToIdentifiers(Applications, true); + const { table, fields } = convertToIdentifiers(ApplicationSignInExperiences, true); + + return pool.maybeOne(sql` + select + ${sql.join(Object.values(fields), sql`, `)}, + ${applications.fields.type}, + ${applications.fields.isThirdParty} from ${table} + join ${applications.table} on ${fields.applicationId}=${applications.fields.id} where ${fields.applicationId}=${applicationId} `); }; diff --git a/packages/integration-tests/src/tests/experience/overrides.test.ts b/packages/integration-tests/src/tests/experience/overrides.test.ts index 67d4790b465f..1ad22b0bd96c 100644 --- a/packages/integration-tests/src/tests/experience/overrides.test.ts +++ b/packages/integration-tests/src/tests/experience/overrides.test.ts @@ -3,13 +3,20 @@ */ import { ConnectorType } from '@logto/connector-kit'; -import { ApplicationType, type Branding, type Color, SignInIdentifier } from '@logto/schemas'; -import { pick } from '@silverhand/essentials'; - +import { + ApplicationType, + type Branding, + type Color, + SignInIdentifier, + type FullSignInExperience, +} from '@logto/schemas'; +import { appendPath, pick } from '@silverhand/essentials'; + +import api from '#src/api/api.js'; import { setApplicationSignInExperience } from '#src/api/application-sign-in-experience.js'; import { createApplication, deleteApplication } from '#src/api/application.js'; import { updateSignInExperience } from '#src/api/sign-in-experience.js'; -import { demoAppRedirectUri, demoAppUrl } from '#src/constants.js'; +import { demoAppRedirectUri, demoAppUrl, logtoUrl } from '#src/constants.js'; import { clearConnectorsByTypes } from '#src/helpers/connector.js'; import { OrganizationApiTest } from '#src/helpers/organization.js'; import ExpectExperience from '#src/ui-helpers/expect-experience.js'; @@ -194,9 +201,39 @@ describe('overrides', () => { await expectMatchBranding('light', organizationBranding.logoUrl, 'rgb(0, 0, 255)'); await expectMatchBranding('dark', organizationBranding.darkLogoUrl, 'rgb(255, 0, 255)'); + await deleteApplication(application.id); await experience.page.close(); }); + it('should not use app-level branding when the app is an third-party app', async () => { + const application = await createApplication( + 'Sign-in experience override', + ApplicationType.Traditional, + { + isThirdParty: true, + oidcClientMetadata: { + redirectUris: [demoAppRedirectUri], + postLogoutRedirectUris: [demoAppRedirectUri], + }, + } + ); + + await setApplicationSignInExperience(application.id, { + color: appColor, + branding: appBranding, + }); + + // It's hard to simulate third-party apps because their type is "Traditional" while our demo + // app is an SPA. Only test the API response here. + const experience = await api + .get(appendPath(new URL(logtoUrl), 'api/.well-known/sign-in-exp')) + .json(); + + expect(experience.branding).toEqual(omniBranding); + + await deleteApplication(application.id); + }); + describe('override fallback', () => { beforeAll(async () => { await updateSignInExperience({ @@ -261,6 +298,7 @@ describe('overrides', () => { expect(faviconElement).toBe(appBranding.favicon); expect(appleFavicon).toBe(appBranding.favicon); + await deleteApplication(application.id); await experience.page.close(); }); }); From 608349e8eafc99d76ac08c0e6f06fd58b7b218db Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 12 Jul 2024 14:19:38 +0800 Subject: [PATCH 005/135] refactor(core): refactor organizations in grants (#6208) --- .../src/oidc/grants/client-credentials.ts | 28 ++--- .../src/oidc/grants/refresh-token.test.ts | 40 ++++-- .../core/src/oidc/grants/refresh-token.ts | 105 ++++------------ .../src/oidc/grants/token-exchange/index.ts | 35 +----- packages/core/src/oidc/grants/utils.ts | 114 +++++++++++++++++- .../api/oidc/refresh-token-grant.test.ts | 18 +-- 6 files changed, 187 insertions(+), 153 deletions(-) diff --git a/packages/core/src/oidc/grants/client-credentials.ts b/packages/core/src/oidc/grants/client-credentials.ts index 8aafdeec2edb..7dffd2be47ab 100644 --- a/packages/core/src/oidc/grants/client-credentials.ts +++ b/packages/core/src/oidc/grants/client-credentials.ts @@ -19,7 +19,6 @@ * The commit hash of the original file is `0c52469f08b0a4a1854d90a96546a3f7aa090e5e`. */ -import { buildOrganizationUrn } from '@logto/core-kit'; import { cond } from '@silverhand/essentials'; import type Provider from 'oidc-provider'; import { errors } from 'oidc-provider'; @@ -30,9 +29,7 @@ import { type EnvSet } from '#src/env-set/index.js'; import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; -import { getSharedResourceServerData, reversedResourceAccessTokenTtl } from '../resource.js'; - -import { handleClientCertificate, handleDPoP } from './utils.js'; +import { handleClientCertificate, handleDPoP, handleOrganizationToken } from './utils.js'; const { AccessDenied, InvalidClient, InvalidGrant, InvalidScope, InvalidTarget } = errors; @@ -130,26 +127,17 @@ export const buildHandler: ( // If it's present, the flow falls into the `checkResource` and `if (resourceServer)` block above. if (organizationId && !resourceServer) { /* === RFC 0006 === */ - const audience = buildOrganizationUrn(organizationId); const availableScopes = await queries.organizations.relations.appsRoles .getApplicationScopes(organizationId, client.clientId) .then((scope) => scope.map(({ name }) => name)); - /** The intersection of the available scopes and the requested scopes. */ - const issuedScopes = availableScopes.filter((scope) => scopes.includes(scope)).join(' '); - - token.aud = audience; - // Note: the original implementation uses `new provider.ResourceServer` to create the resource - // server. But it's not available in the typings. The class is actually very simple and holds - // no provider-specific context. So we just create the object manually. - // See https://github.com/panva/node-oidc-provider/blob/cf2069cbb31a6a855876e95157372d25dde2511c/lib/helpers/resource_server.js - token.resourceServer = { - ...getSharedResourceServerData(envSet), - accessTokenTTL: reversedResourceAccessTokenTtl, - audience, - scope: availableScopes.join(' '), - }; - token.scope = issuedScopes; + await handleOrganizationToken({ + envSet, + availableScopes, + accessToken: token, + organizationId, + scope: new Set(scopes), + }); /* === End RFC 0006 === */ } diff --git a/packages/core/src/oidc/grants/refresh-token.test.ts b/packages/core/src/oidc/grants/refresh-token.test.ts index 69d8e21b77f2..8ec1f459f928 100644 --- a/packages/core/src/oidc/grants/refresh-token.test.ts +++ b/packages/core/src/oidc/grants/refresh-token.test.ts @@ -191,16 +191,6 @@ describe('refresh token grant', () => { ); }); - it('should throw when refresh token has no organization scope', async () => { - const ctx = createOidcContext(validOidcContext); - stubRefreshToken(ctx, { - scopes: new Set(), - }); - await expect(mockHandler()(ctx, noop)).rejects.toMatchError( - new errors.InsufficientScope('refresh token missing required scope', UserScope.Organizations) - ); - }); - it('should throw when refresh token has no grant id or the grant cannot be found', async () => { const ctx = createOidcContext(validOidcContext); const findRefreshToken = stubRefreshToken(ctx, { @@ -311,6 +301,36 @@ describe('refresh token grant', () => { ); }); + it('should throw when refresh token has no organization scope', async () => { + const ctx = createOidcContext({ + ...validOidcContext, + params: { + ...validOidcContext.params, + scope: '', + }, + }); + const tenant = new MockTenant(); + stubRefreshToken(ctx, { + scopes: new Set(), + }); + stubGrant(ctx); + Sinon.stub(tenant.queries.organizations.relations.users, 'exists').resolves(true); + Sinon.stub(tenant.queries.applications, 'findApplicationById').resolves(mockApplication); + Sinon.stub(tenant.queries.organizations.relations.usersRoles, 'getUserScopes').resolves([ + { tenantId: 'default', id: 'foo', name: 'foo', description: 'foo' }, + { tenantId: 'default', id: 'bar', name: 'bar', description: 'bar' }, + { tenantId: 'default', id: 'baz', name: 'baz', description: 'baz' }, + ]); + Sinon.stub(tenant.queries.organizations, 'getMfaStatus').resolves({ + isMfaRequired: false, + hasMfaConfigured: false, + }); + + await expect(mockHandler(tenant)(ctx, noop)).rejects.toMatchError( + new errors.InsufficientScope('refresh token missing required scope', UserScope.Organizations) + ); + }); + it('should not explode when everything looks fine', async () => { const ctx = createPreparedContext(); const tenant = new MockTenant(); diff --git a/packages/core/src/oidc/grants/refresh-token.ts b/packages/core/src/oidc/grants/refresh-token.ts index f3fa05fff5b5..9d885d58fcad 100644 --- a/packages/core/src/oidc/grants/refresh-token.ts +++ b/packages/core/src/oidc/grants/refresh-token.ts @@ -19,8 +19,8 @@ * The commit hash of the original file is `cf2069cbb31a6a855876e95157372d25dde2511c`. */ -import { UserScope, buildOrganizationUrn } from '@logto/core-kit'; -import { isKeyInObject, cond } from '@silverhand/essentials'; +import { UserScope } from '@logto/core-kit'; +import { isKeyInObject } from '@silverhand/essentials'; import type Provider from 'oidc-provider'; import { errors } from 'oidc-provider'; import difference from 'oidc-provider/lib/helpers/_/difference.js'; @@ -35,13 +35,11 @@ import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; import { - getSharedResourceServerData, - isThirdPartyApplication, - reversedResourceAccessTokenTtl, - isOrganizationConsentedToApplication, -} from '../resource.js'; - -import { handleClientCertificate, handleDPoP } from './utils.js'; + handleClientCertificate, + handleDPoP, + handleOrganizationToken, + checkOrganizationAccess, +} from './utils.js'; const { InvalidClient, InvalidGrant, InvalidScope, InsufficientScope, AccessDenied } = errors; @@ -72,7 +70,7 @@ export const buildHandler: ( // eslint-disable-next-line complexity ) => Parameters[1] = (envSet, queries) => async (ctx, next) => { const { client, params, requestParamScopes, provider } = ctx.oidc; - const { RefreshToken, Account, AccessToken, Grant, ReplayDetection, IdToken } = provider; + const { RefreshToken, Account, AccessToken, Grant, IdToken } = provider; assertThat(params, new InvalidGrant('parameters must be available')); assertThat(client, new InvalidClient('client must be available')); @@ -83,11 +81,7 @@ export const buildHandler: ( const { rotateRefreshToken, conformIdTokenClaims, - features: { - mTLS: { getCertificate }, - userinfo, - resourceIndicators, - }, + features: { userinfo, resourceIndicators }, } = providerInstance.configuration(); // @gao: I believe the presence of the param is validated by required parameters of this grant. @@ -107,18 +101,6 @@ export const buildHandler: ( throw new InvalidGrant('refresh token is expired'); } - /* === RFC 0001 === */ - // The value type is `unknown`, which will swallow other type inferences. So we have to cast it - // to `Boolean` first. - const organizationId = cond(Boolean(params.organization_id) && String(params.organization_id)); - if ( - organizationId && // Validate if the refresh token has the required scope from RFC 0001. - !refreshToken.scopes.has(UserScope.Organizations) - ) { - throw new InsufficientScope('refresh token missing required scope', UserScope.Organizations); - } - /* === End RFC 0001 === */ - if (!refreshToken.grantId) { throw new InvalidGrant('grantId not found'); } @@ -177,45 +159,14 @@ export const buildHandler: ( throw new InvalidGrant('refresh token already used'); } - /* === RFC 0001 === */ - if (organizationId) { - // Check membership - if ( - !(await queries.organizations.relations.users.exists({ - organizationId, - userId: account.accountId, - })) - ) { - const error = new AccessDenied('user is not a member of the organization'); - error.statusCode = 403; - throw error; - } - - // Check if the organization is granted (third-party application only) by the user - if ( - (await isThirdPartyApplication(queries, client.clientId)) && - !(await isOrganizationConsentedToApplication( - queries, - client.clientId, - account.accountId, - organizationId - )) - ) { - const error = new AccessDenied('organization access is not granted to the application'); - error.statusCode = 403; - throw error; - } + const { organizationId } = await checkOrganizationAccess(ctx, queries, account); - // Check if the organization requires MFA and the user has MFA enabled - const { isMfaRequired, hasMfaConfigured } = await queries.organizations.getMfaStatus( - organizationId, - account.accountId - ); - if (isMfaRequired && !hasMfaConfigured) { - const error = new AccessDenied('organization requires MFA but user has no MFA configured'); - error.statusCode = 403; - throw error; - } + /* === RFC 0001 === */ + if ( + organizationId && // Validate if the refresh token has the required scope from RFC 0001. + !refreshToken.scopes.has(UserScope.Organizations) + ) { + throw new InsufficientScope('refresh token missing required scope', UserScope.Organizations); } /* === End RFC 0001 === */ @@ -281,27 +232,17 @@ export const buildHandler: ( // the logic is handled in `getResourceServerInfo` and `extraTokenClaims`, see the init file of oidc-provider. if (organizationId && !params.resource) { /* === RFC 0001 === */ - const audience = buildOrganizationUrn(organizationId); /** All available scopes for the user in the organization. */ const availableScopes = await queries.organizations.relations.usersRoles .getUserScopes(organizationId, account.accountId) .then((scopes) => scopes.map(({ name }) => name)); - - /** The intersection of the available scopes and the requested scopes. */ - const issuedScopes = availableScopes.filter((name) => scope.has(name)).join(' '); - - at.aud = audience; - // Note: the original implementation uses `new provider.ResourceServer` to create the resource - // server. But it's not available in the typings. The class is actually very simple and holds - // no provider-specific context. So we just create the object manually. - // See https://github.com/panva/node-oidc-provider/blob/cf2069cbb31a6a855876e95157372d25dde2511c/lib/helpers/resource_server.js - at.resourceServer = { - ...getSharedResourceServerData(envSet), - accessTokenTTL: reversedResourceAccessTokenTtl, - audience, - scope: availableScopes.join(' '), - }; - at.scope = issuedScopes; + await handleOrganizationToken({ + envSet, + availableScopes, + accessToken: at, + organizationId, + scope, + }); /* === End RFC 0001 === */ } else { const resource = await resolveResource( diff --git a/packages/core/src/oidc/grants/token-exchange/index.ts b/packages/core/src/oidc/grants/token-exchange/index.ts index b4758e7537eb..669e1ee49d69 100644 --- a/packages/core/src/oidc/grants/token-exchange/index.ts +++ b/packages/core/src/oidc/grants/token-exchange/index.ts @@ -6,7 +6,7 @@ import { buildOrganizationUrn } from '@logto/core-kit'; import { GrantType } from '@logto/schemas'; -import { cond, trySafe } from '@silverhand/essentials'; +import { trySafe } from '@silverhand/essentials'; import type Provider from 'oidc-provider'; import { errors } from 'oidc-provider'; import resolveResource from 'oidc-provider/lib/helpers/resolve_resource.js'; @@ -22,7 +22,7 @@ import { getSharedResourceServerData, reversedResourceAccessTokenTtl, } from '../../resource.js'; -import { handleClientCertificate, handleDPoP } from '../utils.js'; +import { handleClientCertificate, handleDPoP, checkOrganizationAccess } from '../utils.js'; import { handleActorToken } from './actor-token.js'; import { TokenExchangeTokenType, type TokenExchangeAct } from './types.js'; @@ -96,36 +96,7 @@ export const buildHandler: ( ctx.oidc.entity('Account', account); - /* === RFC 0001 === */ - // The value type is `unknown`, which will swallow other type inferences. So we have to cast it - // to `Boolean` first. - const organizationId = cond(Boolean(params.organization_id) && String(params.organization_id)); - - if (organizationId) { - // Check membership - if ( - !(await queries.organizations.relations.users.exists({ - organizationId, - userId: account.accountId, - })) - ) { - const error = new AccessDenied('user is not a member of the organization'); - error.statusCode = 403; - throw error; - } - - // Check if the organization requires MFA and the user has MFA enabled - const { isMfaRequired, hasMfaConfigured } = await queries.organizations.getMfaStatus( - organizationId, - account.accountId - ); - if (isMfaRequired && !hasMfaConfigured) { - const error = new AccessDenied('organization requires MFA but user has no MFA configured'); - error.statusCode = 403; - throw error; - } - } - /* === End RFC 0001 === */ + const { organizationId } = await checkOrganizationAccess(ctx, queries, account); const accessToken = new AccessToken({ accountId: account.accountId, diff --git a/packages/core/src/oidc/grants/utils.ts b/packages/core/src/oidc/grants/utils.ts index 7720c67ef934..1242cd1b275f 100644 --- a/packages/core/src/oidc/grants/utils.ts +++ b/packages/core/src/oidc/grants/utils.ts @@ -1,13 +1,24 @@ +import { buildOrganizationUrn } from '@logto/core-kit'; +import { cond } from '@silverhand/essentials'; import type Provider from 'oidc-provider'; -import { errors, type KoaContextWithOIDC } from 'oidc-provider'; +import { type Account, errors, type KoaContextWithOIDC } from 'oidc-provider'; import certificateThumbprint from 'oidc-provider/lib/helpers/certificate_thumbprint.js'; import epochTime from 'oidc-provider/lib/helpers/epoch_time.js'; import dpopValidate from 'oidc-provider/lib/helpers/validate_dpop.js'; import instance from 'oidc-provider/lib/helpers/weak_cache.js'; +import { type EnvSet } from '#src/env-set/index.js'; +import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; -const { InvalidGrant, InvalidClient } = errors; +import { + getSharedResourceServerData, + isOrganizationConsentedToApplication, + isThirdPartyApplication, + reversedResourceAccessTokenTtl, +} from '../resource.js'; + +const { InvalidGrant, InvalidClient, AccessDenied } = errors; /** * Handle DPoP bound access tokens. @@ -79,3 +90,102 @@ export const handleClientCertificate = async ( } } }; + +/** + * Implement access check for RFC 0001 + */ +export const checkOrganizationAccess = async ( + ctx: KoaContextWithOIDC, + queries: Queries, + account: Account +): Promise<{ organizationId?: string }> => { + const { client, params } = ctx.oidc; + + assertThat(params, new InvalidGrant('parameters must be available')); + assertThat(client, new InvalidClient('client must be available')); + + const organizationId = cond(Boolean(params.organization_id) && String(params.organization_id)); + + if (organizationId) { + // Check membership + if ( + !(await queries.organizations.relations.users.exists({ + organizationId, + userId: account.accountId, + })) + ) { + const error = new AccessDenied('user is not a member of the organization'); + // eslint-disable-next-line @silverhand/fp/no-mutation + error.statusCode = 403; + throw error; + } + + // Check if the organization is granted (third-party application only) by the user + if ( + (await isThirdPartyApplication(queries, client.clientId)) && + !(await isOrganizationConsentedToApplication( + queries, + client.clientId, + account.accountId, + organizationId + )) + ) { + const error = new AccessDenied('organization access is not granted to the application'); + // eslint-disable-next-line @silverhand/fp/no-mutation + error.statusCode = 403; + throw error; + } + + // Check if the organization requires MFA and the user has MFA enabled + const { isMfaRequired, hasMfaConfigured } = await queries.organizations.getMfaStatus( + organizationId, + account.accountId + ); + if (isMfaRequired && !hasMfaConfigured) { + const error = new AccessDenied('organization requires MFA but user has no MFA configured'); + // eslint-disable-next-line @silverhand/fp/no-mutation + error.statusCode = 403; + throw error; + } + } + + return { organizationId }; +}; + +/** + * Implement organization token for RFC 0001 + */ +export const handleOrganizationToken = async ({ + envSet, + availableScopes, + accessToken: at, + organizationId, + scope, +}: { + envSet: EnvSet; + availableScopes: string[]; + accessToken: InstanceType | InstanceType; + organizationId: string; + scope: Set; +}): Promise => { + /* eslint-disable @silverhand/fp/no-mutation */ + const audience = buildOrganizationUrn(organizationId); + + /** The intersection of the available scopes and the requested scopes. */ + const issuedScopes = availableScopes.filter((name) => scope.has(name)).join(' '); + + at.aud = audience; + // Note: the original implementation uses `new provider.ResourceServer` to create the resource + // server. But it's not available in the typings. The class is actually very simple and holds + // no provider-specific context. So we just create the object manually. + // See https://github.com/panva/node-oidc-provider/blob/cf2069cbb31a6a855876e95157372d25dde2511c/lib/helpers/resource_server.js + at.resourceServer = { + ...getSharedResourceServerData(envSet), + accessTokenTTL: reversedResourceAccessTokenTtl, + audience, + scope: availableScopes.join(' '), + }; + at.scope = issuedScopes; + + /* eslint-enable @silverhand/fp/no-mutation */ +}; diff --git a/packages/integration-tests/src/tests/api/oidc/refresh-token-grant.test.ts b/packages/integration-tests/src/tests/api/oidc/refresh-token-grant.test.ts index 0577f4d3d1d4..c0cbdfc0a06e 100644 --- a/packages/integration-tests/src/tests/api/oidc/refresh-token-grant.test.ts +++ b/packages/integration-tests/src/tests/api/oidc/refresh-token-grant.test.ts @@ -242,13 +242,6 @@ describe('`refresh_token` grant (for organization tokens)', () => { expect(response.access_token).not.toContain('.'); }); - it('should return error when organizations scope is not requested', async () => { - const client = await initClient({ scopes: [] }); - await expect(client.fetchOrganizationToken('1')).rejects.toMatchError( - grantErrorContaining('oidc.insufficient_scope', 'refresh token missing required scope', 403) - ); - }); - it('should return access denied when organization id is invalid', async () => { const client = await initClient(); await expect(client.fetchOrganizationToken('1')).rejects.toMatchError(accessDeniedError); @@ -273,6 +266,17 @@ describe('`refresh_token` grant (for organization tokens)', () => { await expect(client.fetchOrganizationToken(org.id)).rejects.toMatchError(accessDeniedError); }); + it('should return error when organizations scope is not requested', async () => { + const org = await organizationApi.create({ name: 'org' }); + await organizationApi.addUsers(org.id, [userId]); + + const client = await initClient({ scopes: [] }); + await expect(client.fetchOrganizationToken(org.id)).rejects.toMatchError( + grantErrorContaining('oidc.insufficient_scope', 'refresh token missing required scope', 403) + ); + await organizationApi.deleteUser(org.id, userId); + }); + it('should issue organization scopes even organization resource is not requested (handled by SDK)', async () => { const { orgs } = await initOrganizations(); From 485b0a69152bee433e9aa27aa1b85544d5518f59 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 12 Jul 2024 14:56:11 +0800 Subject: [PATCH 006/135] test: add resource test cases for token exchange (#6216) * feat(core): handle dpop and client certificate for token exchange * refactor(core): refactor organizations in grants * test: add resource test cases for token exchange --- .../src/tests/api/oidc/token-exchange.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts b/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts index 92aefa80a634..a6ec845f421d 100644 --- a/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts +++ b/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts @@ -207,6 +207,47 @@ describe('Token Exchange', () => { }); }); + describe('get access token for resource', () => { + it('should exchange an access token with resource as `aud`', async () => { + const { subjectToken } = await createSubjectToken(testUserId); + + const { access_token } = await oidcApi + .post('token', { + headers: formUrlEncodedHeaders, + body: new URLSearchParams({ + client_id: testApplicationId, + grant_type: GrantType.TokenExchange, + subject_token: subjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + resource: testApiResourceInfo.indicator, + }), + }) + .json<{ access_token: string }>(); + + expect(getAccessTokenPayload(access_token)).toHaveProperty( + 'aud', + testApiResourceInfo.indicator + ); + }); + + it('should fail with invalid resource', async () => { + const { subjectToken } = await createSubjectToken(testUserId); + + await expect( + oidcApi.post('token', { + headers: formUrlEncodedHeaders, + body: new URLSearchParams({ + client_id: testApplicationId, + grant_type: GrantType.TokenExchange, + subject_token: subjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + resource: 'invalid_resource', + }), + }) + ).rejects.toThrow(); + }); + }); + describe('get access token for organization', () => { const scopeName = `read:${randomString()}`; From dcb62d69d453e8edd46409e3150f294ae7c71ef9 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Fri, 12 Jul 2024 18:16:43 +0800 Subject: [PATCH 007/135] feat(core,schemas): introduce new PUT experience API (#6212) * feat(core,schemas): introduce new PUT experience API introduce new PUT experience API * fix(core): fix some comments fix some comments --- .../classes/experience-interaction.ts | 50 ++++++++------- packages/core/src/routes/experience/index.ts | 64 +++++++++++++++++-- .../middleware/koa-experience-interaction.ts | 5 +- .../src/client/experience/index.ts | 19 ++++++ .../src/helpers/experience/index.ts | 10 ++- .../api/experience-api/interaction.test.ts | 34 ++++++++++ packages/schemas/src/types/interactions.ts | 11 +++- 7 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 packages/integration-tests/src/tests/api/experience-api/interaction.test.ts diff --git a/packages/core/src/routes/experience/classes/experience-interaction.ts b/packages/core/src/routes/experience/classes/experience-interaction.ts index b3e1c4857628..ec37d2590b6b 100644 --- a/packages/core/src/routes/experience/classes/experience-interaction.ts +++ b/packages/core/src/routes/experience/classes/experience-interaction.ts @@ -38,31 +38,32 @@ const interactionStorageGuard = z.object({ * @see {@link https://github.com/logto-io/rfcs | Logto RFCs} for more information about RFC 0004. */ export default class ExperienceInteraction { - /** - * Factory method to create a new `ExperienceInteraction` using the current context. - */ - static async create(ctx: WithLogContext, tenant: TenantContext) { - const { provider } = tenant; - const interactionDetails = await provider.interactionDetails(ctx.req, ctx.res); - return new ExperienceInteraction(ctx, tenant, interactionDetails); - } - - /** The interaction event for the current interaction. */ - private interactionEvent?: InteractionEvent; /** The user verification record list for the current interaction. */ - private readonly verificationRecords: Map; + private readonly verificationRecords = new Map(); /** The userId of the user for the current interaction. Only available once the user is identified. */ private userId?: string; /** The user provided profile data in the current interaction that needs to be stored to database. */ private readonly profile?: Record; // TODO: Fix the type + /** The interaction event for the current interaction. */ + #interactionEvent?: InteractionEvent; + /** + * Create a new `ExperienceInteraction` instance. + * + * If the `interactionDetails` is provided, the instance will be initialized with the data from the `interactionDetails` storage. + * Otherwise, a brand new instance will be created. + */ constructor( private readonly ctx: WithLogContext, private readonly tenant: TenantContext, - public interactionDetails: Interaction + public interactionDetails?: Interaction ) { const { libraries, queries } = tenant; + if (!interactionDetails) { + return; + } + const result = interactionStorageGuard.safeParse(interactionDetails.result ?? {}); assertThat( @@ -72,12 +73,10 @@ export default class ExperienceInteraction { const { verificationRecords = [], profile, userId, interactionEvent } = result.data; - this.interactionEvent = interactionEvent; + this.#interactionEvent = interactionEvent; this.userId = userId; this.profile = profile; - this.verificationRecords = new Map(); - for (const record of verificationRecords) { const instance = buildVerificationRecord(libraries, queries, record); this.verificationRecords.set(instance.type, instance); @@ -88,10 +87,14 @@ export default class ExperienceInteraction { return this.userId; } + get interactionEvent() { + return this.#interactionEvent; + } + /** Set the interaction event for the current interaction */ public setInteractionEvent(interactionEvent: InteractionEvent) { // TODO: conflict event check (e.g. reset password session can't be used for sign in) - this.interactionEvent = interactionEvent; + this.#interactionEvent = interactionEvent; } /** @@ -172,19 +175,22 @@ export default class ExperienceInteraction { /** Save the current interaction result. */ public async save() { - // `mergeWithLastSubmission` will only merge current request's interaction results. - // Manually merge with previous interaction results here. - // @see {@link https://github.com/panva/node-oidc-provider/blob/c243bf6b6663c41ff3e75c09b95fb978eba87381/lib/actions/authorization/interactions.js#L106} - const { provider } = this.tenant; const details = await provider.interactionDetails(this.ctx.req, this.ctx.res); + const interactionData = this.toJson(); + // `mergeWithLastSubmission` will only merge current request's interaction results. + // Manually merge with previous interaction results here. + // @see {@link https://github.com/panva/node-oidc-provider/blob/c243bf6b6663c41ff3e75c09b95fb978eba87381/lib/actions/authorization/interactions.js#L106} await provider.interactionResult( this.ctx.req, this.ctx.res, - { ...details.result, ...this.toJson() }, + { ...details.result, ...interactionData }, { mergeWithLastSubmission: true } ); + + // Prepend the interaction data to all log entries + this.ctx.prependAllLogEntries({ interaction: interactionData }); } /** Submit the current interaction result to the OIDC provider and clear the interaction data */ diff --git a/packages/core/src/routes/experience/index.ts b/packages/core/src/routes/experience/index.ts index 3b5616ed5aee..5cbe17513e44 100644 --- a/packages/core/src/routes/experience/index.ts +++ b/packages/core/src/routes/experience/index.ts @@ -10,7 +10,7 @@ * The experience APIs can be used by developers to build custom user interaction experiences. */ -import { identificationApiPayloadGuard } from '@logto/schemas'; +import { identificationApiPayloadGuard, InteractionEvent } from '@logto/schemas'; import type Router from 'koa-router'; import { z } from 'zod'; @@ -19,6 +19,7 @@ import koaGuard from '#src/middleware/koa-guard.js'; import { type AnonymousRouter, type RouterInitArgs } from '../types.js'; +import ExperienceInteraction from './classes/experience-interaction.js'; import { experienceRoutes } from './const.js'; import koaExperienceInteraction, { type WithExperienceInteractionContext, @@ -45,6 +46,62 @@ export default function experienceApiRoutes( koaExperienceInteraction(tenant) ); + router.put( + experienceRoutes.prefix, + koaGuard({ + body: z.object({ + interactionEvent: z.nativeEnum(InteractionEvent), + }), + status: [204], + }), + async (ctx, next) => { + const { interactionEvent } = ctx.guard.body; + const { createLog } = ctx; + + createLog(`Interaction.${interactionEvent}.Update`); + + const experienceInteraction = new ExperienceInteraction(ctx, tenant); + experienceInteraction.setInteractionEvent(interactionEvent); + + await experienceInteraction.save(); + + ctx.experienceInteraction = experienceInteraction; + ctx.status = 204; + + return next(); + } + ); + + router.put( + `${experienceRoutes.prefix}/interaction-event`, + koaGuard({ + body: z.object({ + interactionEvent: z.nativeEnum(InteractionEvent), + }), + status: [204], + }), + async (ctx, next) => { + const { interactionEvent } = ctx.guard.body; + const { createLog, experienceInteraction } = ctx; + + const eventLog = createLog( + `Interaction.${experienceInteraction.interactionEvent ?? interactionEvent}.Update` + ); + + experienceInteraction.setInteractionEvent(interactionEvent); + + eventLog.append({ + interactionEvent, + }); + + await experienceInteraction.save(); + + ctx.status = 204; + + return next(); + } + ); + router.post( experienceRoutes.identification, koaGuard({ @@ -52,10 +109,7 @@ export default function experienceApiRoutes( status: [204, 400, 401, 404], }), async (ctx, next) => { - const { interactionEvent, verificationId } = ctx.guard.body; - - // TODO: implement a separate POST interaction route to handle the initiation of the interaction event - ctx.experienceInteraction.setInteractionEvent(interactionEvent); + const { verificationId } = ctx.guard.body; await ctx.experienceInteraction.identifyUser(verificationId); diff --git a/packages/core/src/routes/experience/middleware/koa-experience-interaction.ts b/packages/core/src/routes/experience/middleware/koa-experience-interaction.ts index 19ec4af51da4..2cbcb6fd6ac5 100644 --- a/packages/core/src/routes/experience/middleware/koa-experience-interaction.ts +++ b/packages/core/src/routes/experience/middleware/koa-experience-interaction.ts @@ -25,7 +25,10 @@ export default function koaExperienceInteraction< tenant: TenantContext ): MiddlewareType, ResponseT> { return async (ctx, next) => { - ctx.experienceInteraction = await ExperienceInteraction.create(ctx, tenant); + const { provider } = tenant; + const interactionDetails = await provider.interactionDetails(ctx.req, ctx.res); + + ctx.experienceInteraction = new ExperienceInteraction(ctx, tenant, interactionDetails); return next(); }; diff --git a/packages/integration-tests/src/client/experience/index.ts b/packages/integration-tests/src/client/experience/index.ts index 8a6d74d56257..2bacb75b6ccb 100644 --- a/packages/integration-tests/src/client/experience/index.ts +++ b/packages/integration-tests/src/client/experience/index.ts @@ -1,4 +1,5 @@ import { + type CreateExperienceApiPayload, type IdentificationApiPayload, type InteractionEvent, type PasswordVerificationPayload, @@ -33,6 +34,24 @@ export class ExperienceClient extends MockClient { .json(); } + public async updateInteractionEvent(payload: { interactionEvent: InteractionEvent }) { + return api + .put(`${experienceRoutes.prefix}/interaction-event`, { + headers: { cookie: this.interactionCookie }, + json: payload, + }) + .json(); + } + + public async initInteraction(payload: CreateExperienceApiPayload) { + return api + .put(experienceRoutes.prefix, { + headers: { cookie: this.interactionCookie }, + json: payload, + }) + .json(); + } + public override async submitInteraction(): Promise { return api .post(`${experienceRoutes.prefix}/submit`, { headers: { cookie: this.interactionCookie } }) diff --git a/packages/integration-tests/src/helpers/experience/index.ts b/packages/integration-tests/src/helpers/experience/index.ts index 803e034a5a83..af3ebe3e660a 100644 --- a/packages/integration-tests/src/helpers/experience/index.ts +++ b/packages/integration-tests/src/helpers/experience/index.ts @@ -27,13 +27,14 @@ export const signInWithPassword = async ({ }) => { const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + const { verificationId } = await client.verifyPassword({ identifier, password, }); await client.identifyUser({ - interactionEvent: InteractionEvent.SignIn, verificationId, }); @@ -46,6 +47,8 @@ export const signInWithPassword = async ({ export const signInWithVerificationCode = async (identifier: VerificationCodeIdentifier) => { const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + const { verificationId, code } = await successfullySendVerificationCode(client, { identifier, interactionEvent: InteractionEvent.SignIn, @@ -58,7 +61,6 @@ export const signInWithVerificationCode = async (identifier: VerificationCodeIde }); await client.identifyUser({ - interactionEvent: InteractionEvent.SignIn, verificationId: verifiedVerificationId, }); @@ -80,6 +82,8 @@ export const identifyUserWithUsernamePassword = async ( username: string, password: string ) => { + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + const { verificationId } = await client.verifyPassword({ identifier: { type: InteractionIdentifierType.Username, @@ -88,7 +92,7 @@ export const identifyUserWithUsernamePassword = async ( password, }); - await client.identifyUser({ interactionEvent: InteractionEvent.SignIn, verificationId }); + await client.identifyUser({ verificationId }); return { verificationId }; }; diff --git a/packages/integration-tests/src/tests/api/experience-api/interaction.test.ts b/packages/integration-tests/src/tests/api/experience-api/interaction.test.ts new file mode 100644 index 000000000000..37be24186772 --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/interaction.test.ts @@ -0,0 +1,34 @@ +import { InteractionEvent, InteractionIdentifierType } from '@logto/schemas'; + +import { initExperienceClient } from '#src/helpers/client.js'; +import { expectRejects } from '#src/helpers/index.js'; +import { generateNewUserProfile, UserApiTest } from '#src/helpers/user.js'; +import { devFeatureTest } from '#src/utils.js'; + +devFeatureTest.describe('PUT /experience API', () => { + const userApi = new UserApiTest(); + + afterAll(async () => { + await userApi.cleanUp(); + }); + + it('PUT new experience API should reset all existing verification records', async () => { + const { username, password } = generateNewUserProfile({ username: true, password: true }); + const user = await userApi.create({ username, password }); + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + const { verificationId } = await client.verifyPassword({ + identifier: { type: InteractionIdentifierType.Username, value: username }, + password, + }); + + // PUT /experience + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + + await expectRejects(client.identifyUser({ verificationId }), { + code: 'session.verification_session_not_found', + status: 404, + }); + }); +}); diff --git a/packages/schemas/src/types/interactions.ts b/packages/schemas/src/types/interactions.ts index 38079bd4d791..877974224726 100644 --- a/packages/schemas/src/types/interactions.ts +++ b/packages/schemas/src/types/interactions.ts @@ -117,14 +117,21 @@ export const backupCodeVerificationVerifyPayloadGuard = z.object({ /** Payload type for `POST /api/experience/identification`. */ export type IdentificationApiPayload = { - interactionEvent: InteractionEvent; + /** The ID of the verification record that is used to identify the user. */ verificationId: string; }; export const identificationApiPayloadGuard = z.object({ - interactionEvent: z.nativeEnum(InteractionEvent), verificationId: z.string(), }) satisfies ToZodObject; +/** Payload type for `POST /api/experience`. */ +export type CreateExperienceApiPayload = { + interactionEvent: InteractionEvent; +}; +export const CreateExperienceApiPayloadGuard = z.object({ + interactionEvent: z.nativeEnum(InteractionEvent), +}) satisfies ToZodObject; + // ====== Experience API payload guard and types definitions end ====== /** From d203c8d2ff2ddc323a7442bd52f9adf470f0fb33 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 12 Jul 2024 19:00:36 +0800 Subject: [PATCH 008/135] refactor: experience ssr (#6229) * refactor: experience ssr * refactor: fix parameter issue --- .changeset/shy-baboons-occur.md | 13 ++ .../src/middleware/koa-experience-ssr.test.ts | 81 +++++++++ .../core/src/middleware/koa-experience-ssr.ts | 67 ++++++++ .../core/src/middleware/koa-serve-static.ts | 30 ++-- packages/core/src/oidc/init.ts | 17 +- packages/core/src/oidc/utils.ts | 10 +- packages/core/src/routes/well-known.ts | 23 +-- packages/core/src/tenants/Tenant.ts | 4 +- packages/core/src/utils/i18n.ts | 31 ++++ packages/experience/src/i18n/utils.ts | 41 +++-- packages/experience/src/include.d/global.d.ts | 14 +- packages/experience/src/index.html | 16 +- packages/experience/src/jest.setup.ts | 4 + .../experience/src/utils/search-parameters.ts | 6 +- .../src/utils/sign-in-experience.ts | 17 +- .../src/include.d/global.d.ts | 4 + .../experience/server-side-rendering.test.ts | 162 ++++++++++++++++++ packages/schemas/src/types/cookie.ts | 11 +- packages/schemas/src/types/index.ts | 1 + .../schemas/src/types/sign-in-experience.ts | 2 +- packages/schemas/src/types/ssr.ts | 28 +++ 21 files changed, 500 insertions(+), 82 deletions(-) create mode 100644 .changeset/shy-baboons-occur.md create mode 100644 packages/core/src/middleware/koa-experience-ssr.test.ts create mode 100644 packages/core/src/middleware/koa-experience-ssr.ts create mode 100644 packages/integration-tests/src/include.d/global.d.ts create mode 100644 packages/integration-tests/src/tests/experience/server-side-rendering.test.ts create mode 100644 packages/schemas/src/types/ssr.ts diff --git a/.changeset/shy-baboons-occur.md b/.changeset/shy-baboons-occur.md new file mode 100644 index 000000000000..2ce015d70acc --- /dev/null +++ b/.changeset/shy-baboons-occur.md @@ -0,0 +1,13 @@ +--- +"@logto/experience": minor +"@logto/schemas": minor +"@logto/core": minor +"@logto/integration-tests": patch +--- + +support experience data server-side rendering + +Logto now injects the sign-in experience settings and phrases into the `index.html` file for better first-screen performance. The experience app will still fetch the settings and phrases from the server if: + +- The server didn't inject the settings and phrases. +- The parameters in the URL are different from server-rendered data. diff --git a/packages/core/src/middleware/koa-experience-ssr.test.ts b/packages/core/src/middleware/koa-experience-ssr.test.ts new file mode 100644 index 000000000000..44c17227fcf2 --- /dev/null +++ b/packages/core/src/middleware/koa-experience-ssr.test.ts @@ -0,0 +1,81 @@ +import { ssrPlaceholder } from '@logto/schemas'; + +import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; +import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; + +import koaExperienceSsr from './koa-experience-ssr.js'; + +const { jest } = import.meta; + +describe('koaExperienceSsr()', () => { + const phrases = { foo: 'bar' }; + const baseCtx = Object.freeze({ + ...createContextWithRouteParameters({}), + locale: 'en', + query: {}, + set: jest.fn(), + }); + const tenant = new MockTenant( + undefined, + { + customPhrases: { + findAllCustomLanguageTags: jest.fn().mockResolvedValue([]), + }, + }, + undefined, + { + signInExperiences: { + getFullSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience), + }, + phrases: { getPhrases: jest.fn().mockResolvedValue(phrases) }, + } + ); + + const next = jest.fn().mockReturnValue(Promise.resolve()); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call next() and do nothing if the response body is not a string', async () => { + const symbol = Symbol('nothing'); + const ctx = { ...baseCtx, body: symbol }; + await koaExperienceSsr(tenant.libraries, tenant.queries)(ctx, next); + expect(next).toHaveBeenCalledTimes(1); + expect(ctx.body).toBe(symbol); + }); + + it('should call next() and do nothing if the request path is not an index path', async () => { + const ctx = { ...baseCtx, path: '/foo', body: '...' }; + await koaExperienceSsr(tenant.libraries, tenant.queries)(ctx, next); + expect(next).toHaveBeenCalledTimes(1); + expect(ctx.body).toBe('...'); + }); + + it('should call next() and do nothing if the required placeholders are not present', async () => { + const ctx = { ...baseCtx, path: '/', body: '...' }; + await koaExperienceSsr(tenant.libraries, tenant.queries)(ctx, next); + expect(next).toHaveBeenCalledTimes(1); + expect(ctx.body).toBe('...'); + }); + + it('should prefetch the experience data and inject it into the HTML response', async () => { + const ctx = { + ...baseCtx, + path: '/', + body: ``, + }; + await koaExperienceSsr(tenant.libraries, tenant.queries)(ctx, next); + expect(next).toHaveBeenCalledTimes(1); + expect(ctx.body).not.toContain(ssrPlaceholder); + expect(ctx.body).toContain( + `const logtoSsr=Object.freeze(${JSON.stringify({ + signInExperience: { data: mockSignInExperience }, + phrases: { lng: 'en', data: phrases }, + })});` + ); + }); +}); diff --git a/packages/core/src/middleware/koa-experience-ssr.ts b/packages/core/src/middleware/koa-experience-ssr.ts new file mode 100644 index 000000000000..1ddab180399a --- /dev/null +++ b/packages/core/src/middleware/koa-experience-ssr.ts @@ -0,0 +1,67 @@ +import { type SsrData, logtoCookieKey, logtoUiCookieGuard, ssrPlaceholder } from '@logto/schemas'; +import { pick, trySafe } from '@silverhand/essentials'; +import type { MiddlewareType } from 'koa'; + +import type Libraries from '#src/tenants/Libraries.js'; +import type Queries from '#src/tenants/Queries.js'; +import { getExperienceLanguage } from '#src/utils/i18n.js'; + +import { type WithI18nContext } from './koa-i18next.js'; +import { isIndexPath } from './koa-serve-static.js'; + +/** + * Create a middleware to prefetch the experience data and inject it into the HTML response. Some + * conditions must be met: + * + * - The response body should be a string after the middleware chain (calling `next()`). + * - The request path should be an index path. + * - The SSR placeholder string ({@link ssrPlaceholder}) should be present in the response body. + * + * Otherwise, the middleware will do nothing. + */ +export default function koaExperienceSsr( + libraries: Libraries, + queries: Queries +): MiddlewareType { + return async (ctx, next) => { + await next(); + + if ( + !(typeof ctx.body === 'string' && isIndexPath(ctx.path)) || + !ctx.body.includes(ssrPlaceholder) + ) { + return; + } + + const logtoUiCookie = + trySafe(() => + logtoUiCookieGuard.parse(JSON.parse(ctx.cookies.get(logtoCookieKey) ?? '{}')) + ) ?? {}; + + const [signInExperience, customLanguages] = await Promise.all([ + libraries.signInExperiences.getFullSignInExperience({ + locale: ctx.locale, + ...logtoUiCookie, + }), + queries.customPhrases.findAllCustomLanguageTags(), + ]); + const language = getExperienceLanguage({ + ctx, + languageInfo: signInExperience.languageInfo, + customLanguages, + }); + const phrases = await libraries.phrases.getPhrases(language); + + ctx.set('Content-Language', language); + ctx.body = ctx.body.replace( + ssrPlaceholder, + `Object.freeze(${JSON.stringify({ + signInExperience: { + ...pick(logtoUiCookie, 'appId', 'organizationId'), + data: signInExperience, + }, + phrases: { lng: language, data: phrases }, + } satisfies SsrData)})` + ); + }; +} diff --git a/packages/core/src/middleware/koa-serve-static.ts b/packages/core/src/middleware/koa-serve-static.ts index b6e6b9a95c13..0ce53b25c823 100644 --- a/packages/core/src/middleware/koa-serve-static.ts +++ b/packages/core/src/middleware/koa-serve-static.ts @@ -1,5 +1,6 @@ // Modified from https://github.com/koajs/static/blob/7f0ed88c8902e441da4e30b42f108617d8dff9ec/index.js +import fs from 'node:fs/promises'; import path from 'node:path'; import type { MiddlewareType } from 'koa'; @@ -8,8 +9,11 @@ import send from 'koa-send'; import assertThat from '#src/utils/assert-that.js'; const index = 'index.html'; +const indexContentType = 'text/html; charset=utf-8'; +export const isIndexPath = (path: string) => + ['/', `/${index}`].some((value) => path.endsWith(value)); -export default function serve(root: string) { +export default function koaServeStatic(root: string) { assertThat(root, new Error('Root directory is required to serve files.')); const options: send.SendOptions = { @@ -19,19 +23,19 @@ export default function serve(root: string) { const serve: MiddlewareType = async (ctx, next) => { if (ctx.method === 'HEAD' || ctx.method === 'GET') { - const filePath = await send(ctx, ctx.path, { - ...options, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - ...(!['/', `/${options.index || ''}`].some((path) => ctx.path.endsWith(path)) && { - maxage: 604_800_000 /* 7 days */, - }), - }); - - const filename = path.basename(filePath); - - // No cache for the index file - if (filename === index || filename.startsWith(index + '.')) { + // Directly read and set the content of the index file since we need to replace the + // placeholders in the file with the actual values. It should be OK as the index file is + // small. + if (isIndexPath(ctx.path)) { + const content = await fs.readFile(path.join(root, index), 'utf8'); + ctx.type = indexContentType; + ctx.body = content; ctx.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + } else { + await send(ctx, ctx.path, { + ...options, + maxage: 604_800_000 /* 7 days */, + }); } } diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 590f6488d765..5ec7474ab56f 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -15,7 +15,7 @@ import { type LogtoUiCookie, ExtraParamsKey, } from '@logto/schemas'; -import { conditional, trySafe, tryThat } from '@silverhand/essentials'; +import { removeUndefinedKeys, trySafe, tryThat } from '@silverhand/essentials'; import i18next from 'i18next'; import { koaBody } from 'koa-body'; import Provider, { errors } from 'oidc-provider'; @@ -198,17 +198,20 @@ export default function initOidc( }, interactions: { url: (ctx, { params: { client_id: appId }, prompt }) => { - // @deprecated use search params instead + const params = trySafe(() => extraParamsObjectGuard.parse(ctx.oidc.params ?? {})) ?? {}; + + // Cookies are required to apply the correct server-side rendering ctx.cookies.set( logtoCookieKey, - JSON.stringify({ - appId: conditional(Boolean(appId) && String(appId)), - } satisfies LogtoUiCookie), + JSON.stringify( + removeUndefinedKeys({ + appId: typeof appId === 'string' ? appId : undefined, + organizationId: params.organization_id, + }) satisfies LogtoUiCookie + ), { sameSite: 'lax', overwrite: true, httpOnly: false } ); - const params = trySafe(() => extraParamsObjectGuard.parse(ctx.oidc.params ?? {})) ?? {}; - switch (prompt.name) { case 'login': { return '/' + buildLoginPromptUrl(params, appId); diff --git a/packages/core/src/oidc/utils.ts b/packages/core/src/oidc/utils.ts index 276b56bb134d..f25a4c323f6f 100644 --- a/packages/core/src/oidc/utils.ts +++ b/packages/core/src/oidc/utils.ts @@ -94,17 +94,15 @@ export const buildLoginPromptUrl = (params: ExtraParamsObject, appId?: unknown): searchParams.append('app_id', String(appId)); } + if (params[ExtraParamsKey.OrganizationId]) { + searchParams.append(ExtraParamsKey.OrganizationId, params[ExtraParamsKey.OrganizationId]); + } + if (directSignIn) { searchParams.append('fallback', firstScreen); const [method, target] = directSignIn.split(':'); return path.join('direct', method ?? '', target ?? '') + getSearchParamString(); } - // Append other valid params as-is - const { first_screen: _, interaction_mode: __, direct_sign_in: ___, ...rest } = params; - for (const [key, value] of Object.entries(rest)) { - searchParams.append(key, value); - } - return firstScreen + getSearchParamString(); }; diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts index 3738c0f742b0..416d4b451a34 100644 --- a/packages/core/src/routes/well-known.ts +++ b/packages/core/src/routes/well-known.ts @@ -1,11 +1,9 @@ -import { isBuiltInLanguageTag } from '@logto/phrases-experience'; -import { adminTenantId, guardFullSignInExperience } from '@logto/schemas'; -import { conditionalArray } from '@silverhand/essentials'; +import { adminTenantId, fullSignInExperienceGuard } from '@logto/schemas'; import { z } from 'zod'; import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js'; -import detectLanguage from '#src/i18n/detect-language.js'; import koaGuard from '#src/middleware/koa-guard.js'; +import { getExperienceLanguage } from '#src/utils/i18n.js'; import type { AnonymousRouter, RouterInitArgs } from './types.js'; @@ -43,7 +41,7 @@ export default function wellKnownRoutes( '/.well-known/sign-in-exp', koaGuard({ query: z.object({ organizationId: z.string(), appId: z.string() }).partial(), - response: guardFullSignInExperience, + response: fullSignInExperienceGuard, status: 200, }), async (ctx, next) => { @@ -68,20 +66,9 @@ export default function wellKnownRoutes( query: { lng }, } = ctx.guard; - const { - languageInfo: { autoDetect, fallbackLanguage }, - } = await findDefaultSignInExperience(); - - const acceptableLanguages = conditionalArray( - lng, - autoDetect && detectLanguage(ctx), - fallbackLanguage - ); + const { languageInfo } = await findDefaultSignInExperience(); const customLanguages = await findAllCustomLanguageTags(); - const language = - acceptableLanguages.find( - (tag) => isBuiltInLanguageTag(tag) || customLanguages.includes(tag) - ) ?? 'en'; + const language = getExperienceLanguage({ ctx, languageInfo, customLanguages, lng }); ctx.set('Content-Language', language); ctx.body = await getPhrases(language); diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index 8d082f7fca04..092cfdeb77ba 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -17,6 +17,7 @@ import koaAutoConsent from '#src/middleware/koa-auto-consent.js'; import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js'; import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js'; import koaErrorHandler from '#src/middleware/koa-error-handler.js'; +import koaExperienceSsr from '#src/middleware/koa-experience-ssr.js'; import koaI18next from '#src/middleware/koa-i18next.js'; import koaOidcErrorHandler from '#src/middleware/koa-oidc-error-handler.js'; import koaSecurityHeaders from '#src/middleware/koa-security-headers.js'; @@ -166,9 +167,10 @@ export default class Tenant implements TenantContext { ); } - // Mount UI + // Mount experience app app.use( compose([ + koaExperienceSsr(libraries, queries), koaSpaSessionGuard(provider, queries), mount(`/${experience.routes.consent}`, koaAutoConsent(provider, queries)), koaSpaProxy(mountedApps), diff --git a/packages/core/src/utils/i18n.ts b/packages/core/src/utils/i18n.ts index b240ece70961..a900635db252 100644 --- a/packages/core/src/utils/i18n.ts +++ b/packages/core/src/utils/i18n.ts @@ -1,7 +1,38 @@ +import { isBuiltInLanguageTag } from '@logto/phrases-experience'; +import { type SignInExperience } from '@logto/schemas'; +import { conditionalArray } from '@silverhand/essentials'; import type { i18n } from 'i18next'; import _i18next from 'i18next'; +import { type ParameterizedContext } from 'koa'; +import { type IRouterParamContext } from 'koa-router'; + +import detectLanguage from '#src/i18n/detect-language.js'; // This may be fixed by a cjs require wrapper. TBD. // See https://github.com/microsoft/TypeScript/issues/49189 // eslint-disable-next-line no-restricted-syntax export const i18next = _i18next as unknown as i18n; + +type GetExperienceLanguage = { + ctx: ParameterizedContext; + languageInfo: SignInExperience['languageInfo']; + customLanguages: readonly string[]; + lng?: string; +}; + +export const getExperienceLanguage = ({ + ctx, + languageInfo: { autoDetect, fallbackLanguage }, + customLanguages, + lng, +}: GetExperienceLanguage) => { + const acceptableLanguages = conditionalArray( + lng, + autoDetect && detectLanguage(ctx), + fallbackLanguage + ); + const language = + acceptableLanguages.find((tag) => isBuiltInLanguageTag(tag) || customLanguages.includes(tag)) ?? + 'en'; + return language; +}; diff --git a/packages/experience/src/i18n/utils.ts b/packages/experience/src/i18n/utils.ts index de12fb7b9f35..82bbc432f06b 100644 --- a/packages/experience/src/i18n/utils.ts +++ b/packages/experience/src/i18n/utils.ts @@ -1,31 +1,40 @@ import type { LocalePhrase } from '@logto/phrases-experience'; import resource from '@logto/phrases-experience'; import type { LanguageInfo } from '@logto/schemas'; +import { isObject } from '@silverhand/essentials'; import type { Resource } from 'i18next'; import i18next from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -import { getPhrases } from '@/apis/settings'; +import { getPhrases as getPhrasesApi } from '@/apis/settings'; + +const getPhrases = async (language?: string) => { + // Directly use the server-side phrases if it's already fetched + if (isObject(logtoSsr) && (!language || logtoSsr.phrases.lng === language)) { + return { phrases: logtoSsr.phrases.data, lng: logtoSsr.phrases.lng }; + } -export const getI18nResource = async ( - language?: string -): Promise<{ resources: Resource; lng: string }> => { const detectedLanguage = detectLanguage(); + const response = await getPhrasesApi({ + localLanguage: Array.isArray(detectedLanguage) ? detectedLanguage.join(' ') : detectedLanguage, + language, + }); - try { - const response = await getPhrases({ - localLanguage: Array.isArray(detectedLanguage) - ? detectedLanguage.join(' ') - : detectedLanguage, - language, - }); + const remotePhrases = await response.json(); + const lng = response.headers.get('Content-Language'); + + if (!lng) { + throw new Error('lng not found'); + } - const phrases = await response.json(); - const lng = response.headers.get('Content-Language'); + return { phrases: remotePhrases, lng }; +}; - if (!lng) { - throw new Error('lng not found'); - } +export const getI18nResource = async ( + language?: string +): Promise<{ resources: Resource; lng: string }> => { + try { + const { phrases, lng } = await getPhrases(language); return { resources: { [lng]: phrases }, diff --git a/packages/experience/src/include.d/global.d.ts b/packages/experience/src/include.d/global.d.ts index ce00e0c80abd..48994b57693e 100644 --- a/packages/experience/src/include.d/global.d.ts +++ b/packages/experience/src/include.d/global.d.ts @@ -1,4 +1,4 @@ -// Logto Native SDK +import { type SsrData } from '@logto/schemas'; type LogtoNativeSdkInfo = { platform: 'ios' | 'android'; @@ -10,4 +10,14 @@ type LogtoNativeSdkInfo = { }; }; -declare const logtoNativeSdk: LogtoNativeSdkInfo | undefined; +type LogtoSsr = string | Readonly | undefined; + +declare global { + const logtoNativeSdk: LogtoNativeSdkInfo | undefined; + const logtoSsr: LogtoSsr; + + interface Window { + logtoNativeSdk: LogtoNativeSdkInfo | undefined; + logtoSsr: LogtoSsr; + } +} diff --git a/packages/experience/src/index.html b/packages/experience/src/index.html index e317d14155a1..8b5cb68dc513 100644 --- a/packages/experience/src/index.html +++ b/packages/experience/src/index.html @@ -5,21 +5,9 @@ - diff --git a/packages/experience/src/jest.setup.ts b/packages/experience/src/jest.setup.ts index bc92589ba52c..3206801a8a2f 100644 --- a/packages/experience/src/jest.setup.ts +++ b/packages/experience/src/jest.setup.ts @@ -1,4 +1,5 @@ import { type LocalePhrase } from '@logto/phrases-experience'; +import { ssrPlaceholder } from '@logto/schemas'; import { type DeepPartial } from '@silverhand/essentials'; import i18next from 'i18next'; import { initReactI18next } from 'react-i18next'; @@ -18,3 +19,6 @@ export const setupI18nForTesting = async ( }); void setupI18nForTesting(); + +// eslint-disable-next-line @silverhand/fp/no-mutating-methods +Object.defineProperty(global, 'logtoSsr', { value: ssrPlaceholder }); diff --git a/packages/experience/src/utils/search-parameters.ts b/packages/experience/src/utils/search-parameters.ts index 4c1328cfbf2e..07b779cf0073 100644 --- a/packages/experience/src/utils/search-parameters.ts +++ b/packages/experience/src/utils/search-parameters.ts @@ -1,5 +1,9 @@ import { condString } from '@silverhand/essentials'; +export const searchKeysCamelCase = Object.freeze(['organizationId', 'appId'] as const); + +type SearchKeysCamelCase = (typeof searchKeysCamelCase)[number]; + export const searchKeys = Object.freeze({ /** * The key for specifying the organization ID that may be used to override the default settings. @@ -7,7 +11,7 @@ export const searchKeys = Object.freeze({ organizationId: 'organization_id', /** The current application ID. */ appId: 'app_id', -}); +} satisfies Record); export const handleSearchParametersData = () => { const { search } = window.location; diff --git a/packages/experience/src/utils/sign-in-experience.ts b/packages/experience/src/utils/sign-in-experience.ts index 3175202ee610..6540c54fafde 100644 --- a/packages/experience/src/utils/sign-in-experience.ts +++ b/packages/experience/src/utils/sign-in-experience.ts @@ -4,12 +4,15 @@ */ import { SignInIdentifier } from '@logto/schemas'; +import { isObject } from '@silverhand/essentials'; import i18next from 'i18next'; import { getSignInExperience } from '@/apis/settings'; import type { SignInExperienceResponse } from '@/types'; import { filterSocialConnectors } from '@/utils/social-connectors'; +import { searchKeys, searchKeysCamelCase } from './search-parameters'; + const parseSignInExperienceResponse = ( response: SignInExperienceResponse ): SignInExperienceResponse => { @@ -22,8 +25,20 @@ const parseSignInExperienceResponse = ( }; export const getSignInExperienceSettings = async (): Promise => { - const response = await getSignInExperience(); + if (isObject(logtoSsr)) { + const { data, ...rest } = logtoSsr.signInExperience; + if ( + searchKeysCamelCase.every((key) => { + const ssrValue = rest[key]; + const storageValue = sessionStorage.getItem(searchKeys[key]) ?? undefined; + return (!ssrValue && !storageValue) || ssrValue === storageValue; + }) + ) { + return data; + } + } + const response = await getSignInExperience(); return parseSignInExperienceResponse(response); }; diff --git a/packages/integration-tests/src/include.d/global.d.ts b/packages/integration-tests/src/include.d/global.d.ts new file mode 100644 index 000000000000..27e85338e694 --- /dev/null +++ b/packages/integration-tests/src/include.d/global.d.ts @@ -0,0 +1,4 @@ +interface Window { + /** The SSR object for **experience**. */ + logtoSsr: unknown; +} diff --git a/packages/integration-tests/src/tests/experience/server-side-rendering.test.ts b/packages/integration-tests/src/tests/experience/server-side-rendering.test.ts new file mode 100644 index 000000000000..265c76713ebc --- /dev/null +++ b/packages/integration-tests/src/tests/experience/server-side-rendering.test.ts @@ -0,0 +1,162 @@ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { demoAppApplicationId, fullSignInExperienceGuard } from '@logto/schemas'; +import { type Page } from 'puppeteer'; +import { z } from 'zod'; + +import { demoAppUrl } from '#src/constants.js'; +import { OrganizationApiTest } from '#src/helpers/organization.js'; +import ExpectExperience from '#src/ui-helpers/expect-experience.js'; + +const ssrDataGuard = z.object({ + signInExperience: z.object({ + appId: z.string().optional(), + organizationId: z.string().optional(), + data: fullSignInExperienceGuard, + }), + phrases: z.object({ + lng: z.string(), + data: z.record(z.unknown()), + }), +}); + +class Trace { + protected tracePath?: string; + + constructor(protected page?: Page) {} + + async start() { + if (this.tracePath) { + throw new Error('Trace already started'); + } + + if (!this.page) { + throw new Error('Page not set'); + } + + const traceDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'trace-')); + this.tracePath = path.join(traceDirectory, 'trace.json'); + await this.page.tracing.start({ path: this.tracePath, categories: ['devtools.timeline'] }); + } + + async stop() { + if (!this.page) { + throw new Error('Page not set'); + } + + return this.page.tracing.stop(); + } + + async read() { + if (!this.tracePath) { + throw new Error('Trace not started'); + } + + return JSON.parse(await fs.readFile(this.tracePath, 'utf8')); + } + + reset(page: Page) { + this.page = page; + this.tracePath = undefined; + } + + async cleanup() { + if (this.tracePath) { + await fs.unlink(this.tracePath); + } + } +} + +describe('server-side rendering', () => { + const trace = new Trace(); + const expectTraceNotToHaveWellKnownEndpoints = async () => { + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + const traceData: { traceEvents: unknown[] } = await trace.read(); + expect(traceData.traceEvents).not.toContainEqual( + expect.objectContaining({ + args: expect.objectContaining({ + data: expect.objectContaining({ url: expect.stringContaining('api/.well-known/') }), + }), + }) + ); + /* eslint-enable @typescript-eslint/no-unsafe-assignment */ + }; + + afterEach(async () => { + await trace.cleanup(); + }); + + it('should render the page with data from the server and not request the well-known endpoints', async () => { + const experience = new ExpectExperience(await browser.newPage()); + + trace.reset(experience.page); + await trace.start(); + await experience.navigateTo(demoAppUrl.href); + await trace.stop(); + + // Check page variables + const data = await experience.page.evaluate(() => { + return window.logtoSsr; + }); + + const parsed = ssrDataGuard.parse(data); + + expect(parsed.signInExperience.appId).toBe(demoAppApplicationId); + expect(parsed.signInExperience.organizationId).toBeUndefined(); + + // Check network requests + await expectTraceNotToHaveWellKnownEndpoints(); + }); + + it('should render the page with data from the server with invalid organization ID', async () => { + const experience = new ExpectExperience(await browser.newPage()); + + trace.reset(experience.page); + await trace.start(); + // Although the organization ID is invalid, the server should still render the page with the + // ID provided which indicates the result under the given parameters. + await experience.navigateTo(`${demoAppUrl.href}?organization_id=org-id`); + await trace.stop(); + + // Check page variables + const data = await experience.page.evaluate(() => { + return window.logtoSsr; + }); + + const parsed = ssrDataGuard.parse(data); + + expect(parsed.signInExperience.appId).toBe(demoAppApplicationId); + expect(parsed.signInExperience.organizationId).toBe('org-id'); + + // Check network requests + await expectTraceNotToHaveWellKnownEndpoints(); + }); + + it('should render the page with data from the server with valid organization ID', async () => { + const logoUrl = 'mock://fake-url-for-ssr/logo.png'; + const organizationApi = new OrganizationApiTest(); + const organization = await organizationApi.create({ name: 'foo', branding: { logoUrl } }); + const experience = new ExpectExperience(await browser.newPage()); + + trace.reset(experience.page); + await trace.start(); + await experience.navigateTo(`${demoAppUrl.href}?organization_id=${organization.id}`); + await trace.stop(); + + // Check page variables + const data = await experience.page.evaluate(() => { + return window.logtoSsr; + }); + + const parsed = ssrDataGuard.parse(data); + + expect(parsed.signInExperience.appId).toBe(demoAppApplicationId); + expect(parsed.signInExperience.organizationId).toBe(organization.id); + expect(parsed.signInExperience.data.branding.logoUrl).toBe(logoUrl); + + // Check network requests + await expectTraceNotToHaveWellKnownEndpoints(); + }); +}); diff --git a/packages/schemas/src/types/cookie.ts b/packages/schemas/src/types/cookie.ts index 4e76f7bb01bb..a155067f1367 100644 --- a/packages/schemas/src/types/cookie.ts +++ b/packages/schemas/src/types/cookie.ts @@ -1,5 +1,12 @@ import { z } from 'zod'; -export const logtoUiCookieGuard = z.object({ appId: z.string() }).partial(); +import { type ToZodObject } from '../utils/zod.js'; -export type LogtoUiCookie = z.infer; +export type LogtoUiCookie = Partial<{ + appId: string; + organizationId: string; +}>; + +export const logtoUiCookieGuard = z + .object({ appId: z.string(), organizationId: z.string() }) + .partial() satisfies ToZodObject; diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts index d6bfbac974ae..6806a3ca3381 100644 --- a/packages/schemas/src/types/index.ts +++ b/packages/schemas/src/types/index.ts @@ -29,3 +29,4 @@ export * from './consent.js'; export * from './onboarding.js'; export * from './sign-in-experience.js'; export * from './subject-token.js'; +export * from './ssr.js'; diff --git a/packages/schemas/src/types/sign-in-experience.ts b/packages/schemas/src/types/sign-in-experience.ts index 9f6bf95ebb1e..57bb8d2d96de 100644 --- a/packages/schemas/src/types/sign-in-experience.ts +++ b/packages/schemas/src/types/sign-in-experience.ts @@ -41,7 +41,7 @@ export type FullSignInExperience = SignInExperience & { googleOneTap?: GoogleOneTapConfig & { clientId: string; connectorId: string }; }; -export const guardFullSignInExperience = SignInExperiences.guard.extend({ +export const fullSignInExperienceGuard = SignInExperiences.guard.extend({ socialConnectors: connectorMetadataGuard .omit({ description: true, diff --git a/packages/schemas/src/types/ssr.ts b/packages/schemas/src/types/ssr.ts new file mode 100644 index 000000000000..64f1a67c11b8 --- /dev/null +++ b/packages/schemas/src/types/ssr.ts @@ -0,0 +1,28 @@ +import { type LocalePhrase } from '@logto/phrases-experience'; + +import { type FullSignInExperience } from './sign-in-experience.js'; + +/** + * The server-side rendering data type for **experience**. + */ +export type SsrData = { + signInExperience: { + appId?: string; + organizationId?: string; + data: FullSignInExperience; + }; + phrases: { + lng: string; + data: LocalePhrase; + }; +}; + +/** + * Variable placeholder for **experience** server-side rendering. The value should be replaced by + * the server. + * + * CAUTION: The value should be kept in sync with {@link file://./../../../experience/src/index.html}. + * + * @see {@link SsrData} for the data structure to replace the placeholders. + */ +export const ssrPlaceholder = '"__LOGTO_SSR__"'; From dbf9b2b04bedc010ad302922c2c3eeadceacfb5a Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 12 Jul 2024 20:53:42 +0800 Subject: [PATCH 009/135] chore(deps): upgrade packages --- packages/cli/package.json | 4 +- .../connector-alipay-native/package.json | 2 +- .../connector-alipay-web/package.json | 2 +- .../connector-aliyun-dm/package.json | 2 +- .../connector-aliyun-sms/package.json | 2 +- .../connectors/connector-apple/package.json | 4 +- .../connectors/connector-aws-ses/package.json | 2 +- .../connectors/connector-azuread/package.json | 2 +- .../connector-dingtalk-web/package.json | 2 +- .../connectors/connector-discord/package.json | 2 +- .../connector-facebook/package.json | 2 +- .../connector-feishu-web/package.json | 2 +- .../connectors/connector-github/package.json | 2 +- .../connectors/connector-google/package.json | 4 +- .../connectors/connector-kakao/package.json | 2 +- .../connector-logto-email/package.json | 2 +- .../connector-logto-sms/package.json | 2 +- .../connector-logto-social-demo/package.json | 2 +- .../connectors/connector-mailgun/package.json | 2 +- .../package.json | 2 +- .../connector-mock-email/package.json | 2 +- .../connector-mock-sms/package.json | 2 +- .../connector-mock-social/package.json | 2 +- .../connectors/connector-naver/package.json | 2 +- .../connectors/connector-oauth2/package.json | 4 +- .../connectors/connector-oidc/package.json | 4 +- .../connectors/connector-saml/package.json | 2 +- .../connector-sendgrid-email/package.json | 2 +- .../connectors/connector-smsaero/package.json | 2 +- .../connectors/connector-smtp/package.json | 2 +- .../connector-tencent-sms/package.json | 2 +- .../connector-twilio-sms/package.json | 2 +- .../connector-wechat-native/package.json | 2 +- .../connector-wechat-web/package.json | 2 +- .../connectors/connector-wecom/package.json | 2 +- packages/core/package.json | 22 +- packages/demo-app/package.json | 2 +- packages/experience/package.json | 2 +- packages/integration-tests/package.json | 4 +- packages/schemas/package.json | 2 +- packages/shared/package.json | 2 +- pnpm-lock.yaml | 505 ++++++++---------- 42 files changed, 267 insertions(+), 352 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index b8578a3eb0fa..572b0f8f5426 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,9 +51,9 @@ "@logto/shared": "workspace:^3.1.1", "@silverhand/essentials": "^2.9.1", "@silverhand/slonik": "31.0.0-beta.2", - "chalk": "^5.0.0", + "chalk": "^5.3.0", "decamelize": "^6.0.0", - "dotenv": "^16.0.0", + "dotenv": "^16.4.5", "got": "^14.0.0", "hpagent": "^1.2.0", "inquirer": "^9.0.0", diff --git a/packages/connectors/connector-alipay-native/package.json b/packages/connectors/connector-alipay-native/package.json index 7099bda0ce0b..c7b3a562b37f 100644 --- a/packages/connectors/connector-alipay-native/package.json +++ b/packages/connectors/connector-alipay-native/package.json @@ -9,7 +9,7 @@ "dayjs": "^1.10.5", "got": "^14.0.0", "iconv-lite": "^0.6.3", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/connectors/connector-alipay-web/package.json b/packages/connectors/connector-alipay-web/package.json index 10b5095b6b48..076666999a0c 100644 --- a/packages/connectors/connector-alipay-web/package.json +++ b/packages/connectors/connector-alipay-web/package.json @@ -8,7 +8,7 @@ "dayjs": "^1.10.5", "got": "^14.0.0", "iconv-lite": "^0.6.3", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/connectors/connector-aliyun-dm/package.json b/packages/connectors/connector-aliyun-dm/package.json index 59e41ddd8e81..1304c1ecd0df 100644 --- a/packages/connectors/connector-aliyun-dm/package.json +++ b/packages/connectors/connector-aliyun-dm/package.json @@ -6,7 +6,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-aliyun-sms/package.json b/packages/connectors/connector-aliyun-sms/package.json index 9739f43eb75e..408917679e9f 100644 --- a/packages/connectors/connector-aliyun-sms/package.json +++ b/packages/connectors/connector-aliyun-sms/package.json @@ -6,7 +6,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-apple/package.json b/packages/connectors/connector-apple/package.json index 2c11467106bb..4a71c704f13d 100644 --- a/packages/connectors/connector-apple/package.json +++ b/packages/connectors/connector-apple/package.json @@ -7,8 +7,8 @@ "@logto/shared": "workspace:^3.1.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "jose": "^5.0.0", - "snakecase-keys": "^8.0.0", + "jose": "^5.6.3", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-aws-ses/package.json b/packages/connectors/connector-aws-ses/package.json index 99f6582e3020..ee69073596a3 100644 --- a/packages/connectors/connector-aws-ses/package.json +++ b/packages/connectors/connector-aws-ses/package.json @@ -9,7 +9,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-azuread/package.json b/packages/connectors/connector-azuread/package.json index 773b2da57be4..cbb02ed66d76 100644 --- a/packages/connectors/connector-azuread/package.json +++ b/packages/connectors/connector-azuread/package.json @@ -8,7 +8,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-dingtalk-web/package.json b/packages/connectors/connector-dingtalk-web/package.json index d2815d24c294..4b0724e4055e 100644 --- a/packages/connectors/connector-dingtalk-web/package.json +++ b/packages/connectors/connector-dingtalk-web/package.json @@ -8,7 +8,7 @@ "dayjs": "^1.10.5", "got": "^14.0.0", "iconv-lite": "^0.6.3", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/connectors/connector-discord/package.json b/packages/connectors/connector-discord/package.json index eef708ea75b5..31f71d801d26 100644 --- a/packages/connectors/connector-discord/package.json +++ b/packages/connectors/connector-discord/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-facebook/package.json b/packages/connectors/connector-facebook/package.json index 787adb612c6e..6cced17415bc 100644 --- a/packages/connectors/connector-facebook/package.json +++ b/packages/connectors/connector-facebook/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-feishu-web/package.json b/packages/connectors/connector-feishu-web/package.json index b6db69458b6f..29439aacbb43 100644 --- a/packages/connectors/connector-feishu-web/package.json +++ b/packages/connectors/connector-feishu-web/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-github/package.json b/packages/connectors/connector-github/package.json index 1e9eeb2a4e95..e16618dde5fa 100644 --- a/packages/connectors/connector-github/package.json +++ b/packages/connectors/connector-github/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "ky": "^1.2.3", "query-string": "^9.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-google/package.json b/packages/connectors/connector-google/package.json index 6f5c8da1591c..0e1ef23eb64b 100644 --- a/packages/connectors/connector-google/package.json +++ b/packages/connectors/connector-google/package.json @@ -7,8 +7,8 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "jose": "^5.0.0", - "snakecase-keys": "^8.0.0", + "jose": "^5.6.3", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-kakao/package.json b/packages/connectors/connector-kakao/package.json index 900f095e6c51..5a30d189702b 100644 --- a/packages/connectors/connector-kakao/package.json +++ b/packages/connectors/connector-kakao/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-logto-email/package.json b/packages/connectors/connector-logto-email/package.json index d209481cfcce..925df9aa414f 100644 --- a/packages/connectors/connector-logto-email/package.json +++ b/packages/connectors/connector-logto-email/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-logto-sms/package.json b/packages/connectors/connector-logto-sms/package.json index 00fa94c4f508..bcec96f2bed5 100644 --- a/packages/connectors/connector-logto-sms/package.json +++ b/packages/connectors/connector-logto-sms/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-logto-social-demo/package.json b/packages/connectors/connector-logto-social-demo/package.json index 0e765182fbf8..05764d0dfae4 100644 --- a/packages/connectors/connector-logto-social-demo/package.json +++ b/packages/connectors/connector-logto-social-demo/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-mailgun/package.json b/packages/connectors/connector-mailgun/package.json index 07cacc5bdb80..d4d9cd80fe7c 100644 --- a/packages/connectors/connector-mailgun/package.json +++ b/packages/connectors/connector-mailgun/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-mock-email-alternative/package.json b/packages/connectors/connector-mock-email-alternative/package.json index 9132e8f85300..8eb74883a516 100644 --- a/packages/connectors/connector-mock-email-alternative/package.json +++ b/packages/connectors/connector-mock-email-alternative/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "scripts": { diff --git a/packages/connectors/connector-mock-email/package.json b/packages/connectors/connector-mock-email/package.json index f1b4024346a6..6320c05c110c 100644 --- a/packages/connectors/connector-mock-email/package.json +++ b/packages/connectors/connector-mock-email/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "scripts": { diff --git a/packages/connectors/connector-mock-sms/package.json b/packages/connectors/connector-mock-sms/package.json index 1ae46035a40d..d1ca73d047f2 100644 --- a/packages/connectors/connector-mock-sms/package.json +++ b/packages/connectors/connector-mock-sms/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "scripts": { diff --git a/packages/connectors/connector-mock-social/package.json b/packages/connectors/connector-mock-social/package.json index 902cecb18f76..205147858a52 100644 --- a/packages/connectors/connector-mock-social/package.json +++ b/packages/connectors/connector-mock-social/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "scripts": { diff --git a/packages/connectors/connector-naver/package.json b/packages/connectors/connector-naver/package.json index 22a3e4228165..565e5140aae3 100644 --- a/packages/connectors/connector-naver/package.json +++ b/packages/connectors/connector-naver/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-oauth2/package.json b/packages/connectors/connector-oauth2/package.json index 9fb22112f820..db885246476a 100644 --- a/packages/connectors/connector-oauth2/package.json +++ b/packages/connectors/connector-oauth2/package.json @@ -7,10 +7,10 @@ "@logto/connector-kit": "workspace:^4.0.0", "@logto/shared": "workspace:^3.1.1", "@silverhand/essentials": "^2.9.1", - "jose": "^5.0.0", + "jose": "^5.6.3", "ky": "^1.2.3", "query-string": "^9.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-oidc/package.json b/packages/connectors/connector-oidc/package.json index bffa1bbc5091..85c97e3f23e6 100644 --- a/packages/connectors/connector-oidc/package.json +++ b/packages/connectors/connector-oidc/package.json @@ -7,10 +7,10 @@ "@logto/connector-oauth": "workspace:^1.3.1", "@logto/shared": "workspace:^3.1.1", "@silverhand/essentials": "^2.9.1", - "jose": "^5.0.0", + "jose": "^5.6.3", "ky": "^1.2.3", "nanoid": "^5.0.1", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-saml/package.json b/packages/connectors/connector-saml/package.json index 7e84b900cf4a..ffd5649c899c 100644 --- a/packages/connectors/connector-saml/package.json +++ b/packages/connectors/connector-saml/package.json @@ -9,7 +9,7 @@ "fast-xml-parser": "^4.3.6", "got": "^14.0.0", "samlify": "2.8.11", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-sendgrid-email/package.json b/packages/connectors/connector-sendgrid-email/package.json index 58231c9eb008..9aa6191625d3 100644 --- a/packages/connectors/connector-sendgrid-email/package.json +++ b/packages/connectors/connector-sendgrid-email/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-smsaero/package.json b/packages/connectors/connector-smsaero/package.json index 306eb38dbb3e..5756aa8836af 100644 --- a/packages/connectors/connector-smsaero/package.json +++ b/packages/connectors/connector-smsaero/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-smtp/package.json b/packages/connectors/connector-smtp/package.json index 8fbd5f468691..b626bf815bbd 100644 --- a/packages/connectors/connector-smtp/package.json +++ b/packages/connectors/connector-smtp/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "nodemailer": "^6.9.9", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/connectors/connector-tencent-sms/package.json b/packages/connectors/connector-tencent-sms/package.json index a0da5a6bcae9..49678b1a59fb 100644 --- a/packages/connectors/connector-tencent-sms/package.json +++ b/packages/connectors/connector-tencent-sms/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-twilio-sms/package.json b/packages/connectors/connector-twilio-sms/package.json index 976f30702497..3964f6ac8630 100644 --- a/packages/connectors/connector-twilio-sms/package.json +++ b/packages/connectors/connector-twilio-sms/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-wechat-native/package.json b/packages/connectors/connector-wechat-native/package.json index 9a68f2dc4626..77b0e1f98323 100644 --- a/packages/connectors/connector-wechat-native/package.json +++ b/packages/connectors/connector-wechat-native/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-wechat-web/package.json b/packages/connectors/connector-wechat-web/package.json index 07d99ad9280d..6a7908efa566 100644 --- a/packages/connectors/connector-wechat-web/package.json +++ b/packages/connectors/connector-wechat-web/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-wecom/package.json b/packages/connectors/connector-wecom/package.json index b06794e294c2..ba26fbc621e9 100644 --- a/packages/connectors/connector-wecom/package.json +++ b/packages/connectors/connector-wecom/package.json @@ -7,7 +7,7 @@ "@logto/connector-kit": "workspace:^4.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "main": "./lib/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index ad3556771195..05cbc87fffa4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -48,30 +48,30 @@ "@simplewebauthn/server": "^10.0.0", "@withtyped/client": "^0.8.7", "camelcase": "^8.0.0", - "camelcase-keys": "^9.0.0", - "chalk": "^5.0.0", + "camelcase-keys": "^9.1.3", + "chalk": "^5.3.0", "clean-deep": "^3.4.0", "date-fns": "^2.29.3", "decamelize": "^6.0.0", "deepmerge": "^4.2.2", - "dotenv": "^16.0.0", + "dotenv": "^16.4.5", "etag": "^1.8.1", "fast-xml-parser": "^4.3.6", "find-up": "^7.0.0", "got": "^14.0.0", - "hash-wasm": "^4.9.0", - "helmet": "^7.0.0", + "hash-wasm": "^4.11.0", + "helmet": "^7.1.0", "i18next": "^22.4.15", "iconv-lite": "0.6.3", - "jose": "^5.0.0", - "koa": "^2.13.1", + "jose": "^5.6.3", + "koa": "^2.15.3", "koa-body": "^6.0.1", "koa-compose": "^4.1.0", - "koa-compress": "^5.1.0", + "koa-compress": "^5.1.1", "koa-logger": "^3.2.1", "koa-mount": "^4.0.0", "koa-proxies": "^0.12.4", - "koa-router": "^12.0.0", + "koa-router": "^12.0.1", "koa-send": "^5.0.1", "ky": "^1.2.3", "lru-cache": "^11.0.0", @@ -89,7 +89,7 @@ "samlify": "2.8.11", "semver": "^7.3.8", "snake-case": "^4.0.0", - "snakecase-keys": "^8.0.0", + "snakecase-keys": "^8.0.1", "zod": "^3.22.4" }, "devDependencies": { @@ -99,7 +99,7 @@ "@types/debug": "^4.1.7", "@types/etag": "^1.8.1", "@types/jest": "^29.4.0", - "@types/koa": "^2.13.3", + "@types/koa": "^2.15.0", "@types/koa-compose": "^3.2.5", "@types/koa-compress": "^4.0.3", "@types/koa-logger": "^3.1.1", diff --git a/packages/demo-app/package.json b/packages/demo-app/package.json index d5478a1dbdb6..7f989910854e 100644 --- a/packages/demo-app/package.json +++ b/packages/demo-app/package.json @@ -37,7 +37,7 @@ "eslint": "^8.56.0", "i18next": "^22.4.15", "i18next-browser-languagedetector": "^8.0.0", - "jose": "^5.0.0", + "jose": "^5.6.3", "lint-staged": "^15.0.0", "parcel": "2.9.3", "postcss": "^8.4.31", diff --git a/packages/experience/package.json b/packages/experience/package.json index 0265214d98aa..72481759defb 100644 --- a/packages/experience/package.json +++ b/packages/experience/package.json @@ -52,7 +52,7 @@ "@types/react-helmet": "^6.1.6", "@types/react-modal": "^3.13.1", "@types/react-router-dom": "^5.3.2", - "camelcase-keys": "^9.0.0", + "camelcase-keys": "^9.1.3", "classnames": "^2.3.1", "color": "^4.2.3", "core-js": "^3.34.0", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index d238a5de8cf3..984fb7eed068 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -35,13 +35,13 @@ "@silverhand/ts-config": "6.0.0", "@types/jest": "^29.4.0", "@types/node": "^20.9.5", - "dotenv": "^16.0.0", + "dotenv": "^16.4.5", "eslint": "^8.56.0", "expect-puppeteer": "^10.0.0", "jest": "^29.7.0", "jest-matcher-specific-error": "^1.0.0", "jest-puppeteer": "^10.0.1", - "jose": "^5.0.0", + "jose": "^5.6.3", "ky": "^1.2.3", "openapi-schema-validator": "^12.1.3", "openapi-types": "^12.1.3", diff --git a/packages/schemas/package.json b/packages/schemas/package.json index e7da8c566f34..173a793f88e4 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -48,7 +48,7 @@ "@types/pluralize": "^0.0.33", "@vitest/coverage-v8": "^1.4.0", "camelcase": "^8.0.0", - "chalk": "^5.0.0", + "chalk": "^5.3.0", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "pluralize": "^8.0.0", diff --git a/packages/shared/package.json b/packages/shared/package.json index 5f80353075c2..98fec1fb9351 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -60,7 +60,7 @@ "prettier": "@silverhand/eslint-config/.prettierrc", "dependencies": { "@silverhand/essentials": "^2.9.1", - "chalk": "^5.0.0", + "chalk": "^5.3.0", "find-up": "^7.0.0", "libphonenumber-js": "^1.9.49", "nanoid": "^5.0.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 095450077188..9de320e453e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,14 +110,14 @@ importers: specifier: 31.0.0-beta.2 version: 31.0.0-beta.2 chalk: - specifier: ^5.0.0 - version: 5.1.2 + specifier: ^5.3.0 + version: 5.3.0 decamelize: specifier: ^6.0.0 version: 6.0.0 dotenv: - specifier: ^16.0.0 - version: 16.0.0 + specifier: ^16.4.5 + version: 16.4.5 got: specifier: ^14.0.0 version: 14.0.0 @@ -228,8 +228,8 @@ importers: specifier: ^0.6.3 version: 0.6.3 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -310,8 +310,8 @@ importers: specifier: ^0.6.3 version: 0.6.3 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -386,8 +386,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -459,8 +459,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -535,11 +535,11 @@ importers: specifier: ^14.0.0 version: 14.0.0 jose: - specifier: ^5.0.0 - version: 5.0.1 + specifier: ^5.6.3 + version: 5.6.3 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -617,8 +617,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -693,8 +693,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -772,8 +772,8 @@ importers: specifier: ^0.6.3 version: 0.6.3 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -848,8 +848,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -921,8 +921,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -994,8 +994,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1070,8 +1070,8 @@ importers: specifier: ^9.0.0 version: 9.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1143,11 +1143,11 @@ importers: specifier: ^14.0.0 version: 14.0.0 jose: - specifier: ^5.0.0 - version: 5.2.4 + specifier: ^5.6.3 + version: 5.6.3 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1292,8 +1292,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1365,8 +1365,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1441,8 +1441,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1514,8 +1514,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1587,8 +1587,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1660,8 +1660,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1733,8 +1733,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1806,8 +1806,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1879,8 +1879,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1952,8 +1952,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2025,8 +2025,8 @@ importers: specifier: ^2.9.1 version: 2.9.1 jose: - specifier: ^5.0.0 - version: 5.2.2 + specifier: ^5.6.3 + version: 5.6.3 ky: specifier: ^1.2.3 version: 1.2.3 @@ -2034,8 +2034,8 @@ importers: specifier: ^9.0.0 version: 9.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2110,8 +2110,8 @@ importers: specifier: ^2.9.1 version: 2.9.1 jose: - specifier: ^5.0.0 - version: 5.0.1 + specifier: ^5.6.3 + version: 5.6.3 ky: specifier: ^1.2.3 version: 1.2.3 @@ -2119,8 +2119,8 @@ importers: specifier: ^5.0.1 version: 5.0.1 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2198,8 +2198,8 @@ importers: specifier: 2.8.11 version: 2.8.11 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2271,8 +2271,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2344,8 +2344,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2420,8 +2420,8 @@ importers: specifier: ^6.9.9 version: 6.9.9 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2496,8 +2496,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2569,8 +2569,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2642,8 +2642,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2715,8 +2715,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -2788,8 +2788,8 @@ importers: specifier: ^14.0.0 version: 14.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -3227,11 +3227,11 @@ importers: specifier: ^8.0.0 version: 8.0.0 camelcase-keys: - specifier: ^9.0.0 - version: 9.0.0 + specifier: ^9.1.3 + version: 9.1.3 chalk: - specifier: ^5.0.0 - version: 5.1.2 + specifier: ^5.3.0 + version: 5.3.0 clean-deep: specifier: ^3.4.0 version: 3.4.0 @@ -3245,8 +3245,8 @@ importers: specifier: ^4.2.2 version: 4.2.2 dotenv: - specifier: ^16.0.0 - version: 16.0.0 + specifier: ^16.4.5 + version: 16.4.5 etag: specifier: ^1.8.1 version: 1.8.1 @@ -3260,11 +3260,11 @@ importers: specifier: ^14.0.0 version: 14.0.0 hash-wasm: - specifier: ^4.9.0 - version: 4.9.0 + specifier: ^4.11.0 + version: 4.11.0 helmet: - specifier: ^7.0.0 - version: 7.0.0 + specifier: ^7.1.0 + version: 7.1.0 i18next: specifier: ^22.4.15 version: 22.4.15 @@ -3272,11 +3272,11 @@ importers: specifier: 0.6.3 version: 0.6.3 jose: - specifier: ^5.0.0 - version: 5.0.1 + specifier: ^5.6.3 + version: 5.6.3 koa: - specifier: ^2.13.1 - version: 2.13.4 + specifier: ^2.15.3 + version: 2.15.3 koa-body: specifier: ^6.0.1 version: 6.0.1 @@ -3284,8 +3284,8 @@ importers: specifier: ^4.1.0 version: 4.1.0 koa-compress: - specifier: ^5.1.0 - version: 5.1.0 + specifier: ^5.1.1 + version: 5.1.1 koa-logger: specifier: ^3.2.1 version: 3.2.1 @@ -3294,10 +3294,10 @@ importers: version: 4.0.0 koa-proxies: specifier: ^0.12.4 - version: 0.12.4(koa@2.13.4) + version: 0.12.4(koa@2.15.3) koa-router: - specifier: ^12.0.0 - version: 12.0.0 + specifier: ^12.0.1 + version: 12.0.1 koa-send: specifier: ^5.0.1 version: 5.0.1 @@ -3350,8 +3350,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 snakecase-keys: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -3375,8 +3375,8 @@ importers: specifier: ^29.4.0 version: 29.4.0 '@types/koa': - specifier: ^2.13.3 - version: 2.13.4 + specifier: ^2.15.0 + version: 2.15.0 '@types/koa-compose': specifier: ^3.2.5 version: 3.2.5 @@ -3421,7 +3421,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + version: 29.7.0(@types/node@20.10.4) jest-matcher-specific-error: specifier: ^1.0.0 version: 1.0.0 @@ -3516,8 +3516,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 jose: - specifier: ^5.0.0 - version: 5.2.4 + specifier: ^5.6.3 + version: 5.6.3 lint-staged: specifier: ^15.0.0 version: 15.0.2 @@ -3648,8 +3648,8 @@ importers: specifier: ^5.3.2 version: 5.3.2 camelcase-keys: - specifier: ^9.0.0 - version: 9.0.0 + specifier: ^9.1.3 + version: 9.1.3 classnames: specifier: ^2.3.1 version: 2.3.1 @@ -3676,7 +3676,7 @@ importers: version: 3.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) + version: 29.7.0(@types/node@20.12.7) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -3685,7 +3685,7 @@ importers: version: 2.0.0 jest-transformer-svg: specifier: ^2.0.0 - version: 2.0.0(jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)))(react@18.2.0) + version: 2.0.0(jest@29.7.0(@types/node@20.12.7))(react@18.2.0) js-base64: specifier: ^3.7.5 version: 3.7.5 @@ -3814,8 +3814,8 @@ importers: specifier: ^20.9.5 version: 20.10.4 dotenv: - specifier: ^16.0.0 - version: 16.0.0 + specifier: ^16.4.5 + version: 16.4.5 eslint: specifier: ^8.56.0 version: 8.57.0 @@ -3824,7 +3824,7 @@ importers: version: 10.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + version: 29.7.0(@types/node@20.10.4) jest-matcher-specific-error: specifier: ^1.0.0 version: 1.0.0 @@ -3832,8 +3832,8 @@ importers: specifier: ^10.0.1 version: 10.0.1(puppeteer@22.6.5(typescript@5.3.3))(typescript@5.3.3) jose: - specifier: ^5.0.0 - version: 5.0.1 + specifier: ^5.6.3 + version: 5.6.3 ky: specifier: ^1.2.3 version: 1.2.3 @@ -3982,8 +3982,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 chalk: - specifier: ^5.0.0 - version: 5.1.2 + specifier: ^5.3.0 + version: 5.3.0 eslint: specifier: ^8.56.0 version: 8.57.0 @@ -4012,8 +4012,8 @@ importers: specifier: ^2.9.1 version: 2.9.1 chalk: - specifier: ^5.0.0 - version: 5.1.2 + specifier: ^5.3.0 + version: 5.3.0 find-up: specifier: ^7.0.0 version: 7.0.0 @@ -6570,9 +6570,6 @@ packages: '@types/koa-send@4.1.3': resolution: {integrity: sha512-daaTqPZlgjIJycSTNjKpHYuKhXYP30atFc1pBcy6HHqB9+vcymDgYTguPdx9tO4HMOqNyz6bz/zqpxt5eLR+VA==} - '@types/koa@2.13.4': - resolution: {integrity: sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==} - '@types/koa@2.15.0': resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} @@ -7285,8 +7282,8 @@ packages: resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} engines: {node: '>=12'} - camelcase-keys@9.0.0: - resolution: {integrity: sha512-GdZ92DNXdcfFB/5Kq4O82EL6UW5neiRBhfNP5M3mGw7CX2sPDbVA04ZPLsqbp7oMi2l3m2I0AZ/kFP5Nk5kopA==} + camelcase-keys@9.1.3: + resolution: {integrity: sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==} engines: {node: '>=16'} camelcase@5.3.1: @@ -7319,10 +7316,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.1.2: - resolution: {integrity: sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chalk@5.3.0: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -8124,8 +8117,8 @@ packages: dotenv-expand@5.1.0: resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} - dotenv@16.0.0: - resolution: {integrity: sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} dotenv@7.0.0: @@ -9035,8 +9028,8 @@ packages: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} - hash-wasm@4.9.0: - resolution: {integrity: sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==} + hash-wasm@4.11.0: + resolution: {integrity: sha512-HVusNXlVqHe0fzIzdQOGolnFN6mX/fqcrSAOcTBXdvzrXVHwTz11vXeKRmkR5gTuwVpvHZEIyKoePDvuAR+XwQ==} hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} @@ -9060,8 +9053,8 @@ packages: hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - helmet@7.0.0: - resolution: {integrity: sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==} + helmet@7.1.0: + resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==} engines: {node: '>=16.0.0'} hexoid@1.0.0: @@ -9818,14 +9811,8 @@ packages: joi@17.12.2: resolution: {integrity: sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==} - jose@5.0.1: - resolution: {integrity: sha512-gRVzy7s3RRdGbXmcTdlOswJOjhwPLx1ijIgAqLY6ktzFpOJxxYn4l0fC2vHaHHi4YBX/5FOL3aY+6W0cvQgpug==} - - jose@5.2.2: - resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==} - - jose@5.2.4: - resolution: {integrity: sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==} + jose@5.6.3: + resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} js-base64@3.7.5: resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} @@ -9976,9 +9963,9 @@ packages: koa-compose@4.1.0: resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} - koa-compress@5.1.0: - resolution: {integrity: sha512-G3Ppo9jrUwlchp6qdoRgQNMiGZtM0TAHkxRZQ7EoVvIG8E47J4nAsMJxXHAUQ+0oc7t0MDxSdONWTFcbzX7/Bg==} - engines: {node: '>= 8.0.0'} + koa-compress@5.1.1: + resolution: {integrity: sha512-UgMIN7ZoEP2DuoSQmD6CYvFSLt0NReGlc2qSY4bO4Oq0L56OiD9pDG41Kj/zFmVY/A3Wvmn4BqKcfq5H30LGIg==} + engines: {node: '>= 12'} koa-convert@2.0.0: resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} @@ -10000,10 +9987,9 @@ packages: peerDependencies: koa: '>=2' - koa-router@12.0.0: - resolution: {integrity: sha512-zGrdiXygGYW8WvrzeGsHZvKnHs4DzyGoqJ9a8iHlRkiwuEAOAPyI27//OlhoWdgFAEIM3qbUgr0KCuRaP/TCag==} + koa-router@12.0.1: + resolution: {integrity: sha512-gaDdj3GtzoLoeosacd50kBBTnnh3B9AYxDThQUo4sfUyXdOhY6ku1qyZKW88tQCRgc3Sw6ChXYXWZwwgjOxE0w==} engines: {node: '>= 12'} - deprecated: '**IMPORTANT 10x+ PERFORMANCE UPGRADE**: Please upgrade to v12.0.1+ as we have fixed an issue with debuglog causing 10x slower router benchmark performance, see https://github.com/koajs/router/pull/173' koa-send@5.0.1: resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} @@ -12159,8 +12145,8 @@ packages: resolution: {integrity: sha512-slvG6efKZ3GYUUZdhPOq/lLIqutwQ4TdPViD1VKqsbf0u76U/aPRswPKjOaAS9T7fAPmRmXuN6C/nM0xsMaFLQ==} deprecated: Use `change-case` - snakecase-keys@8.0.0: - resolution: {integrity: sha512-qS7XvESuFxskrmD8/O9tOMdLdV9YTuPfNLdRJIvNJKNEZtH9XVPwmZioROwl4TsVDbOFqlQ/o7pURuF8MnjYsg==} + snakecase-keys@8.0.1: + resolution: {integrity: sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==} engines: {node: '>=18'} socks-proxy-agent@8.0.2: @@ -12707,10 +12693,6 @@ packages: resolution: {integrity: sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==} engines: {node: '>=16'} - type-fest@4.2.0: - resolution: {integrity: sha512-5zknd7Dss75pMSED270A1RQS3KloqRJA9XbXLe0eCxyw7xXFb3rd+9B0UQ/0E+LQT6lnrLviEolYORlRWamn4w==} - engines: {node: '>=16'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -14641,41 +14623,6 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.12.7 - 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.7.0 - jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.5 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - '@jest/create-cache-key-function@27.5.1': dependencies: '@jest/types': 27.5.1 @@ -14933,7 +14880,7 @@ snapshots: '@logto/js': 4.1.4 '@silverhand/essentials': 2.9.1 camelcase-keys: 7.0.2 - jose: 5.2.4 + jose: 5.6.3 '@logto/cloud@0.2.5-3046fa6(zod@3.22.4)': dependencies: @@ -16004,10 +15951,10 @@ snapshots: eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-config-xo: 0.44.0(eslint@8.57.0) eslint-config-xo-typescript: 4.0.0(@typescript-eslint/eslint-plugin@7.7.0(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-consistent-default-export-name: 0.0.15 eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-n: 17.2.1(eslint@8.57.0) eslint-plugin-no-use-extend-native: 0.5.0 eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.0.0) @@ -16781,35 +16728,24 @@ snapshots: '@types/koa-compose@3.2.5': dependencies: - '@types/koa': 2.13.4 + '@types/koa': 2.15.0 '@types/koa-compress@4.0.3': dependencies: - '@types/koa': 2.13.4 + '@types/koa': 2.15.0 '@types/node': 20.12.7 '@types/koa-logger@3.1.2': dependencies: - '@types/koa': 2.13.4 + '@types/koa': 2.15.0 '@types/koa-mount@4.0.1': dependencies: - '@types/koa': 2.13.4 + '@types/koa': 2.15.0 '@types/koa-send@4.1.3': dependencies: - '@types/koa': 2.13.4 - - '@types/koa@2.13.4': - dependencies: - '@types/accepts': 1.3.5 - '@types/content-disposition': 0.5.4 - '@types/cookies': 0.7.7 - '@types/http-assert': 1.5.3 - '@types/http-errors': 1.8.2 - '@types/keygrip': 1.0.2 - '@types/koa-compose': 3.2.5 - '@types/node': 20.12.7 + '@types/koa': 2.15.0 '@types/koa@2.15.0': dependencies: @@ -16824,7 +16760,7 @@ snapshots: '@types/koa__cors@5.0.0': dependencies: - '@types/koa': 2.13.4 + '@types/koa': 2.15.0 '@types/mdast@3.0.15': dependencies: @@ -16871,7 +16807,7 @@ snapshots: '@types/oidc-provider@8.4.4': dependencies: - '@types/koa': 2.13.4 + '@types/koa': 2.15.0 '@types/node': 20.12.7 '@types/parse-json@4.0.0': {} @@ -17722,12 +17658,12 @@ snapshots: quick-lru: 5.1.1 type-fest: 1.4.0 - camelcase-keys@9.0.0: + camelcase-keys@9.1.3: dependencies: camelcase: 8.0.0 map-obj: 5.0.0 quick-lru: 6.1.1 - type-fest: 4.2.0 + type-fest: 4.15.0 camelcase@5.3.1: {} @@ -17760,8 +17696,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.1.2: {} - chalk@5.3.0: {} char-regex@1.0.2: {} @@ -18036,13 +17970,13 @@ snapshots: dependencies: lodash.get: 4.4.2 - create-jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): + create-jest@29.7.0(@types/node@20.10.4): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + jest-config: 29.7.0(@types/node@20.10.4) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -18567,7 +18501,7 @@ snapshots: dotenv-expand@5.1.0: {} - dotenv@16.0.0: {} + dotenv@16.4.5: {} dotenv@7.0.0: {} @@ -18848,13 +18782,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.3 is-core-module: 2.13.1 @@ -18865,14 +18799,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -18894,7 +18828,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -18904,7 +18838,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -19729,7 +19663,7 @@ snapshots: dependencies: function-bind: 1.1.1 - hash-wasm@4.9.0: {} + hash-wasm@4.11.0: {} hasown@2.0.2: dependencies: @@ -19795,7 +19729,7 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 - helmet@7.0.0: {} + helmet@7.1.0: {} hexoid@1.0.0: {} @@ -20026,7 +19960,7 @@ snapshots: inquirer@9.1.4: dependencies: ansi-escapes: 6.0.0 - chalk: 5.1.2 + chalk: 5.3.0 cli-cursor: 4.0.0 cli-width: 4.0.0 external-editor: 3.1.0 @@ -20379,16 +20313,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): + jest-cli@29.7.0(@types/node@20.10.4): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + create-jest: 29.7.0(@types/node@20.10.4) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + jest-config: 29.7.0(@types/node@20.10.4) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -20398,7 +20332,7 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): + jest-cli@29.7.0(@types/node@20.12.7): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) '@jest/test-result': 29.7.0 @@ -20417,38 +20351,26 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): + jest-cli@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): dependencies: - '@babel/core': 7.24.4 - '@jest/test-sequencer': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) + '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.24.4) 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.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 + create-jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.10.4 - ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3) + yargs: 17.7.2 transitivePeerDependencies: + - '@types/node' - babel-plugin-macros - supports-color + - ts-node - jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): + jest-config@29.7.0(@types/node@20.10.4): dependencies: '@babel/core': 7.24.4 '@jest/test-sequencer': 29.7.0 @@ -20473,13 +20395,12 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.12.7 - ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) + '@types/node': 20.10.4 transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): + jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): dependencies: '@babel/core': 7.24.4 '@jest/test-sequencer': 29.7.0 @@ -20505,7 +20426,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.12.7 - ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3) + ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -20766,6 +20687,11 @@ snapshots: jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) react: 18.2.0 + jest-transformer-svg@2.0.0(jest@29.7.0(@types/node@20.12.7))(react@18.2.0): + dependencies: + jest: 29.7.0(@types/node@20.12.7) + react: 18.2.0 + jest-util@29.5.0: dependencies: '@jest/types': 29.6.3 @@ -20818,12 +20744,24 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): + jest@29.7.0(@types/node@20.10.4): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@20.10.4) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest@29.7.0(@types/node@20.12.7): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + jest-cli: 29.7.0(@types/node@20.12.7) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -20852,11 +20790,7 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@5.0.1: {} - - jose@5.2.2: {} - - jose@5.2.4: {} + jose@5.6.3: {} js-base64@3.7.5: {} @@ -21030,13 +20964,12 @@ snapshots: koa-compose@4.1.0: {} - koa-compress@5.1.0: + koa-compress@5.1.1: dependencies: bytes: 3.1.2 compressible: 2.0.18 http-errors: 1.8.1 koa-is-json: 1.0.0 - statuses: 2.0.1 koa-convert@2.0.0: dependencies: @@ -21059,21 +20992,24 @@ snapshots: transitivePeerDependencies: - supports-color - koa-proxies@0.12.4(koa@2.13.4): + koa-proxies@0.12.4(koa@2.15.3): dependencies: http-proxy: 1.18.1 - koa: 2.13.4 + koa: 2.15.3 path-match: 1.2.4 uuid: 8.3.2 transitivePeerDependencies: - debug - koa-router@12.0.0: + koa-router@12.0.1: dependencies: + debug: 4.3.4 http-errors: 2.0.0 koa-compose: 4.1.0 methods: 1.1.2 path-to-regexp: 6.2.1 + transitivePeerDependencies: + - supports-color koa-send@5.0.1: dependencies: @@ -22364,7 +22300,7 @@ snapshots: debug: 4.3.4 eta: 3.4.0 got: 13.0.0 - jose: 5.2.4 + jose: 5.6.3 jsesc: 3.0.2 koa: 2.15.3 nanoid: 5.0.7 @@ -23796,7 +23732,7 @@ snapshots: dependencies: no-case: 4.0.0 - snakecase-keys@8.0.0: + snakecase-keys@8.0.1: dependencies: map-obj: 4.3.0 snake-case: 3.0.4 @@ -24334,25 +24270,6 @@ snapshots: optionalDependencies: '@swc/core': 1.3.52(@swc/helpers@0.5.1) - ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.9 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.10.4 - acorn: 8.10.0 - acorn-walk: 8.2.0 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.3.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optional: true - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -24408,8 +24325,6 @@ snapshots: type-fest@4.15.0: {} - type-fest@4.2.0: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 From e3109af0264b5f08f128f2837db6dcefa405734a Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 12 Jul 2024 20:56:01 +0800 Subject: [PATCH 010/135] chore(deps): upgrade zod --- packages/cli/package.json | 2 +- .../connector-alipay-native/package.json | 2 +- .../connector-alipay-web/package.json | 2 +- .../connector-aliyun-dm/package.json | 2 +- .../connector-aliyun-sms/package.json | 2 +- .../connectors/connector-apple/package.json | 2 +- .../connectors/connector-aws-ses/package.json | 2 +- .../connectors/connector-azuread/package.json | 2 +- .../connector-dingtalk-web/package.json | 2 +- .../connectors/connector-discord/package.json | 2 +- .../connector-facebook/package.json | 2 +- .../connector-feishu-web/package.json | 2 +- .../connectors/connector-github/package.json | 2 +- .../connectors/connector-google/package.json | 2 +- .../connector-huggingface/package.json | 2 +- .../connectors/connector-kakao/package.json | 2 +- .../connector-logto-email/package.json | 2 +- .../connector-logto-sms/package.json | 2 +- .../connector-logto-social-demo/package.json | 2 +- .../connectors/connector-mailgun/package.json | 2 +- .../package.json | 2 +- .../connector-mock-email/package.json | 2 +- .../connector-mock-sms/package.json | 2 +- .../connector-mock-social/package.json | 2 +- .../connectors/connector-naver/package.json | 2 +- .../connectors/connector-oauth2/package.json | 2 +- .../connectors/connector-oidc/package.json | 2 +- .../connectors/connector-saml/package.json | 2 +- .../connector-sendgrid-email/package.json | 2 +- .../connectors/connector-smsaero/package.json | 2 +- .../connectors/connector-smtp/package.json | 2 +- .../connector-tencent-sms/package.json | 2 +- .../connector-twilio-sms/package.json | 2 +- .../connector-wechat-native/package.json | 2 +- .../connector-wechat-web/package.json | 2 +- .../connectors/connector-wecom/package.json | 2 +- packages/console/package.json | 2 +- packages/core/package.json | 2 +- packages/demo-app/package.json | 2 +- packages/experience/package.json | 2 +- packages/integration-tests/package.json | 2 +- packages/phrases-experience/package.json | 2 +- packages/phrases/package.json | 2 +- packages/schemas/package.json | 2 +- packages/toolkit/connector-kit/package.json | 2 +- packages/toolkit/core-kit/package.json | 2 +- packages/toolkit/language-kit/package.json | 2 +- pnpm-lock.yaml | 253 +++++++++--------- 48 files changed, 176 insertions(+), 171 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 572b0f8f5426..23ce094254ab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,7 +68,7 @@ "tar": "^7.0.0", "typescript": "^5.3.3", "yargs": "^17.6.0", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@silverhand/eslint-config": "6.0.1", diff --git a/packages/connectors/connector-alipay-native/package.json b/packages/connectors/connector-alipay-native/package.json index c7b3a562b37f..b988d62f72b7 100644 --- a/packages/connectors/connector-alipay-native/package.json +++ b/packages/connectors/connector-alipay-native/package.json @@ -10,7 +10,7 @@ "got": "^14.0.0", "iconv-lite": "^0.6.3", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@rollup/plugin-commonjs": "^26.0.0", diff --git a/packages/connectors/connector-alipay-web/package.json b/packages/connectors/connector-alipay-web/package.json index 076666999a0c..5e563347dda1 100644 --- a/packages/connectors/connector-alipay-web/package.json +++ b/packages/connectors/connector-alipay-web/package.json @@ -9,7 +9,7 @@ "got": "^14.0.0", "iconv-lite": "^0.6.3", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@rollup/plugin-commonjs": "^26.0.0", diff --git a/packages/connectors/connector-aliyun-dm/package.json b/packages/connectors/connector-aliyun-dm/package.json index 1304c1ecd0df..2c99f154cf02 100644 --- a/packages/connectors/connector-aliyun-dm/package.json +++ b/packages/connectors/connector-aliyun-dm/package.json @@ -7,7 +7,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-aliyun-sms/package.json b/packages/connectors/connector-aliyun-sms/package.json index 408917679e9f..eafcec10a69f 100644 --- a/packages/connectors/connector-aliyun-sms/package.json +++ b/packages/connectors/connector-aliyun-sms/package.json @@ -7,7 +7,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-apple/package.json b/packages/connectors/connector-apple/package.json index 4a71c704f13d..3a14101d7be2 100644 --- a/packages/connectors/connector-apple/package.json +++ b/packages/connectors/connector-apple/package.json @@ -9,7 +9,7 @@ "got": "^14.0.0", "jose": "^5.6.3", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-aws-ses/package.json b/packages/connectors/connector-aws-ses/package.json index ee69073596a3..cf9af03a2ab6 100644 --- a/packages/connectors/connector-aws-ses/package.json +++ b/packages/connectors/connector-aws-ses/package.json @@ -10,7 +10,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-azuread/package.json b/packages/connectors/connector-azuread/package.json index cbb02ed66d76..473ecb02f606 100644 --- a/packages/connectors/connector-azuread/package.json +++ b/packages/connectors/connector-azuread/package.json @@ -9,7 +9,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-dingtalk-web/package.json b/packages/connectors/connector-dingtalk-web/package.json index 4b0724e4055e..18649dcf635d 100644 --- a/packages/connectors/connector-dingtalk-web/package.json +++ b/packages/connectors/connector-dingtalk-web/package.json @@ -9,7 +9,7 @@ "got": "^14.0.0", "iconv-lite": "^0.6.3", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@rollup/plugin-commonjs": "^26.0.0", diff --git a/packages/connectors/connector-discord/package.json b/packages/connectors/connector-discord/package.json index 31f71d801d26..86db3fc3486a 100644 --- a/packages/connectors/connector-discord/package.json +++ b/packages/connectors/connector-discord/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-facebook/package.json b/packages/connectors/connector-facebook/package.json index 6cced17415bc..ce27ef1646c6 100644 --- a/packages/connectors/connector-facebook/package.json +++ b/packages/connectors/connector-facebook/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-feishu-web/package.json b/packages/connectors/connector-feishu-web/package.json index 29439aacbb43..0bde995cbdc8 100644 --- a/packages/connectors/connector-feishu-web/package.json +++ b/packages/connectors/connector-feishu-web/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-github/package.json b/packages/connectors/connector-github/package.json index e16618dde5fa..c5693f5f83b6 100644 --- a/packages/connectors/connector-github/package.json +++ b/packages/connectors/connector-github/package.json @@ -9,7 +9,7 @@ "ky": "^1.2.3", "query-string": "^9.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-google/package.json b/packages/connectors/connector-google/package.json index 0e1ef23eb64b..014771a3c7b4 100644 --- a/packages/connectors/connector-google/package.json +++ b/packages/connectors/connector-google/package.json @@ -9,7 +9,7 @@ "got": "^14.0.0", "jose": "^5.6.3", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-huggingface/package.json b/packages/connectors/connector-huggingface/package.json index b87196f02f6b..2abcbe5ff5bb 100644 --- a/packages/connectors/connector-huggingface/package.json +++ b/packages/connectors/connector-huggingface/package.json @@ -8,7 +8,7 @@ "@logto/connector-oauth": "workspace:^1.3.1", "@silverhand/essentials": "^2.9.1", "ky": "^1.2.3", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-kakao/package.json b/packages/connectors/connector-kakao/package.json index 5a30d189702b..117f9f957826 100644 --- a/packages/connectors/connector-kakao/package.json +++ b/packages/connectors/connector-kakao/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-logto-email/package.json b/packages/connectors/connector-logto-email/package.json index 925df9aa414f..880eacea9483 100644 --- a/packages/connectors/connector-logto-email/package.json +++ b/packages/connectors/connector-logto-email/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-logto-sms/package.json b/packages/connectors/connector-logto-sms/package.json index bcec96f2bed5..bc84b6c8ebcf 100644 --- a/packages/connectors/connector-logto-sms/package.json +++ b/packages/connectors/connector-logto-sms/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-logto-social-demo/package.json b/packages/connectors/connector-logto-social-demo/package.json index 05764d0dfae4..b730cbe7eca2 100644 --- a/packages/connectors/connector-logto-social-demo/package.json +++ b/packages/connectors/connector-logto-social-demo/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-mailgun/package.json b/packages/connectors/connector-mailgun/package.json index d4d9cd80fe7c..405a50119934 100644 --- a/packages/connectors/connector-mailgun/package.json +++ b/packages/connectors/connector-mailgun/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-mock-email-alternative/package.json b/packages/connectors/connector-mock-email-alternative/package.json index 8eb74883a516..ac83e91b0435 100644 --- a/packages/connectors/connector-mock-email-alternative/package.json +++ b/packages/connectors/connector-mock-email-alternative/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "scripts": { "precommit": "lint-staged", diff --git a/packages/connectors/connector-mock-email/package.json b/packages/connectors/connector-mock-email/package.json index 6320c05c110c..3780931281b0 100644 --- a/packages/connectors/connector-mock-email/package.json +++ b/packages/connectors/connector-mock-email/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "scripts": { "precommit": "lint-staged", diff --git a/packages/connectors/connector-mock-sms/package.json b/packages/connectors/connector-mock-sms/package.json index d1ca73d047f2..0601596e0e5f 100644 --- a/packages/connectors/connector-mock-sms/package.json +++ b/packages/connectors/connector-mock-sms/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "scripts": { "precommit": "lint-staged", diff --git a/packages/connectors/connector-mock-social/package.json b/packages/connectors/connector-mock-social/package.json index 205147858a52..a74df4b407a2 100644 --- a/packages/connectors/connector-mock-social/package.json +++ b/packages/connectors/connector-mock-social/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "scripts": { "precommit": "lint-staged", diff --git a/packages/connectors/connector-naver/package.json b/packages/connectors/connector-naver/package.json index 565e5140aae3..e3abd3f5ae30 100644 --- a/packages/connectors/connector-naver/package.json +++ b/packages/connectors/connector-naver/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-oauth2/package.json b/packages/connectors/connector-oauth2/package.json index db885246476a..c55cc1b6be9c 100644 --- a/packages/connectors/connector-oauth2/package.json +++ b/packages/connectors/connector-oauth2/package.json @@ -11,7 +11,7 @@ "ky": "^1.2.3", "query-string": "^9.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-oidc/package.json b/packages/connectors/connector-oidc/package.json index 85c97e3f23e6..5a78b4c73c97 100644 --- a/packages/connectors/connector-oidc/package.json +++ b/packages/connectors/connector-oidc/package.json @@ -11,7 +11,7 @@ "ky": "^1.2.3", "nanoid": "^5.0.1", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-saml/package.json b/packages/connectors/connector-saml/package.json index ffd5649c899c..81229f661dcb 100644 --- a/packages/connectors/connector-saml/package.json +++ b/packages/connectors/connector-saml/package.json @@ -10,7 +10,7 @@ "got": "^14.0.0", "samlify": "2.8.11", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-sendgrid-email/package.json b/packages/connectors/connector-sendgrid-email/package.json index 9aa6191625d3..fd484dbb9744 100644 --- a/packages/connectors/connector-sendgrid-email/package.json +++ b/packages/connectors/connector-sendgrid-email/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-smsaero/package.json b/packages/connectors/connector-smsaero/package.json index 5756aa8836af..ff86413d4cb4 100644 --- a/packages/connectors/connector-smsaero/package.json +++ b/packages/connectors/connector-smsaero/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-smtp/package.json b/packages/connectors/connector-smtp/package.json index b626bf815bbd..63e5289f2aac 100644 --- a/packages/connectors/connector-smtp/package.json +++ b/packages/connectors/connector-smtp/package.json @@ -9,7 +9,7 @@ "got": "^14.0.0", "nodemailer": "^6.9.9", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@rollup/plugin-commonjs": "^26.0.0", diff --git a/packages/connectors/connector-tencent-sms/package.json b/packages/connectors/connector-tencent-sms/package.json index 49678b1a59fb..3a838cb2662d 100644 --- a/packages/connectors/connector-tencent-sms/package.json +++ b/packages/connectors/connector-tencent-sms/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-twilio-sms/package.json b/packages/connectors/connector-twilio-sms/package.json index 3964f6ac8630..91baf1c68eb8 100644 --- a/packages/connectors/connector-twilio-sms/package.json +++ b/packages/connectors/connector-twilio-sms/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-wechat-native/package.json b/packages/connectors/connector-wechat-native/package.json index 77b0e1f98323..b4722f44e959 100644 --- a/packages/connectors/connector-wechat-native/package.json +++ b/packages/connectors/connector-wechat-native/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-wechat-web/package.json b/packages/connectors/connector-wechat-web/package.json index 6a7908efa566..5c47c436e1e6 100644 --- a/packages/connectors/connector-wechat-web/package.json +++ b/packages/connectors/connector-wechat-web/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-wecom/package.json b/packages/connectors/connector-wecom/package.json index ba26fbc621e9..f25b6d2b39af 100644 --- a/packages/connectors/connector-wecom/package.json +++ b/packages/connectors/connector-wecom/package.json @@ -8,7 +8,7 @@ "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/console/package.json b/packages/console/package.json index 1049dc20d665..83eed75a5be5 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -124,7 +124,7 @@ "ts-node": "^10.9.2", "tslib": "^2.4.1", "typescript": "^5.3.3", - "zod": "^3.22.4", + "zod": "^3.23.8", "zod-to-ts": "^1.2.0" }, "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index 05cbc87fffa4..e6eecbcd404e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -90,7 +90,7 @@ "semver": "^7.3.8", "snake-case": "^4.0.0", "snakecase-keys": "^8.0.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@logto/cloud": "0.2.5-3046fa6", diff --git a/packages/demo-app/package.json b/packages/demo-app/package.json index 7f989910854e..0546f88f802a 100644 --- a/packages/demo-app/package.json +++ b/packages/demo-app/package.json @@ -47,7 +47,7 @@ "react-i18next": "^12.3.1", "stylelint": "^15.0.0", "typescript": "^5.3.3", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "engines": { "node": "^20.9.0" diff --git a/packages/experience/package.json b/packages/experience/package.json index 72481759defb..46146440b16d 100644 --- a/packages/experience/package.json +++ b/packages/experience/package.json @@ -90,7 +90,7 @@ "tiny-cookie": "^2.4.1", "typescript": "^5.3.3", "use-debounced-loader": "^0.1.1", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "engines": { "node": "^20.9.0" diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 984fb7eed068..649d8264d7e6 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -48,7 +48,7 @@ "prettier": "^3.0.0", "puppeteer": "^22.6.5", "typescript": "^5.3.3", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "engines": { "node": "^20.9.0" diff --git a/packages/phrases-experience/package.json b/packages/phrases-experience/package.json index 9bf7ce13a4ed..2d3355421a84 100644 --- a/packages/phrases-experience/package.json +++ b/packages/phrases-experience/package.json @@ -38,7 +38,7 @@ "@silverhand/essentials": "^2.9.1" }, "peerDependencies": { - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@silverhand/eslint-config": "6.0.1", diff --git a/packages/phrases/package.json b/packages/phrases/package.json index 8d4963278dfd..dad5dab76b19 100644 --- a/packages/phrases/package.json +++ b/packages/phrases/package.json @@ -37,7 +37,7 @@ "@silverhand/essentials": "^2.9.1" }, "peerDependencies": { - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@silverhand/eslint-config": "6.0.1", diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 173a793f88e4..457d271dc0a0 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -88,6 +88,6 @@ "nanoid": "^5.0.1" }, "peerDependencies": { - "zod": "^3.22.4" + "zod": "^3.23.8" } } diff --git a/packages/toolkit/connector-kit/package.json b/packages/toolkit/connector-kit/package.json index 08deba5cb1be..39c61e1b0aec 100644 --- a/packages/toolkit/connector-kit/package.json +++ b/packages/toolkit/connector-kit/package.json @@ -40,7 +40,7 @@ "@withtyped/server": "^0.13.6" }, "optionalDependencies": { - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@silverhand/eslint-config": "6.0.1", diff --git a/packages/toolkit/core-kit/package.json b/packages/toolkit/core-kit/package.json index cf8118822aa0..38b0b78f11a6 100644 --- a/packages/toolkit/core-kit/package.json +++ b/packages/toolkit/core-kit/package.json @@ -51,7 +51,7 @@ "color": "^4.2.3" }, "optionalDependencies": { - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@silverhand/eslint-config": "6.0.1", diff --git a/packages/toolkit/language-kit/package.json b/packages/toolkit/language-kit/package.json index 631d95327352..d218eeefb73d 100644 --- a/packages/toolkit/language-kit/package.json +++ b/packages/toolkit/language-kit/package.json @@ -33,7 +33,7 @@ "node": "^20.9.0" }, "optionalDependencies": { - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@silverhand/eslint-config": "6.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9de320e453e5..81178076f66b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,8 +161,8 @@ importers: specifier: ^17.6.0 version: 17.6.0 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@silverhand/eslint-config': specifier: 6.0.1 @@ -193,7 +193,7 @@ importers: version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) '@withtyped/server': specifier: ^0.13.6 - version: 0.13.6(zod@3.22.4) + version: 0.13.6(zod@3.23.8) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -231,8 +231,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -313,8 +313,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -389,8 +389,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -462,8 +462,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -541,8 +541,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -620,8 +620,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -696,8 +696,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -775,8 +775,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -851,8 +851,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -924,8 +924,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -997,8 +997,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1073,8 +1073,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1149,8 +1149,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1222,8 +1222,8 @@ importers: specifier: ^1.2.3 version: 1.2.3 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1295,8 +1295,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1368,12 +1368,12 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@logto/cloud': specifier: 0.2.5-a7eedce - version: 0.2.5-a7eedce(zod@3.22.4) + version: 0.2.5-a7eedce(zod@3.23.8) '@rollup/plugin-commonjs': specifier: ^26.0.0 version: 26.0.1(rollup@4.12.0) @@ -1444,8 +1444,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1517,8 +1517,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1590,8 +1590,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1663,8 +1663,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1736,8 +1736,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1809,8 +1809,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1882,8 +1882,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -1955,8 +1955,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2037,8 +2037,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2122,8 +2122,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2201,8 +2201,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2274,8 +2274,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2347,8 +2347,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2423,8 +2423,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2499,8 +2499,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2572,8 +2572,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2645,8 +2645,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2718,8 +2718,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2791,8 +2791,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.0 @@ -2859,7 +2859,7 @@ importers: version: 29.5.0 '@logto/cloud': specifier: 0.2.5-a7eedce - version: 0.2.5-a7eedce(zod@3.22.4) + version: 0.2.5-a7eedce(zod@3.23.8) '@logto/connector-kit': specifier: workspace:^4.0.0 version: link:../toolkit/connector-kit @@ -2964,7 +2964,7 @@ importers: version: 15.5.1 '@withtyped/client': specifier: ^0.8.7 - version: 0.8.7(zod@3.22.4) + version: 0.8.7(zod@3.23.8) buffer: specifier: ^6.0.0 version: 6.0.3 @@ -3149,11 +3149,11 @@ importers: specifier: ^5.3.3 version: 5.3.3 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 zod-to-ts: specifier: ^1.2.0 - version: 1.2.0(typescript@5.3.3)(zod@3.22.4) + version: 1.2.0(typescript@5.3.3)(zod@3.23.8) packages/core: dependencies: @@ -3222,7 +3222,7 @@ importers: version: 10.0.0 '@withtyped/client': specifier: ^0.8.7 - version: 0.8.7(zod@3.22.4) + version: 0.8.7(zod@3.23.8) camelcase: specifier: ^8.0.0 version: 8.0.0 @@ -3353,12 +3353,12 @@ importers: specifier: ^8.0.1 version: 8.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@logto/cloud': specifier: 0.2.5-3046fa6 - version: 0.2.5-3046fa6(zod@3.22.4) + version: 0.2.5-3046fa6(zod@3.23.8) '@silverhand/eslint-config': specifier: 6.0.1 version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.3.3) @@ -3546,8 +3546,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 packages/experience: devDependencies: @@ -3762,8 +3762,8 @@ importers: specifier: ^0.1.1 version: 0.1.1(react@18.2.0) zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 packages/integration-tests: dependencies: @@ -3853,8 +3853,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 packages/phrases: dependencies: @@ -3865,8 +3865,8 @@ importers: specifier: ^2.9.1 version: 2.9.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@silverhand/eslint-config': specifier: 6.0.1 @@ -3899,8 +3899,8 @@ importers: specifier: ^2.9.1 version: 2.9.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@silverhand/eslint-config': specifier: 6.0.1 @@ -3946,13 +3946,13 @@ importers: version: link:../shared '@withtyped/server': specifier: ^0.13.6 - version: 0.13.6(zod@3.22.4) + version: 0.13.6(zod@3.23.8) nanoid: specifier: ^5.0.1 version: 5.0.7 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@silverhand/eslint-config': specifier: 6.0.1 @@ -4065,14 +4065,14 @@ importers: version: 2.9.1 '@withtyped/client': specifier: ^0.8.7 - version: 0.8.7(zod@3.22.4) + version: 0.8.7(zod@3.23.8) '@withtyped/server': specifier: ^0.13.6 - version: 0.13.6(zod@3.22.4) + version: 0.13.6(zod@3.23.8) optionalDependencies: zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@silverhand/eslint-config': specifier: 6.0.1 @@ -4118,8 +4118,8 @@ importers: version: 4.2.3 optionalDependencies: zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@silverhand/eslint-config': specifier: 6.0.1 @@ -4170,8 +4170,8 @@ importers: packages/toolkit/language-kit: optionalDependencies: zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@silverhand/eslint-config': specifier: 6.0.1 @@ -13165,6 +13165,9 @@ packages: zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zwitch@2.0.2: resolution: {integrity: sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==} @@ -14882,17 +14885,17 @@ snapshots: camelcase-keys: 7.0.2 jose: 5.6.3 - '@logto/cloud@0.2.5-3046fa6(zod@3.22.4)': + '@logto/cloud@0.2.5-3046fa6(zod@3.23.8)': dependencies: '@silverhand/essentials': 2.9.1 - '@withtyped/server': 0.13.6(zod@3.22.4) + '@withtyped/server': 0.13.6(zod@3.23.8) transitivePeerDependencies: - zod - '@logto/cloud@0.2.5-a7eedce(zod@3.22.4)': + '@logto/cloud@0.2.5-a7eedce(zod@3.23.8)': dependencies: '@silverhand/essentials': 2.9.1 - '@withtyped/server': 0.13.6(zod@3.22.4) + '@withtyped/server': 0.13.6(zod@3.23.8) transitivePeerDependencies: - zod @@ -15951,10 +15954,10 @@ snapshots: eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-config-xo: 0.44.0(eslint@8.57.0) eslint-config-xo-typescript: 4.0.0(@typescript-eslint/eslint-plugin@7.7.0(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-consistent-default-export-name: 0.0.15 eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-n: 17.2.1(eslint@8.57.0) eslint-plugin-no-use-extend-native: 0.5.0 eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.0.0) @@ -17134,19 +17137,19 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 - '@withtyped/client@0.8.7(zod@3.22.4)': + '@withtyped/client@0.8.7(zod@3.23.8)': dependencies: - '@withtyped/server': 0.13.6(zod@3.22.4) + '@withtyped/server': 0.13.6(zod@3.23.8) '@withtyped/shared': 0.2.2 transitivePeerDependencies: - zod - '@withtyped/server@0.13.6(zod@3.22.4)': + '@withtyped/server@0.13.6(zod@3.23.8)': dependencies: '@silverhand/essentials': 2.9.1 '@withtyped/shared': 0.2.2 nanoid: 4.0.2 - zod: 3.22.4 + zod: 3.23.8 '@withtyped/shared@0.2.2': {} @@ -18782,13 +18785,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.3 is-core-module: 2.13.1 @@ -18799,14 +18802,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -18828,7 +18831,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -18838,7 +18841,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -20960,7 +20963,7 @@ snapshots: '@types/koa': 2.15.0 co-body: 6.1.0 formidable: 3.5.1 - zod: 3.22.4 + zod: 3.23.8 koa-compose@4.1.0: {} @@ -24954,11 +24957,13 @@ snapshots: yocto-queue@1.1.1: {} - zod-to-ts@1.2.0(typescript@5.3.3)(zod@3.22.4): + zod-to-ts@1.2.0(typescript@5.3.3)(zod@3.23.8): dependencies: typescript: 5.3.3 - zod: 3.22.4 + zod: 3.23.8 zod@3.22.4: {} + zod@3.23.8: {} + zwitch@2.0.2: {} From 6bf3bebb4066e47a939c65b7e95476cc08e2075d Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 12 Jul 2024 21:51:10 +0800 Subject: [PATCH 011/135] feat(experience): support loading state for buttons (#6232) --- .../src/assets/icons/loading-ring.svg | 3 ++ .../Button/RotatingRingIcon.module.scss | 14 ++++++++ .../components/Button/RotatingRingIcon.tsx | 7 ++++ .../Button/SocialLinkButton.module.scss | 9 +++++ .../components/Button/SocialLinkButton.tsx | 24 +++++++++++-- .../src/components/Button/index.module.scss | 34 +++++++++++++++---- .../src/components/Button/index.tsx | 22 +++++++++--- packages/experience/src/scss/_colors.scss | 1 + 8 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 packages/experience/src/assets/icons/loading-ring.svg create mode 100644 packages/experience/src/components/Button/RotatingRingIcon.module.scss create mode 100644 packages/experience/src/components/Button/RotatingRingIcon.tsx diff --git a/packages/experience/src/assets/icons/loading-ring.svg b/packages/experience/src/assets/icons/loading-ring.svg new file mode 100644 index 000000000000..6e05dc9be5b0 --- /dev/null +++ b/packages/experience/src/assets/icons/loading-ring.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/experience/src/components/Button/RotatingRingIcon.module.scss b/packages/experience/src/components/Button/RotatingRingIcon.module.scss new file mode 100644 index 000000000000..a2af2a979953 --- /dev/null +++ b/packages/experience/src/components/Button/RotatingRingIcon.module.scss @@ -0,0 +1,14 @@ +.icon { + animation: rotating 1s linear infinite; +} + + +@keyframes rotating { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} diff --git a/packages/experience/src/components/Button/RotatingRingIcon.tsx b/packages/experience/src/components/Button/RotatingRingIcon.tsx new file mode 100644 index 000000000000..ef29529fedad --- /dev/null +++ b/packages/experience/src/components/Button/RotatingRingIcon.tsx @@ -0,0 +1,7 @@ +import Ring from '@/assets/icons/loading-ring.svg'; + +import * as RotatingRingIconStyles from './RotatingRingIcon.module.scss'; + +const RotatingRingIcon = () => ; + +export default RotatingRingIcon; diff --git a/packages/experience/src/components/Button/SocialLinkButton.module.scss b/packages/experience/src/components/Button/SocialLinkButton.module.scss index aeb4c59da541..8ae94cfe7581 100644 --- a/packages/experience/src/components/Button/SocialLinkButton.module.scss +++ b/packages/experience/src/components/Button/SocialLinkButton.module.scss @@ -12,6 +12,15 @@ overflow: hidden; } +.loadingIcon { + display: block; + // To avoid the layout shift, add padding manually (keep the same size as the icon) + padding: 0 1.5px; + color: var(--color-brand-70); + font-size: 0; + line-height: normal; +} + .name { flex: 1; overflow: hidden; diff --git a/packages/experience/src/components/Button/SocialLinkButton.tsx b/packages/experience/src/components/Button/SocialLinkButton.tsx index 0b732ed8ed37..ddfec56613be 100644 --- a/packages/experience/src/components/Button/SocialLinkButton.tsx +++ b/packages/experience/src/components/Button/SocialLinkButton.tsx @@ -1,11 +1,14 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; +import { useDebouncedLoader } from 'use-debounced-loader'; +import RotatingRingIcon from './RotatingRingIcon'; import * as socialLinkButtonStyles from './SocialLinkButton.module.scss'; import * as styles from './index.module.scss'; export type Props = { readonly isDisabled?: boolean; + readonly isLoading?: boolean; readonly className?: string; readonly target: string; readonly logo: string; @@ -13,7 +16,15 @@ export type Props = { readonly onClick?: () => void; }; -const SocialLinkButton = ({ isDisabled, className, target, name, logo, onClick }: Props) => { +const SocialLinkButton = ({ + isDisabled, + isLoading = false, + className, + target, + name, + logo, + onClick, +}: Props) => { const { t, i18n: { language }, @@ -21,6 +32,8 @@ const SocialLinkButton = ({ isDisabled, className, target, name, logo, onClick } const localName = name[language] ?? name.en; + const isLoadingActive = useDebouncedLoader(isLoading, 300); + return ( ); }; diff --git a/packages/experience/src/scss/_colors.scss b/packages/experience/src/scss/_colors.scss index b1d1e4b0080e..2952cb3918fa 100644 --- a/packages/experience/src/scss/_colors.scss +++ b/packages/experience/src/scss/_colors.scss @@ -38,6 +38,7 @@ --color-brand-30: #4300da; --color-brand-40: #5d34f2; --color-brand-50: #7958ff; + --color-brand-70: #af9eff; --color-alert-60: #ca8000; --color-alert-70: #eb9918; From f96277b41088769a492b540b419ed8fb90f8579a Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sat, 13 Jul 2024 21:30:35 +0800 Subject: [PATCH 012/135] refactor: patch type issues --- packages/core/src/oidc/utils.ts | 15 +++++++++++---- .../src/routes/logto-config/jwt-customizer.ts | 3 ++- packages/core/src/utils/SchemaRouter.ts | 5 ++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/core/src/oidc/utils.ts b/packages/core/src/oidc/utils.ts index f25a4c323f6f..83c4e5f01b17 100644 --- a/packages/core/src/oidc/utils.ts +++ b/packages/core/src/oidc/utils.ts @@ -55,12 +55,19 @@ export const buildOidcClientMetadata = (metadata?: OidcClientMetadata): OidcClie ...metadata, }); -export const validateCustomClientMetadata = (key: string, value: unknown) => { - const result = customClientMetadataGuard.pick({ [key]: true }).safeParse({ [key]: value }); +// eslint-disable-next-line @typescript-eslint/ban-types +const isKeyOf = (object: T, key: string | number | symbol): key is keyof T => + key in object; - if (!result.success) { - throw new errors.InvalidClientMetadata(key); +export const validateCustomClientMetadata = (key: string, value: unknown) => { + if (isKeyOf(customClientMetadataGuard.shape, key)) { + const result = customClientMetadataGuard.shape[key].safeParse(value); + if (result.success) { + return; + } } + + throw new errors.InvalidClientMetadata(key); }; export const isOriginAllowed = ( diff --git a/packages/core/src/routes/logto-config/jwt-customizer.ts b/packages/core/src/routes/logto-config/jwt-customizer.ts index 43ae020e530e..437601a0af40 100644 --- a/packages/core/src/routes/logto-config/jwt-customizer.ts +++ b/packages/core/src/routes/logto-config/jwt-customizer.ts @@ -8,6 +8,7 @@ import { jwtCustomizerConfigsGuard, jwtCustomizerTestRequestBodyGuard, } from '@logto/schemas'; +import { removeUndefinedKeys } from '@silverhand/essentials'; import { ResponseError } from '@withtyped/client'; import { ZodError, z } from 'zod'; @@ -236,7 +237,7 @@ export default function logtoConfigJwtCustomizerRoutes { @@ -337,6 +339,7 @@ export default class SchemaRouter< this.post( '/', koaGuard({ + // @ts-expect-error -- `.omit()` doesn't play well with generics body: schema.createGuard.omit({ id: true }), response: entityGuard ?? schema.guard, status: [201], // TODO: 409/422 for conflict? From 62f5e5e0caef590acd49d243875f9937817ecbbb Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sun, 14 Jul 2024 17:42:55 +0800 Subject: [PATCH 013/135] chore: add changesets (#6239) --- .changeset/polite-plums-tease.md | 12 ++++++++++++ .changeset/swift-stingrays-tap.md | 9 +++++++++ 2 files changed, 21 insertions(+) create mode 100644 .changeset/polite-plums-tease.md create mode 100644 .changeset/swift-stingrays-tap.md diff --git a/.changeset/polite-plums-tease.md b/.changeset/polite-plums-tease.md new file mode 100644 index 000000000000..540584ea057a --- /dev/null +++ b/.changeset/polite-plums-tease.md @@ -0,0 +1,12 @@ +--- +"@logto/console": minor +"@logto/core": minor +"@logto/experience": minor +"@logto/integration-tests": patch +--- + +support app-level branding + +You can now set logos, favicons, and colors for your app. These settings will be used in the sign-in experience when the app initiates the authentication flow. For apps that have no branding settings, the omni sign-in experience branding will be used. + +If `organization_id` is provided in the authentication request, the app-level branding settings will be overridden by the organization's branding settings, if available. diff --git a/.changeset/swift-stingrays-tap.md b/.changeset/swift-stingrays-tap.md new file mode 100644 index 000000000000..55803974cf45 --- /dev/null +++ b/.changeset/swift-stingrays-tap.md @@ -0,0 +1,9 @@ +--- +"@logto/console": minor +"@logto/experience": minor +"@logto/schemas": minor +--- + +support dark favicon + +The favicon for the dark theme now can be set in the sign-in experience branding settings. From f6901f591c20399e0d48edecaa2d6f439753cb98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 14:58:09 +0000 Subject: [PATCH 014/135] chore(deps): update vitest monorepo to v2 (major) (#6202) * chore(deps): update vitest monorepo to v2 * refactor: remove unused lint ignorings --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Gao Sun --- packages/app-insights/package.json | 4 +- packages/cli/package.json | 4 +- .../connector-alipay-native/package.json | 4 +- .../connector-alipay-web/package.json | 4 +- .../connector-aliyun-dm/package.json | 4 +- .../src/single-send-mail.test.ts | 1 - .../connector-aliyun-dm/src/utils.test.ts | 1 - .../connector-aliyun-sms/package.json | 4 +- .../src/single-send-text.test.ts | 1 - .../connector-aliyun-sms/src/utils.test.ts | 1 - .../connectors/connector-apple/package.json | 4 +- .../connectors/connector-aws-ses/package.json | 4 +- .../connectors/connector-azuread/package.json | 4 +- .../connector-dingtalk-web/package.json | 4 +- .../connectors/connector-discord/package.json | 4 +- .../connector-facebook/package.json | 4 +- .../connector-feishu-web/package.json | 4 +- .../connectors/connector-github/package.json | 4 +- .../connectors/connector-google/package.json | 4 +- .../connector-huggingface/package.json | 4 +- .../connectors/connector-kakao/package.json | 4 +- .../connector-logto-email/package.json | 4 +- .../connector-logto-sms/package.json | 4 +- .../connector-logto-social-demo/package.json | 4 +- .../connectors/connector-mailgun/package.json | 4 +- .../package.json | 4 +- .../connector-mock-email/package.json | 4 +- .../connector-mock-sms/package.json | 4 +- .../connector-mock-social/package.json | 4 +- .../connectors/connector-naver/package.json | 4 +- .../connectors/connector-oauth2/package.json | 4 +- .../connectors/connector-oidc/package.json | 4 +- .../connectors/connector-saml/package.json | 4 +- .../connector-sendgrid-email/package.json | 4 +- .../connectors/connector-smsaero/package.json | 4 +- .../connectors/connector-smtp/package.json | 4 +- .../connector-tencent-sms/package.json | 4 +- .../connector-twilio-sms/package.json | 4 +- .../connector-wechat-native/package.json | 4 +- .../connector-wechat-web/package.json | 4 +- .../connectors/connector-wecom/package.json | 4 +- packages/schemas/package.json | 4 +- packages/shared/package.json | 4 +- packages/toolkit/connector-kit/package.json | 4 +- packages/toolkit/core-kit/package.json | 4 +- packages/toolkit/language-kit/package.json | 4 +- pnpm-lock.yaml | 833 ++++++++---------- 47 files changed, 453 insertions(+), 552 deletions(-) diff --git a/packages/app-insights/package.json b/packages/app-insights/package.json index 8016dc027944..aaaf49c98095 100644 --- a/packages/app-insights/package.json +++ b/packages/app-insights/package.json @@ -32,12 +32,12 @@ "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/node": "^20.9.5", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "engines": { "node": "^20.9.0" diff --git a/packages/cli/package.json b/packages/cli/package.json index b8578a3eb0fa..3d66337fda66 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -79,13 +79,13 @@ "@types/sinon": "^17.0.0", "@types/tar": "^6.1.12", "@types/yargs": "^17.0.13", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "@withtyped/server": "^0.13.6", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", "sinon": "^18.0.0", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "eslintConfig": { "extends": "@silverhand", diff --git a/packages/connectors/connector-alipay-native/package.json b/packages/connectors/connector-alipay-native/package.json index 7099bda0ce0b..86412243f315 100644 --- a/packages/connectors/connector-alipay-native/package.json +++ b/packages/connectors/connector-alipay-native/package.json @@ -22,7 +22,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -31,7 +31,7 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-alipay-web/package.json b/packages/connectors/connector-alipay-web/package.json index 10b5095b6b48..9d7b13b70cba 100644 --- a/packages/connectors/connector-alipay-web/package.json +++ b/packages/connectors/connector-alipay-web/package.json @@ -21,7 +21,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -30,7 +30,7 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-aliyun-dm/package.json b/packages/connectors/connector-aliyun-dm/package.json index 59e41ddd8e81..9a76d6849528 100644 --- a/packages/connectors/connector-aliyun-dm/package.json +++ b/packages/connectors/connector-aliyun-dm/package.json @@ -59,7 +59,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -68,6 +68,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts b/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts index 1162b5d32104..cf4f13e33db3 100644 --- a/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts +++ b/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts @@ -21,7 +21,6 @@ describe('singleSendMail', () => { }, '' ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const calledData = request.mock.calls[0]; expect(calledData).not.toBeUndefined(); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/packages/connectors/connector-aliyun-dm/src/utils.test.ts b/packages/connectors/connector-aliyun-dm/src/utils.test.ts index 24da02e7a882..ac40456fe085 100644 --- a/packages/connectors/connector-aliyun-dm/src/utils.test.ts +++ b/packages/connectors/connector-aliyun-dm/src/utils.test.ts @@ -24,7 +24,6 @@ describe('request', () => { it('should call got.post with extended params', async () => { const parameters = mockedParameters; await request('http://test.endpoint.com', parameters, 'testsecret'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const calledData = post.mock.calls[0]; expect(calledData).not.toBeUndefined(); const payload = calledData?.[0].form as Record; diff --git a/packages/connectors/connector-aliyun-sms/package.json b/packages/connectors/connector-aliyun-sms/package.json index 9739f43eb75e..a52cce3ac608 100644 --- a/packages/connectors/connector-aliyun-sms/package.json +++ b/packages/connectors/connector-aliyun-sms/package.json @@ -59,7 +59,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -68,6 +68,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts b/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts index ec7da9219648..142628c87c64 100644 --- a/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts +++ b/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts @@ -20,7 +20,6 @@ describe('sendSms', () => { }, '' ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const calledData = request.mock.calls[0]; expect(calledData).not.toBeUndefined(); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/packages/connectors/connector-aliyun-sms/src/utils.test.ts b/packages/connectors/connector-aliyun-sms/src/utils.test.ts index 24da02e7a882..ac40456fe085 100644 --- a/packages/connectors/connector-aliyun-sms/src/utils.test.ts +++ b/packages/connectors/connector-aliyun-sms/src/utils.test.ts @@ -24,7 +24,6 @@ describe('request', () => { it('should call got.post with extended params', async () => { const parameters = mockedParameters; await request('http://test.endpoint.com', parameters, 'testsecret'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const calledData = post.mock.calls[0]; expect(calledData).not.toBeUndefined(); const payload = calledData?.[0].form as Record; diff --git a/packages/connectors/connector-apple/package.json b/packages/connectors/connector-apple/package.json index 2c11467106bb..43cf7a443cec 100644 --- a/packages/connectors/connector-apple/package.json +++ b/packages/connectors/connector-apple/package.json @@ -61,7 +61,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -70,6 +70,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-aws-ses/package.json b/packages/connectors/connector-aws-ses/package.json index 99f6582e3020..4bf0dce98d14 100644 --- a/packages/connectors/connector-aws-ses/package.json +++ b/packages/connectors/connector-aws-ses/package.json @@ -62,7 +62,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -71,6 +71,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-azuread/package.json b/packages/connectors/connector-azuread/package.json index 773b2da57be4..e4036cd2d881 100644 --- a/packages/connectors/connector-azuread/package.json +++ b/packages/connectors/connector-azuread/package.json @@ -61,7 +61,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -70,6 +70,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-dingtalk-web/package.json b/packages/connectors/connector-dingtalk-web/package.json index d2815d24c294..7b503defa415 100644 --- a/packages/connectors/connector-dingtalk-web/package.json +++ b/packages/connectors/connector-dingtalk-web/package.json @@ -21,7 +21,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -30,7 +30,7 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-discord/package.json b/packages/connectors/connector-discord/package.json index eef708ea75b5..d6e851d07d90 100644 --- a/packages/connectors/connector-discord/package.json +++ b/packages/connectors/connector-discord/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-facebook/package.json b/packages/connectors/connector-facebook/package.json index 787adb612c6e..d3ddc5e7b73d 100644 --- a/packages/connectors/connector-facebook/package.json +++ b/packages/connectors/connector-facebook/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-feishu-web/package.json b/packages/connectors/connector-feishu-web/package.json index b6db69458b6f..dd1b65e5f3ff 100644 --- a/packages/connectors/connector-feishu-web/package.json +++ b/packages/connectors/connector-feishu-web/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-github/package.json b/packages/connectors/connector-github/package.json index 1e9eeb2a4e95..78bb96d22a20 100644 --- a/packages/connectors/connector-github/package.json +++ b/packages/connectors/connector-github/package.json @@ -61,7 +61,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "14.0.0-beta.7", @@ -70,6 +70,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-google/package.json b/packages/connectors/connector-google/package.json index 6f5c8da1591c..c193dd1774ea 100644 --- a/packages/connectors/connector-google/package.json +++ b/packages/connectors/connector-google/package.json @@ -61,7 +61,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -70,6 +70,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-huggingface/package.json b/packages/connectors/connector-huggingface/package.json index b87196f02f6b..ac34b6f3d3af 100644 --- a/packages/connectors/connector-huggingface/package.json +++ b/packages/connectors/connector-huggingface/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "14.0.0-beta.7", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-kakao/package.json b/packages/connectors/connector-kakao/package.json index 900f095e6c51..3d2b24b12d8c 100644 --- a/packages/connectors/connector-kakao/package.json +++ b/packages/connectors/connector-kakao/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-logto-email/package.json b/packages/connectors/connector-logto-email/package.json index d209481cfcce..ec8c8f2ba9c1 100644 --- a/packages/connectors/connector-logto-email/package.json +++ b/packages/connectors/connector-logto-email/package.json @@ -61,7 +61,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -70,6 +70,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-logto-sms/package.json b/packages/connectors/connector-logto-sms/package.json index 00fa94c4f508..a9d0e0b35520 100644 --- a/packages/connectors/connector-logto-sms/package.json +++ b/packages/connectors/connector-logto-sms/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-logto-social-demo/package.json b/packages/connectors/connector-logto-social-demo/package.json index 0e765182fbf8..5ba4d835f7a8 100644 --- a/packages/connectors/connector-logto-social-demo/package.json +++ b/packages/connectors/connector-logto-social-demo/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-mailgun/package.json b/packages/connectors/connector-mailgun/package.json index 07cacc5bdb80..afc15dbaecf6 100644 --- a/packages/connectors/connector-mailgun/package.json +++ b/packages/connectors/connector-mailgun/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-mock-email-alternative/package.json b/packages/connectors/connector-mock-email-alternative/package.json index 9132e8f85300..31f3192dfd6d 100644 --- a/packages/connectors/connector-mock-email-alternative/package.json +++ b/packages/connectors/connector-mock-email-alternative/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-mock-email/package.json b/packages/connectors/connector-mock-email/package.json index f1b4024346a6..77b394b53ddc 100644 --- a/packages/connectors/connector-mock-email/package.json +++ b/packages/connectors/connector-mock-email/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-mock-sms/package.json b/packages/connectors/connector-mock-sms/package.json index 1ae46035a40d..e0a6d0e1957f 100644 --- a/packages/connectors/connector-mock-sms/package.json +++ b/packages/connectors/connector-mock-sms/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-mock-social/package.json b/packages/connectors/connector-mock-social/package.json index 902cecb18f76..e0e275c8c724 100644 --- a/packages/connectors/connector-mock-social/package.json +++ b/packages/connectors/connector-mock-social/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-naver/package.json b/packages/connectors/connector-naver/package.json index 22a3e4228165..dfb29b99fd5e 100644 --- a/packages/connectors/connector-naver/package.json +++ b/packages/connectors/connector-naver/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-oauth2/package.json b/packages/connectors/connector-oauth2/package.json index 9fb22112f820..856448d78cbf 100644 --- a/packages/connectors/connector-oauth2/package.json +++ b/packages/connectors/connector-oauth2/package.json @@ -64,7 +64,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "14.0.0-beta.7", @@ -73,6 +73,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-oidc/package.json b/packages/connectors/connector-oidc/package.json index bffa1bbc5091..378b9b6eadb1 100644 --- a/packages/connectors/connector-oidc/package.json +++ b/packages/connectors/connector-oidc/package.json @@ -63,7 +63,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "14.0.0-beta.7", @@ -72,6 +72,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-saml/package.json b/packages/connectors/connector-saml/package.json index 7e84b900cf4a..ddae5bff9c23 100644 --- a/packages/connectors/connector-saml/package.json +++ b/packages/connectors/connector-saml/package.json @@ -62,7 +62,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -71,6 +71,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-sendgrid-email/package.json b/packages/connectors/connector-sendgrid-email/package.json index 58231c9eb008..9df84c3610e3 100644 --- a/packages/connectors/connector-sendgrid-email/package.json +++ b/packages/connectors/connector-sendgrid-email/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-smsaero/package.json b/packages/connectors/connector-smsaero/package.json index 306eb38dbb3e..6a10c2aa7556 100644 --- a/packages/connectors/connector-smsaero/package.json +++ b/packages/connectors/connector-smsaero/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-smtp/package.json b/packages/connectors/connector-smtp/package.json index 8fbd5f468691..5a15d69c331d 100644 --- a/packages/connectors/connector-smtp/package.json +++ b/packages/connectors/connector-smtp/package.json @@ -21,7 +21,7 @@ "@types/node": "^20.11.20", "@types/nodemailer": "^6.4.7", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -30,7 +30,7 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/connectors/connector-tencent-sms/package.json b/packages/connectors/connector-tencent-sms/package.json index a0da5a6bcae9..6573974599f6 100644 --- a/packages/connectors/connector-tencent-sms/package.json +++ b/packages/connectors/connector-tencent-sms/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-twilio-sms/package.json b/packages/connectors/connector-twilio-sms/package.json index 976f30702497..704299ec2d82 100644 --- a/packages/connectors/connector-twilio-sms/package.json +++ b/packages/connectors/connector-twilio-sms/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-wechat-native/package.json b/packages/connectors/connector-wechat-native/package.json index 9a68f2dc4626..6b342f8f7262 100644 --- a/packages/connectors/connector-wechat-native/package.json +++ b/packages/connectors/connector-wechat-native/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-wechat-web/package.json b/packages/connectors/connector-wechat-web/package.json index 07d99ad9280d..03e7405a10be 100644 --- a/packages/connectors/connector-wechat-web/package.json +++ b/packages/connectors/connector-wechat-web/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.11.20", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/connectors/connector-wecom/package.json b/packages/connectors/connector-wecom/package.json index b06794e294c2..b6dba9424ed7 100644 --- a/packages/connectors/connector-wecom/package.json +++ b/packages/connectors/connector-wecom/package.json @@ -60,7 +60,7 @@ "@silverhand/ts-config": "6.0.0", "@types/node": "^20.10.4", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.2", "nock": "^13.3.1", @@ -69,6 +69,6 @@ "rollup-plugin-output-size": "^1.3.0", "supertest": "^7.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" } } diff --git a/packages/schemas/package.json b/packages/schemas/package.json index e7da8c566f34..4aa2e66ed924 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -46,7 +46,7 @@ "@types/inquirer": "^9.0.0", "@types/node": "^20.9.5", "@types/pluralize": "^0.0.33", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "camelcase": "^8.0.0", "chalk": "^5.0.0", "eslint": "^8.56.0", @@ -55,7 +55,7 @@ "prettier": "^3.0.0", "roarr": "^7.11.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "eslintConfig": { "extends": "@silverhand", diff --git a/packages/shared/package.json b/packages/shared/package.json index 5f80353075c2..db6b44d90f2b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -41,12 +41,12 @@ "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/node": "^20.9.5", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "engines": { "node": "^20.9.0" diff --git a/packages/toolkit/connector-kit/package.json b/packages/toolkit/connector-kit/package.json index 08deba5cb1be..60e1a9db155b 100644 --- a/packages/toolkit/connector-kit/package.json +++ b/packages/toolkit/connector-kit/package.json @@ -46,12 +46,12 @@ "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/node": "^20.9.5", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "engines": { "node": "^20.9.0" diff --git a/packages/toolkit/core-kit/package.json b/packages/toolkit/core-kit/package.json index cf8118822aa0..51835938803c 100644 --- a/packages/toolkit/core-kit/package.json +++ b/packages/toolkit/core-kit/package.json @@ -61,14 +61,14 @@ "@types/color": "^3.0.3", "@types/node": "^20.9.5", "@types/react": "^18.0.31", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "postcss": "^8.4.31", "prettier": "^3.0.0", "stylelint": "^15.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "eslintConfig": { "extends": "@silverhand" diff --git a/packages/toolkit/language-kit/package.json b/packages/toolkit/language-kit/package.json index 631d95327352..7af348836131 100644 --- a/packages/toolkit/language-kit/package.json +++ b/packages/toolkit/language-kit/package.json @@ -39,12 +39,12 @@ "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/node": "^20.9.5", - "@vitest/coverage-v8": "^1.4.0", + "@vitest/coverage-v8": "^2.0.0", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", "typescript": "^5.3.3", - "vitest": "^1.4.0" + "vitest": "^2.0.0" }, "eslintConfig": { "extends": "@silverhand" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 095450077188..e78201f4f945 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,8 +62,8 @@ importers: specifier: ^20.9.5 version: 20.10.4 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -77,8 +77,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/cli: dependencies: @@ -189,8 +189,8 @@ importers: specifier: ^17.0.13 version: 17.0.13 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) '@withtyped/server': specifier: ^0.13.6 version: 0.13.6(zod@3.22.4) @@ -207,8 +207,8 @@ importers: specifier: ^18.0.0 version: 18.0.0 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-alipay-native: dependencies: @@ -262,8 +262,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -289,8 +289,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-alipay-web: dependencies: @@ -344,8 +344,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -371,8 +371,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-aliyun-dm: dependencies: @@ -417,8 +417,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -444,8 +444,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-aliyun-sms: dependencies: @@ -490,8 +490,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -517,8 +517,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-apple: dependencies: @@ -569,8 +569,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -596,8 +596,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-aws-ses: dependencies: @@ -648,8 +648,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -675,8 +675,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-azuread: dependencies: @@ -724,8 +724,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -751,8 +751,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-dingtalk-web: dependencies: @@ -806,8 +806,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -833,8 +833,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-discord: dependencies: @@ -879,8 +879,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -906,8 +906,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-facebook: dependencies: @@ -952,8 +952,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -979,8 +979,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-feishu-web: dependencies: @@ -1025,8 +1025,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1052,8 +1052,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-github: dependencies: @@ -1101,8 +1101,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1128,8 +1128,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-google: dependencies: @@ -1177,8 +1177,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1204,8 +1204,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-huggingface: dependencies: @@ -1250,8 +1250,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1277,8 +1277,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-kakao: dependencies: @@ -1323,8 +1323,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1350,8 +1350,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-logto-email: dependencies: @@ -1399,8 +1399,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1426,8 +1426,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-logto-sms: dependencies: @@ -1472,8 +1472,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1499,8 +1499,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-logto-social-demo: dependencies: @@ -1545,8 +1545,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1572,8 +1572,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-mailgun: dependencies: @@ -1618,8 +1618,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1645,8 +1645,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-mock-email: dependencies: @@ -1691,8 +1691,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1718,8 +1718,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-mock-email-alternative: dependencies: @@ -1764,8 +1764,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1791,8 +1791,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-mock-sms: dependencies: @@ -1837,8 +1837,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1864,8 +1864,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-mock-social: dependencies: @@ -1910,8 +1910,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -1937,8 +1937,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-naver: dependencies: @@ -1983,8 +1983,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2010,8 +2010,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-oauth2: dependencies: @@ -2065,8 +2065,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2092,8 +2092,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-oidc: dependencies: @@ -2150,8 +2150,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2177,8 +2177,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-saml: dependencies: @@ -2229,8 +2229,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2256,8 +2256,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-sendgrid-email: dependencies: @@ -2302,8 +2302,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2329,8 +2329,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-smsaero: dependencies: @@ -2375,8 +2375,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2402,8 +2402,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-smtp: dependencies: @@ -2454,8 +2454,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2481,8 +2481,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-tencent-sms: dependencies: @@ -2527,8 +2527,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2554,8 +2554,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-twilio-sms: dependencies: @@ -2600,8 +2600,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2627,8 +2627,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-wechat-native: dependencies: @@ -2673,8 +2673,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2700,8 +2700,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-wechat-web: dependencies: @@ -2746,8 +2746,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2773,8 +2773,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/connectors/connector-wecom: dependencies: @@ -2819,8 +2819,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -2846,8 +2846,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/console: devDependencies: @@ -3976,8 +3976,8 @@ importers: specifier: ^0.0.33 version: 0.0.33 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) camelcase: specifier: ^8.0.0 version: 8.0.0 @@ -4003,8 +4003,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/shared: dependencies: @@ -4037,8 +4037,8 @@ importers: specifier: ^20.9.5 version: 20.10.4 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -4052,8 +4052,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/toolkit/connector-kit: dependencies: @@ -4084,8 +4084,8 @@ importers: specifier: ^20.9.5 version: 20.10.4 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -4099,8 +4099,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/toolkit/core-kit: dependencies: @@ -4143,8 +4143,8 @@ importers: specifier: ^18.0.31 version: 18.0.31 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -4164,8 +4164,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages/toolkit/language-kit: optionalDependencies: @@ -4183,8 +4183,8 @@ importers: specifier: ^20.9.5 version: 20.10.4 '@vitest/coverage-v8': - specifier: ^1.4.0 - version: 1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) + specifier: ^2.0.0 + version: 2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1)) eslint: specifier: ^8.56.0 version: 8.57.0 @@ -4198,8 +4198,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) packages: @@ -4528,10 +4528,6 @@ packages: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.19.4': - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.23.4': resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} engines: {node: '>=6.9.0'} @@ -4540,10 +4536,6 @@ packages: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.22.5': - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.23.5': resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} @@ -4560,11 +4552,6 @@ packages: resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} engines: {node: '>=6.9.0'} - '@babel/parser@7.24.0': - resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.24.4': resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} engines: {node: '>=6.0.0'} @@ -4671,10 +4658,6 @@ packages: resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.20.2': - resolution: {integrity: sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==} - engines: {node: '>=6.9.0'} - '@babel/types@7.24.0': resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} engines: {node: '>=6.9.0'} @@ -5165,15 +5148,9 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/trace-mapping@0.3.18': - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -6808,25 +6785,25 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitest/coverage-v8@1.4.0': - resolution: {integrity: sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==} + '@vitest/coverage-v8@2.0.0': + resolution: {integrity: sha512-k2OgMH8e4AyUVsmLk2P3vT++VVaUQbvVaP5NJQDgXgv1q4lG56Z4nb26QNcxgk4ZLypmOrfultAShMo+okIOYw==} peerDependencies: - vitest: 1.4.0 + vitest: 2.0.0 - '@vitest/expect@1.4.0': - resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} + '@vitest/expect@2.0.0': + resolution: {integrity: sha512-5BSfZ0+dAVmC6uPF7s+TcKx0i7oyYHb1WQQL5gg6G2c+Qkaa5BNrdRM74sxDfUIZUgYCr6bfCqmJp+X5bfcNxQ==} - '@vitest/runner@1.4.0': - resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} + '@vitest/runner@2.0.0': + resolution: {integrity: sha512-OovFmlkfRmdhevbWImBUtn9IEM+CKac8O+m9p6W9jTATGVBnDJQ6/jb1gpHyWxsu0ALi5f+TLi+Uyst7AAimMw==} - '@vitest/snapshot@1.4.0': - resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} + '@vitest/snapshot@2.0.0': + resolution: {integrity: sha512-B520cSAQwtWgocPpARadnNLslHCxFs5tf7SG2TT96qz+SZgsXqcB1xI3w3/S9kUzdqykEKrMLvW+sIIpMcuUdw==} - '@vitest/spy@1.4.0': - resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} + '@vitest/spy@2.0.0': + resolution: {integrity: sha512-0g7ho4wBK09wq8iNZFtUcQZcUcbPmbLWFotL0GXel0fvk5yPi4nTEKpIvZ+wA5eRyqPUCIfIUl10AWzLr67cmA==} - '@vitest/utils@1.4.0': - resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} + '@vitest/utils@2.0.0': + resolution: {integrity: sha512-t0jbx8VugWEP6A29NbyfQKVU68Vo6oUw0iX3a8BwO3nrZuivfHcFO4Y5UsqXlplX+83P9UaqEvC2YQhspC0JSA==} '@withtyped/client@0.8.7': resolution: {integrity: sha512-qK+Tsczvko8mBRACtHGYj0CdMZFaBmosMGUahTAr544Jb183INPZPn/NpUFtTEpl5g3e4lUjMc5jPH0V78D0+g==} @@ -7058,8 +7035,9 @@ packages: resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} engines: {node: '>=12.0.0'} - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -7307,9 +7285,9 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -7355,8 +7333,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -7913,6 +7892,15 @@ packages: supports-color: optional: true + debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -7954,8 +7942,8 @@ packages: babel-plugin-macros: optional: true - deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} deep-equal@1.0.1: @@ -8886,11 +8874,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.3.12: - resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - glob@10.4.2: resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} engines: {node: '>=16 || 14 >=14.18'} @@ -9612,8 +9595,8 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.4: - resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} istanbul-reports@3.1.7: @@ -9623,10 +9606,6 @@ packages: iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - jackspeak@3.4.0: resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} engines: {node: '>=14'} @@ -9833,8 +9812,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@8.0.3: - resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} js-types@1.0.0: resolution: {integrity: sha512-bfwqBW9cC/Lp7xcRpug7YrXm0IVw+T9e3g4mCYnv0Pjr3zIzU9PCQElYU9oSGAWzXlbdl9X5SAMPejO9sxkeUw==} @@ -10122,10 +10101,6 @@ packages: resolution: {integrity: sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==} engines: {node: '>= 12.13.0'} - local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -10226,8 +10201,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -10269,12 +10244,15 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magic-string@0.30.7: resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} engines: {node: '>=12'} - magicast@0.3.3: - resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} + magicast@0.3.4: + resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -10665,9 +10643,6 @@ packages: engines: {node: '>=10'} hasBin: true - mlly@1.6.1: - resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} - module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} @@ -11000,10 +10975,6 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - p-limit@6.0.0: resolution: {integrity: sha512-Dx+NzOuILWwjJE9OYtEKuQRy0i3c5QVAmDsVrvvRSgyNnPuB27D2DyEjl6QTNyeePaAHjaPk+ya0yA0Frld8RA==} engines: {node: '>=18'} @@ -11126,10 +11097,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.10.2: - resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} - engines: {node: '>=16 || 14 >=14.17'} - path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -11147,8 +11114,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -11228,6 +11196,9 @@ packages: picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -11253,9 +11224,6 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} - pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} - pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -12379,8 +12347,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@2.0.0: - resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} + strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} @@ -12516,6 +12484,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -12539,18 +12511,18 @@ packages: tiny-cookie@2.4.1: resolution: {integrity: sha512-h8ueaMyvUd/9ZfRqCfa1t+0tXqfVFhdK8WpLHz8VXMqsiaj3Sqg64AOCH/xevLQGZk0ZV+/75ouITdkvp3taVA==} - tinybench@2.6.0: - resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinypool@0.8.2: - resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} - engines: {node: '>=14.0.0'} + tinypool@1.0.0: + resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} + engines: {node: ^18.0.0 || >=20.0.0} - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} engines: {node: '>=14.0.0'} tmp@0.0.33: @@ -12744,9 +12716,6 @@ packages: ua-parser-js@1.0.37: resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} - ufo@1.5.2: - resolution: {integrity: sha512-eiutMaL0J2MKdhcOM1tUy13pIrYnyR87fEd8STJQFrrAwImwvlXkxlZEjaKah8r2viPohld08lt73QfLG1NxMg==} - unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -12874,8 +12843,8 @@ packages: vfile@6.0.1: resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - vite-node@1.4.0: - resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + vite-node@2.0.0: + resolution: {integrity: sha512-jZtezmjcgZTkMisIi68TdY8w/PqPTxK2pbfTU9/4Gqus1K3AVZqkwH0z7Vshe3CD6mq9rJq8SpqmuefDMIqkfQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -12907,15 +12876,15 @@ packages: terser: optional: true - vitest@1.4.0: - resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + vitest@2.0.0: + resolution: {integrity: sha512-NvccE2tZhIoPSq3o3AoTBmItwhHNjzIxvOgfdzILIscyzSGOtw2+A1d/JJbS86HDVbc6TS5HnckQuCgTfp0HDQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.4.0 - '@vitest/ui': 1.4.0 + '@vitest/browser': 2.0.0 + '@vitest/ui': 2.0.0 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -13971,14 +13940,10 @@ snapshots: dependencies: '@babel/types': 7.24.0 - '@babel/helper-string-parser@7.19.4': {} - '@babel/helper-string-parser@7.23.4': {} '@babel/helper-validator-identifier@7.22.20': {} - '@babel/helper-validator-identifier@7.22.5': {} - '@babel/helper-validator-option@7.23.5': {} '@babel/helpers@7.24.4': @@ -13991,7 +13956,7 @@ snapshots: '@babel/highlight@7.22.5': dependencies: - '@babel/helper-validator-identifier': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 @@ -14002,10 +13967,6 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.0.0 - '@babel/parser@7.24.0': - dependencies: - '@babel/types': 7.24.0 - '@babel/parser@7.24.4': dependencies: '@babel/types': 7.24.0 @@ -14123,12 +14084,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.20.2': - dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.22.5 - to-fast-properties: 2.0.0 - '@babel/types@7.24.0': dependencies: '@babel/helper-string-parser': 7.23.4 @@ -14723,7 +14678,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.25 '@types/node': 20.12.7 chalk: 4.1.2 collect-v8-coverage: 1.0.1 @@ -14856,15 +14811,8 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.4.14': {} - '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/trace-mapping@0.3.18': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.0 @@ -16490,7 +16438,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@6.5.1': dependencies: - '@babel/types': 7.20.2 + '@babel/types': 7.24.0 entities: 4.4.0 '@svgr/plugin-jsx@6.5.1(@svgr/core@6.5.1)': @@ -17109,93 +17057,89 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitest/coverage-v8@1.4.0(vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1))': + '@vitest/coverage-v8@2.0.0(vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.4 + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.4 + istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.7 - magicast: 0.3.3 - picocolors: 1.0.0 + magic-string: 0.30.10 + magicast: 0.3.4 + picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.0.0 - test-exclude: 6.0.0 - v8-to-istanbul: 9.2.0 - vitest: 1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + strip-literal: 2.1.0 + test-exclude: 7.0.1 + vitest: 2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@1.4.0(vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1))': + '@vitest/coverage-v8@2.0.0(vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.4 + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.4 + istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.7 - magicast: 0.3.3 - picocolors: 1.0.0 + magic-string: 0.30.10 + magicast: 0.3.4 + picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.0.0 - test-exclude: 6.0.0 - v8-to-istanbul: 9.2.0 - vitest: 1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + strip-literal: 2.1.0 + test-exclude: 7.0.1 + vitest: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@1.4.0(vitest@1.4.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1))': + '@vitest/coverage-v8@2.0.0(vitest@2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.4 + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.4 + istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.7 - magicast: 0.3.3 - picocolors: 1.0.0 + magic-string: 0.30.10 + magicast: 0.3.4 + picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.0.0 - test-exclude: 6.0.0 - v8-to-istanbul: 9.2.0 - vitest: 1.4.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) + strip-literal: 2.1.0 + test-exclude: 7.0.1 + vitest: 2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1) transitivePeerDependencies: - supports-color - '@vitest/expect@1.4.0': + '@vitest/expect@2.0.0': dependencies: - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 - chai: 4.4.1 + '@vitest/spy': 2.0.0 + '@vitest/utils': 2.0.0 + chai: 5.1.1 - '@vitest/runner@1.4.0': + '@vitest/runner@2.0.0': dependencies: - '@vitest/utils': 1.4.0 - p-limit: 5.0.0 + '@vitest/utils': 2.0.0 pathe: 1.1.2 - '@vitest/snapshot@1.4.0': + '@vitest/snapshot@2.0.0': dependencies: - magic-string: 0.30.7 + magic-string: 0.30.10 pathe: 1.1.2 pretty-format: 29.7.0 - '@vitest/spy@1.4.0': + '@vitest/spy@2.0.0': dependencies: - tinyspy: 2.2.1 + tinyspy: 3.0.0 - '@vitest/utils@1.4.0': + '@vitest/utils@2.0.0': dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 - loupe: 2.3.7 + loupe: 3.1.1 pretty-format: 29.7.0 '@withtyped/client@0.8.7(zod@3.22.4)': @@ -17464,7 +17408,7 @@ snapshots: pvutils: 1.1.3 tslib: 2.6.2 - assertion-error@1.1.0: {} + assertion-error@2.0.1: {} ast-types-flow@0.0.8: {} @@ -17739,15 +17683,13 @@ snapshots: ccount@2.0.1: {} - chai@4.4.1: + chai@5.1.1: dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.3 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.0.8 + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 chalk@2.4.2: dependencies: @@ -17782,9 +17724,7 @@ snapshots: chardet@0.7.0: {} - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 + check-error@2.1.1: {} chokidar@3.5.3: dependencies: @@ -18387,6 +18327,10 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.3.5: + dependencies: + ms: 2.1.2 + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 @@ -18414,9 +18358,7 @@ snapshots: dedent@1.5.1: {} - deep-eql@4.1.3: - dependencies: - type-detect: 4.0.8 + deep-eql@5.0.2: {} deep-equal@1.0.1: {} @@ -19541,14 +19483,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.3.12: - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.4 - minipass: 7.0.4 - path-scurry: 1.10.2 - glob@10.4.2: dependencies: foreground-child: 3.1.1 @@ -20314,10 +20248,10 @@ snapshots: transitivePeerDependencies: - supports-color - istanbul-lib-source-maps@5.0.4: + istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.4 + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -20335,12 +20269,6 @@ snapshots: reflect.getprototypeof: 1.0.6 set-function-name: 2.0.2 - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jackspeak@3.4.0: dependencies: '@isaacs/cliui': 8.0.2 @@ -20862,7 +20790,7 @@ snapshots: js-tokens@4.0.0: {} - js-tokens@8.0.3: {} + js-tokens@9.0.0: {} js-types@1.0.0: {} @@ -21247,11 +21175,6 @@ snapshots: loader-utils@3.2.0: {} - local-pkg@0.5.0: - dependencies: - mlly: 1.6.1 - pkg-types: 1.0.3 - locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -21334,7 +21257,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@2.3.7: + loupe@3.1.1: dependencies: get-func-name: 2.0.2 @@ -21372,15 +21295,19 @@ snapshots: lz-string@1.5.0: {} + magic-string@0.30.10: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + magic-string@0.30.7: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - magicast@0.3.3: + magicast@0.3.4: dependencies: - '@babel/parser': 7.24.0 + '@babel/parser': 7.24.4 '@babel/types': 7.24.0 - source-map-js: 1.0.2 + source-map-js: 1.2.0 make-dir@4.0.0: dependencies: @@ -22118,13 +22045,6 @@ snapshots: mkdirp@3.0.1: {} - mlly@1.6.1: - dependencies: - acorn: 8.11.3 - pathe: 1.1.2 - pkg-types: 1.0.3 - ufo: 1.5.2 - module-details-from-path@1.0.3: {} monaco-editor@0.47.0: {} @@ -22484,10 +22404,6 @@ snapshots: dependencies: yocto-queue: 1.0.0 - p-limit@5.0.0: - dependencies: - yocto-queue: 1.0.0 - p-limit@6.0.0: dependencies: yocto-queue: 1.1.1 @@ -22635,11 +22551,6 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.10.2: - dependencies: - lru-cache: 10.2.0 - minipass: 7.0.4 - path-scurry@1.11.1: dependencies: lru-cache: 10.2.0 @@ -22655,7 +22566,7 @@ snapshots: pathe@1.1.2: {} - pathval@1.1.1: {} + pathval@2.0.0: {} pend@1.2.0: {} @@ -22741,6 +22652,8 @@ snapshots: picocolors@1.0.0: {} + picocolors@1.0.1: {} + picomatch@2.3.1: {} pidtree@0.6.0: {} @@ -22757,12 +22670,6 @@ snapshots: dependencies: find-up: 5.0.0 - pkg-types@1.0.3: - dependencies: - jsonc-parser: 3.2.0 - mlly: 1.6.1 - pathe: 1.1.2 - pluralize@8.0.0: {} pngjs@5.0.0: {} @@ -23524,7 +23431,7 @@ snapshots: rimraf@5.0.5: dependencies: - glob: 10.3.12 + glob: 10.4.2 roarr@7.11.0: dependencies: @@ -24023,9 +23930,9 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@2.0.0: + strip-literal@2.1.0: dependencies: - js-tokens: 8.0.3 + js-tokens: 9.0.0 strnum@1.0.5: {} @@ -24239,6 +24146,12 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.2 + minimatch: 9.0.4 + text-extensions@2.4.0: {} text-table@0.2.0: {} @@ -24255,13 +24168,13 @@ snapshots: tiny-cookie@2.4.1: {} - tinybench@2.6.0: {} + tinybench@2.8.0: {} tinycolor2@1.6.0: {} - tinypool@0.8.2: {} + tinypool@1.0.0: {} - tinyspy@2.2.1: {} + tinyspy@3.0.0: {} tmp@0.0.33: dependencies: @@ -24453,8 +24366,6 @@ snapshots: ua-parser-js@1.0.37: {} - ufo@1.5.2: {} - unbox-primitive@1.0.2: dependencies: call-bind: 1.0.7 @@ -24597,12 +24508,12 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-node@1.4.0(@types/node@20.10.4)(sass@1.56.1): + vite-node@2.0.0(@types/node@20.10.4)(sass@1.56.1): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.3.5 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 vite: 5.2.9(@types/node@20.10.4)(sass@1.56.1) transitivePeerDependencies: - '@types/node' @@ -24614,12 +24525,12 @@ snapshots: - supports-color - terser - vite-node@1.4.0(@types/node@20.11.20)(sass@1.56.1): + vite-node@2.0.0(@types/node@20.11.20)(sass@1.56.1): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.3.5 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 vite: 5.2.9(@types/node@20.11.20)(sass@1.56.1) transitivePeerDependencies: - '@types/node' @@ -24631,12 +24542,12 @@ snapshots: - supports-color - terser - vite-node@1.4.0(@types/node@20.12.7)(sass@1.56.1): + vite-node@2.0.0(@types/node@20.12.7)(sass@1.56.1): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.3.5 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 vite: 5.2.9(@types/node@20.12.7)(sass@1.56.1) transitivePeerDependencies: - '@types/node' @@ -24678,27 +24589,25 @@ snapshots: fsevents: 2.3.3 sass: 1.56.1 - vitest@1.4.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1): + vitest@2.0.0(@types/node@20.10.4)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1): dependencies: - '@vitest/expect': 1.4.0 - '@vitest/runner': 1.4.0 - '@vitest/snapshot': 1.4.0 - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 - acorn-walk: 8.3.2 - chai: 4.4.1 - debug: 4.3.4 + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.0 + '@vitest/runner': 2.0.0 + '@vitest/snapshot': 2.0.0 + '@vitest/spy': 2.0.0 + '@vitest/utils': 2.0.0 + chai: 5.1.1 + debug: 4.3.5 execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.7 + magic-string: 0.30.10 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.2 + tinybench: 2.8.0 + tinypool: 1.0.0 vite: 5.2.9(@types/node@20.10.4)(sass@1.56.1) - vite-node: 1.4.0(@types/node@20.10.4)(sass@1.56.1) + vite-node: 2.0.0(@types/node@20.10.4)(sass@1.56.1) why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.10.4 @@ -24713,27 +24622,25 @@ snapshots: - supports-color - terser - vitest@1.4.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1): + vitest@2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1): dependencies: - '@vitest/expect': 1.4.0 - '@vitest/runner': 1.4.0 - '@vitest/snapshot': 1.4.0 - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 - acorn-walk: 8.3.2 - chai: 4.4.1 - debug: 4.3.4 + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.0 + '@vitest/runner': 2.0.0 + '@vitest/snapshot': 2.0.0 + '@vitest/spy': 2.0.0 + '@vitest/utils': 2.0.0 + chai: 5.1.1 + debug: 4.3.5 execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.7 + magic-string: 0.30.10 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.2 + tinybench: 2.8.0 + tinypool: 1.0.0 vite: 5.2.9(@types/node@20.11.20)(sass@1.56.1) - vite-node: 1.4.0(@types/node@20.11.20)(sass@1.56.1) + vite-node: 2.0.0(@types/node@20.11.20)(sass@1.56.1) why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.11.20 @@ -24748,27 +24655,25 @@ snapshots: - supports-color - terser - vitest@1.4.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1): + vitest@2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(sass@1.56.1): dependencies: - '@vitest/expect': 1.4.0 - '@vitest/runner': 1.4.0 - '@vitest/snapshot': 1.4.0 - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 - acorn-walk: 8.3.2 - chai: 4.4.1 - debug: 4.3.4 + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.0 + '@vitest/runner': 2.0.0 + '@vitest/snapshot': 2.0.0 + '@vitest/spy': 2.0.0 + '@vitest/utils': 2.0.0 + chai: 5.1.1 + debug: 4.3.5 execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.7 + magic-string: 0.30.10 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.2 + tinybench: 2.8.0 + tinypool: 1.0.0 vite: 5.2.9(@types/node@20.12.7)(sass@1.56.1) - vite-node: 1.4.0(@types/node@20.12.7)(sass@1.56.1) + vite-node: 2.0.0(@types/node@20.12.7)(sass@1.56.1) why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.12.7 From 5bae495cc9f6a095c5d0c338dcd5dbea2c8d7166 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 15 Jul 2024 09:53:50 +0800 Subject: [PATCH 015/135] feat(core,schemas): implement the sie settings guard (#6215) * feat(core,schemas): implement the sie settings guard implement the sie settings guard * fix(test): fix integration test fix integration test * test(core): add sie guard ut add sie guard ut * chore(core): add some comment add some comment * refactor(core): rename the sign-in-experience-settings class rename the sign-in-experience-settings class --- .../classes/experience-interaction.ts | 122 +++-- .../src/routes/experience/classes/utils.ts | 50 +- .../sign-in-experience-validator.test.ts | 500 ++++++++++++++++++ .../sign-in-experience-validator.ts | 230 ++++++++ .../verifications/code-verification.ts | 43 +- packages/core/src/routes/experience/index.ts | 19 +- .../backup-code-verification.ts | 2 +- .../verification-routes/totp-verification.ts | 4 +- .../verification-routes/verification-code.ts | 4 +- .../src/helpers/experience/index.ts | 4 +- .../api/experience-api/interaction.test.ts | 4 +- .../sign-in-interaction/password.test.ts | 4 +- .../verification-code.test.ts | 8 +- .../backup-code-verification.test.ts | 2 +- .../password-verification.test.ts | 4 +- .../verifications/social-verification.test.ts | 4 +- .../verifications/totp-verification.test.ts | 6 +- .../verifications/verification-code.test.ts | 6 +- .../phrases/src/locales/en/errors/session.ts | 1 - packages/schemas/src/types/interactions.ts | 20 +- 20 files changed, 882 insertions(+), 155 deletions(-) create mode 100644 packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts create mode 100644 packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts diff --git a/packages/core/src/routes/experience/classes/experience-interaction.ts b/packages/core/src/routes/experience/classes/experience-interaction.ts index ec37d2590b6b..9be9636029ce 100644 --- a/packages/core/src/routes/experience/classes/experience-interaction.ts +++ b/packages/core/src/routes/experience/classes/experience-interaction.ts @@ -9,7 +9,7 @@ import assertThat from '#src/utils/assert-that.js'; import type { Interaction } from '../types.js'; -import { validateSieVerificationMethod } from './utils.js'; +import { SignInExperienceValidator } from './validators/sign-in-experience-validator.js'; import { buildVerificationRecord, verificationRecordDataGuard, @@ -38,6 +38,8 @@ const interactionStorageGuard = z.object({ * @see {@link https://github.com/logto-io/rfcs | Logto RFCs} for more information about RFC 0004. */ export default class ExperienceInteraction { + public readonly signInExperienceValidator: SignInExperienceValidator; + /** The user verification record list for the current interaction. */ private readonly verificationRecords = new Map(); /** The userId of the user for the current interaction. Only available once the user is identified. */ @@ -60,12 +62,15 @@ export default class ExperienceInteraction { ) { const { libraries, queries } = tenant; + this.signInExperienceValidator = new SignInExperienceValidator(libraries, queries); + if (!interactionDetails) { return; } const result = interactionStorageGuard.safeParse(interactionDetails.result ?? {}); + // `interactionDetails.result` is not a valid experience interaction storage assertThat( result.success, new RequestError({ code: 'session.interaction_not_found', status: 404 }) @@ -91,9 +96,26 @@ export default class ExperienceInteraction { return this.#interactionEvent; } - /** Set the interaction event for the current interaction */ - public setInteractionEvent(interactionEvent: InteractionEvent) { - // TODO: conflict event check (e.g. reset password session can't be used for sign in) + /** + * Set the interaction event for the current interaction + * + * @throws RequestError with 403 if the interaction event is not allowed by the `SignInExperienceValidator` + * @throws RequestError with 400 if the interaction event is `ForgotPassword` and the current interaction event is not `ForgotPassword` + * @throws RequestError with 400 if the interaction event is not `ForgotPassword` and the current interaction event is `ForgotPassword` + */ + public async setInteractionEvent(interactionEvent: InteractionEvent) { + await this.signInExperienceValidator.guardInteractionEvent(interactionEvent); + + // `ForgotPassword` interaction event can not interchanged with other events + if (this.interactionEvent) { + assertThat( + interactionEvent === InteractionEvent.ForgotPassword + ? this.interactionEvent === InteractionEvent.ForgotPassword + : this.interactionEvent !== InteractionEvent.ForgotPassword, + new RequestError({ code: 'session.not_supported_for_forgot_password', status: 400 }) + ); + } + this.#interactionEvent = interactionEvent; } @@ -101,13 +123,13 @@ export default class ExperienceInteraction { * Identify the user using the verification record. * * - Check if the verification record exists. - * - Check if the verification record is valid for the current interaction event. + * - Verify the verification record with `SignInExperienceValidator`. * - Create a new user using the verification record if the current interaction event is `Register`. * - Identify the user using the verification record if the current interaction event is `SignIn` or `ForgotPassword`. * - Set the user id to the current interaction. * - * @throws RequestError with 404 if the verification record is not found * @throws RequestError with 404 if the interaction event is not set + * @throws RequestError with 404 if the verification record is not found * @throws RequestError with 400 if the verification record is not valid for the current interaction event * @throws RequestError with 401 if the user is suspended * @throws RequestError with 409 if the current session has already identified a different user @@ -116,47 +138,26 @@ export default class ExperienceInteraction { const verificationRecord = this.getVerificationRecordById(verificationId); assertThat( - verificationRecord && this.interactionEvent, + this.interactionEvent, + new RequestError({ code: 'session.interaction_not_found', status: 404 }) + ); + + assertThat( + verificationRecord, new RequestError({ code: 'session.verification_session_not_found', status: 404 }) ); - // Existing user identification flow - validateSieVerificationMethod(this.interactionEvent, verificationRecord); + await this.signInExperienceValidator.verifyIdentificationMethod( + this.interactionEvent, + verificationRecord + ); - // User creation flow if (this.interactionEvent === InteractionEvent.Register) { - this.createNewUser(verificationRecord); + await this.createNewUser(verificationRecord); return; } - switch (verificationRecord.type) { - case VerificationType.Password: - case VerificationType.VerificationCode: - case VerificationType.Social: - case VerificationType.EnterpriseSso: { - // TODO: social sign-in with verified email - - const { id, isSuspended } = await verificationRecord.identifyUser(); - - assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); - - // Throws an 409 error if the current session has already identified a different user - if (this.userId) { - assertThat( - this.userId === id, - new RequestError({ code: 'session.identity_conflict', status: 409 }) - ); - return; - } - - this.userId = id; - break; - } - default: { - // Unsupported verification type for identification, such as MFA verification. - throw new RequestError({ code: 'session.verification_failed', status: 400 }); - } - } + await this.identifyExistingUser(verificationRecord); } /** @@ -223,7 +224,46 @@ export default class ExperienceInteraction { return [...this.verificationRecords.values()]; } - private createNewUser(verificationRecord: VerificationRecord) { - // TODO: create new user for the Register event + private async identifyExistingUser(verificationRecord: VerificationRecord) { + switch (verificationRecord.type) { + case VerificationType.Password: + case VerificationType.VerificationCode: + case VerificationType.Social: + case VerificationType.EnterpriseSso: { + // TODO: social sign-in with verified email + const { id, isSuspended } = await verificationRecord.identifyUser(); + + assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); + + // Throws an 409 error if the current session has already identified a different user + if (this.userId) { + assertThat( + this.userId === id, + new RequestError({ code: 'session.identity_conflict', status: 409 }) + ); + return; + } + + this.userId = id; + break; + } + default: { + // Unsupported verification type for identification, such as MFA verification. + throw new RequestError({ code: 'session.verification_failed', status: 400 }); + } + } + } + + private async createNewUser(verificationRecord: VerificationRecord) { + // TODO: To be implemented + switch (verificationRecord.type) { + case VerificationType.VerificationCode: { + break; + } + default: { + // Unsupported verification type for user creation, such as MFA verification. + throw new RequestError({ code: 'session.verification_failed', status: 400 }); + } + } } } diff --git a/packages/core/src/routes/experience/classes/utils.ts b/packages/core/src/routes/experience/classes/utils.ts index dfaa48847563..575908b45fa3 100644 --- a/packages/core/src/routes/experience/classes/utils.ts +++ b/packages/core/src/routes/experience/classes/utils.ts @@ -1,62 +1,20 @@ -import { - InteractionEvent, - InteractionIdentifierType, - VerificationType, - type InteractionIdentifier, -} from '@logto/schemas'; +import { SignInIdentifier, type InteractionIdentifier } from '@logto/schemas'; -import RequestError from '#src/errors/RequestError/index.js'; import type Queries from '#src/tenants/Queries.js'; -import assertThat from '#src/utils/assert-that.js'; - -import { type VerificationRecord } from './verifications/index.js'; export const findUserByIdentifier = async ( userQuery: Queries['users'], { type, value }: InteractionIdentifier ) => { switch (type) { - case InteractionIdentifierType.Username: { + case SignInIdentifier.Username: { return userQuery.findUserByUsername(value); } - case InteractionIdentifierType.Email: { + case SignInIdentifier.Email: { return userQuery.findUserByEmail(value); } - case InteractionIdentifierType.Phone: { + case SignInIdentifier.Phone: { return userQuery.findUserByPhone(value); } } }; - -/** - * Check if the verification record is valid for the current interaction event. - * - * This function will compare the verification record for the current interaction event with Logto's SIE settings - * - * @throws RequestError with 400 if the verification record is not valid for the current interaction event - */ -export const validateSieVerificationMethod = ( - interactionEvent: InteractionEvent, - verificationRecord: VerificationRecord -) => { - switch (interactionEvent) { - case InteractionEvent.SignIn: { - // TODO: sign-in methods validation - break; - } - case InteractionEvent.Register: { - // TODO: sign-up methods validation - break; - } - case InteractionEvent.ForgotPassword: { - // Forgot password only supports verification code type verification record - // The verification record's interaction event must be ForgotPassword - assertThat( - verificationRecord.type === VerificationType.VerificationCode && - verificationRecord.interactionEvent === InteractionEvent.ForgotPassword, - new RequestError({ code: 'session.verification_session_not_found', status: 400 }) - ); - break; - } - } -}; diff --git a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts new file mode 100644 index 000000000000..f7f49f296cef --- /dev/null +++ b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts @@ -0,0 +1,500 @@ +/* eslint-disable max-lines */ +import { + InteractionEvent, + type SignInExperience, + SignInIdentifier, + SignInMode, + VerificationType, +} from '@logto/schemas'; + +import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; +import RequestError from '#src/errors/RequestError/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; + +import { CodeVerification } from '../verifications/code-verification.js'; +import { EnterpriseSsoVerification } from '../verifications/enterprise-sso-verification.js'; +import { type VerificationRecord } from '../verifications/index.js'; +import { PasswordVerification } from '../verifications/password-verification.js'; +import { SocialVerification } from '../verifications/social-verification.js'; + +import { SignInExperienceValidator } from './sign-in-experience-validator.js'; + +const { jest } = import.meta; + +const emailDomain = 'logto.io'; + +const signInExperiences = { + findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience), +}; +const ssoConnectors = { + getAvailableSsoConnectors: jest.fn().mockResolvedValue([]), +}; + +const mockTenant = new MockTenant(undefined, { signInExperiences }, undefined, { ssoConnectors }); + +const passwordVerificationRecords = Object.fromEntries( + Object.values(SignInIdentifier).map((identifier) => [ + identifier, + PasswordVerification.create(mockTenant.libraries, mockTenant.queries, { + type: identifier, + value: identifier === SignInIdentifier.Email ? `foo@${emailDomain}` : 'value', + }), + ]) +) as Record; + +const verificationCodeVerificationRecords = Object.freeze({ + [SignInIdentifier.Email]: CodeVerification.create( + mockTenant.libraries, + mockTenant.queries, + { + type: SignInIdentifier.Email, + value: `foo@${emailDomain}`, + }, + InteractionEvent.SignIn + ), + [SignInIdentifier.Phone]: CodeVerification.create( + mockTenant.libraries, + mockTenant.queries, + { + type: SignInIdentifier.Phone, + value: 'value', + }, + InteractionEvent.SignIn + ), +}); + +const enterpriseSsoVerificationRecords = EnterpriseSsoVerification.create( + mockTenant.libraries, + mockTenant.queries, + 'mock_connector_id' +); + +const socialVerificationRecord = new SocialVerification(mockTenant.libraries, mockTenant.queries, { + id: 'social_verification_id', + type: VerificationType.Social, + connectorId: 'mock_connector_id', + socialUserInfo: { + id: 'user_id', + email: `foo@${emailDomain}`, + }, +}); + +describe('SignInExperienceValidator', () => { + describe('guardInteractionEvent', () => { + it('SignInMode.Register', async () => { + const signInExperience = { + signInMode: SignInMode.Register, + }; + signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.SignIn) + ).rejects.toMatchError(new RequestError({ code: 'auth.forbidden', status: 403 })); + + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.Register) + ).resolves.not.toThrow(); + + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.ForgotPassword) + ).resolves.not.toThrow(); + }); + + it('SignInMode.SignIn', async () => { + const signInExperience = { + signInMode: SignInMode.SignIn, + }; + signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.Register) + ).rejects.toMatchError(new RequestError({ code: 'auth.forbidden', status: 403 })); + + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.SignIn) + ).resolves.not.toThrow(); + + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.ForgotPassword) + ).resolves.not.toThrow(); + }); + + it('SignInMode.SignInAndRegister', async () => { + const signInExperience = { + signInMode: SignInMode.SignInAndRegister, + }; + signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.Register) + ).resolves.not.toThrow(); + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.SignIn) + ).resolves.not.toThrow(); + await expect( + signInExperienceSettings.guardInteractionEvent(InteractionEvent.ForgotPassword) + ).resolves.not.toThrow(); + }); + }); + + describe('verifyIdentificationMethod (SignIn)', () => { + const signInVerificationTestCases: Record< + string, + { + signInExperience: SignInExperience; + cases: Array<{ verificationRecord: VerificationRecord; accepted: boolean }>; + } + > = Object.freeze({ + 'password enabled for all identifiers': { + signInExperience: mockSignInExperience, + cases: [ + { + verificationRecord: passwordVerificationRecords[SignInIdentifier.Username], + accepted: true, + }, + { + verificationRecord: passwordVerificationRecords[SignInIdentifier.Email], + accepted: true, + }, + { + verificationRecord: passwordVerificationRecords[SignInIdentifier.Phone], + accepted: true, + }, + ], + }, + 'password disabled for email and phone': { + signInExperience: { + ...mockSignInExperience, + signIn: { + methods: mockSignInExperience.signIn.methods.map((method) => + method.identifier === SignInIdentifier.Username + ? method + : { ...method, password: false } + ), + }, + }, + cases: [ + { + verificationRecord: passwordVerificationRecords[SignInIdentifier.Username], + accepted: true, + }, + { + verificationRecord: passwordVerificationRecords[SignInIdentifier.Email], + accepted: false, + }, + { + verificationRecord: passwordVerificationRecords[SignInIdentifier.Phone], + accepted: false, + }, + ], + }, + 'verification code enabled for email and phone': { + signInExperience: mockSignInExperience, + cases: [ + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email], + accepted: true, + }, + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone], + accepted: true, + }, + ], + }, + 'verification code disabled for email and phone': { + signInExperience: { + ...mockSignInExperience, + signIn: { + methods: mockSignInExperience.signIn.methods.map((method) => + method.identifier === SignInIdentifier.Username + ? method + : { ...method, verificationCode: false } + ), + }, + }, + cases: [ + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email], + accepted: false, + }, + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone], + accepted: false, + }, + ], + }, + 'no sign-in methods is enabled': { + signInExperience: { + ...mockSignInExperience, + signIn: { + methods: [], + }, + }, + cases: [ + { + verificationRecord: passwordVerificationRecords[SignInIdentifier.Username], + accepted: false, + }, + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email], + accepted: false, + }, + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone], + accepted: false, + }, + ], + }, + 'single sign-on enabled': { + signInExperience: { + ...mockSignInExperience, + singleSignOnEnabled: true, + }, + cases: [ + { + verificationRecord: enterpriseSsoVerificationRecords, + accepted: true, + }, + ], + }, + 'single sign-on disabled': { + signInExperience: { + ...mockSignInExperience, + singleSignOnEnabled: false, + }, + cases: [ + { + verificationRecord: enterpriseSsoVerificationRecords, + accepted: false, + }, + ], + }, + }); + + describe.each(Object.keys(signInVerificationTestCases))(`%s`, (testCase) => { + const { signInExperience, cases } = signInVerificationTestCases[testCase]!; + + it.each(cases)('guard verification record %p', async ({ verificationRecord, accepted }) => { + signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + + await (accepted + ? expect( + signInExperienceSettings.verifyIdentificationMethod( + InteractionEvent.SignIn, + verificationRecord + ) + ).resolves.not.toThrow() + : expect( + signInExperienceSettings.verifyIdentificationMethod( + InteractionEvent.SignIn, + verificationRecord + ) + ).rejects.toMatchError( + new RequestError({ code: 'user.sign_in_method_not_enabled', status: 422 }) + )); + }); + }); + }); + + describe('verifyIdentificationMethod (Register)', () => { + const registerVerificationTestCases: Record< + string, + { + signInExperience: SignInExperience; + cases: Array<{ verificationRecord: VerificationRecord; accepted: boolean }>; + } + > = Object.freeze({ + 'only username is enabled for sign-up': { + signInExperience: mockSignInExperience, + cases: [ + // TODO: username password registration + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email], + accepted: false, + }, + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone], + accepted: false, + }, + ], + }, + 'email is enabled for sign-up': { + signInExperience: { + ...mockSignInExperience, + signUp: { + identifiers: [SignInIdentifier.Email], + password: true, + verify: true, + }, + }, + cases: [ + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email], + accepted: true, + }, + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone], + accepted: false, + }, + ], + }, + 'email and phone are enabled for sign-up': { + signInExperience: { + ...mockSignInExperience, + signUp: { + identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone], + password: true, + verify: true, + }, + }, + cases: [ + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email], + accepted: true, + }, + { + verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone], + accepted: true, + }, + ], + }, + 'enterprise sso enabled': { + signInExperience: { + ...mockSignInExperience, + singleSignOnEnabled: true, + }, + cases: [ + { + verificationRecord: enterpriseSsoVerificationRecords, + accepted: true, + }, + ], + }, + 'enterprise sso disabled': { + signInExperience: { + ...mockSignInExperience, + singleSignOnEnabled: false, + }, + cases: [ + { + verificationRecord: enterpriseSsoVerificationRecords, + accepted: false, + }, + ], + }, + }); + + describe.each(Object.keys(registerVerificationTestCases))(`%s`, (testCase) => { + const { signInExperience, cases } = registerVerificationTestCases[testCase]!; + + it.each(cases)('guard verification record %p', async ({ verificationRecord, accepted }) => { + signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + + await (accepted + ? expect( + signInExperienceSettings.verifyIdentificationMethod( + InteractionEvent.Register, + verificationRecord + ) + ).resolves.not.toThrow() + : expect( + signInExperienceSettings.verifyIdentificationMethod( + InteractionEvent.Register, + verificationRecord + ) + ).rejects.toMatchError( + new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 }) + )); + }); + }); + }); + + describe('guardSsoOnlyEmailIdentifier: identifier with SSO enabled domain should throw', () => { + const mockSsoConnector = { + domains: [emailDomain], + }; + + const expectError = new RequestError( + { + code: 'session.sso_enabled', + status: 422, + }, + { + ssoConnectors: [mockSsoConnector], + } + ); + + it('email password verification record', async () => { + ssoConnectors.getAvailableSsoConnectors.mockResolvedValueOnce([mockSsoConnector]); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + + await expect( + signInExperienceSettings.verifyIdentificationMethod( + InteractionEvent.SignIn, + passwordVerificationRecords[SignInIdentifier.Email] + ) + ).rejects.toMatchError(expectError); + }); + + it('email verification code verification record', async () => { + ssoConnectors.getAvailableSsoConnectors.mockResolvedValueOnce([mockSsoConnector]); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + + await expect( + signInExperienceSettings.verifyIdentificationMethod( + InteractionEvent.SignIn, + verificationCodeVerificationRecords[SignInIdentifier.Email] + ) + ).rejects.toMatchError(expectError); + }); + + it('social verification record', async () => { + ssoConnectors.getAvailableSsoConnectors.mockResolvedValueOnce([mockSsoConnector]); + + const signInExperienceSettings = new SignInExperienceValidator( + mockTenant.libraries, + mockTenant.queries + ); + + await expect( + signInExperienceSettings.verifyIdentificationMethod( + InteractionEvent.SignIn, + socialVerificationRecord + ) + ).rejects.toMatchError(expectError); + }); + }); +}); +/* eslint-enable max-lines */ diff --git a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts new file mode 100644 index 000000000000..26f095f3a911 --- /dev/null +++ b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts @@ -0,0 +1,230 @@ +import { + InteractionEvent, + type SignInExperience, + SignInMode, + VerificationType, +} from '@logto/schemas'; + +import RequestError from '#src/errors/RequestError/index.js'; +import type Libraries from '#src/tenants/Libraries.js'; +import type Queries from '#src/tenants/Queries.js'; +import assertThat from '#src/utils/assert-that.js'; + +import { type VerificationRecord } from '../verifications/index.js'; + +const getEmailIdentifierFromVerificationRecord = (verificationRecord: VerificationRecord) => { + switch (verificationRecord.type) { + case VerificationType.Password: + case VerificationType.VerificationCode: { + const { + identifier: { type, value }, + } = verificationRecord; + + return type === 'email' ? value : undefined; + } + case VerificationType.Social: { + const { socialUserInfo } = verificationRecord; + return socialUserInfo?.email; + } + default: { + break; + } + } +}; + +/** + * SignInExperienceValidator class provides all the sign-in experience settings validation logic. + * + * - Guard the interaction event based on the sign-in experience settings + * - Guard the identification method based on the sign-in experience settings + * - Guard the email identifier with SSO enabled domains + */ +export class SignInExperienceValidator { + private signInExperienceDataCache?: SignInExperience; + + constructor( + private readonly libraries: Libraries, + private readonly queries: Queries + ) {} + + public async guardInteractionEvent(event: InteractionEvent) { + const { signInMode } = await this.getSignInExperienceData(); + + switch (event) { + case InteractionEvent.SignIn: { + assertThat( + signInMode !== SignInMode.Register, + new RequestError({ code: 'auth.forbidden', status: 403 }) + ); + break; + } + case InteractionEvent.Register: { + assertThat( + signInMode !== SignInMode.SignIn, + new RequestError({ code: 'auth.forbidden', status: 403 }) + ); + break; + } + case InteractionEvent.ForgotPassword: { + break; + } + } + } + + async verifyIdentificationMethod( + event: InteractionEvent, + verificationRecord: VerificationRecord + ) { + switch (event) { + case InteractionEvent.SignIn: { + await this.guardSignInVerificationMethod(verificationRecord); + break; + } + case InteractionEvent.Register: { + await this.guardRegisterVerificationMethod(verificationRecord); + break; + } + case InteractionEvent.ForgotPassword: { + this.guardForgotPasswordVerificationMethod(verificationRecord); + break; + } + } + + await this.guardSsoOnlyEmailIdentifier(verificationRecord); + } + + private async getSignInExperienceData() { + this.signInExperienceDataCache ||= + await this.queries.signInExperiences.findDefaultSignInExperience(); + + return this.signInExperienceDataCache; + } + + /** + * Guard the verification records contains email identifier with SSO enabled + * + * @remarks + * Email identifier with SSO enabled domain will be blocked. + * Can only verify/identify via SSO verification record. + * + * - VerificationCode with email identifier + * - Social userinfo with email + **/ + private async guardSsoOnlyEmailIdentifier(verificationRecord: VerificationRecord) { + const emailIdentifier = getEmailIdentifierFromVerificationRecord(verificationRecord); + + if (!emailIdentifier) { + return; + } + + const domain = emailIdentifier.split('@')[1]; + const { singleSignOnEnabled } = await this.getSignInExperienceData(); + + if (!singleSignOnEnabled || !domain) { + return; + } + + const { getAvailableSsoConnectors } = this.libraries.ssoConnectors; + const availableSsoConnectors = await getAvailableSsoConnectors(); + + const domainEnabledConnectors = availableSsoConnectors.filter(({ domains }) => + domains.includes(domain) + ); + + assertThat( + domainEnabledConnectors.length === 0, + new RequestError( + { + code: 'session.sso_enabled', + status: 422, + }, + { + ssoConnectors: domainEnabledConnectors, + } + ) + ); + } + + private async guardSignInVerificationMethod(verificationRecord: VerificationRecord) { + const { + signIn: { methods: signInMethods }, + singleSignOnEnabled, + } = await this.getSignInExperienceData(); + + switch (verificationRecord.type) { + case VerificationType.Password: + case VerificationType.VerificationCode: { + const { + identifier: { type }, + } = verificationRecord; + + assertThat( + signInMethods.some(({ identifier: method, password, verificationCode }) => { + return ( + method === type && + (verificationRecord.type === VerificationType.Password ? password : verificationCode) + ); + }), + new RequestError({ code: 'user.sign_in_method_not_enabled', status: 422 }) + ); + break; + } + + case VerificationType.Social: { + // No need to verify social verification method + break; + } + case VerificationType.EnterpriseSso: { + assertThat( + singleSignOnEnabled, + new RequestError({ code: 'user.sign_in_method_not_enabled', status: 422 }) + ); + break; + } + default: { + throw new RequestError({ code: 'user.sign_in_method_not_enabled', status: 422 }); + } + } + } + + private async guardRegisterVerificationMethod(verificationRecord: VerificationRecord) { + const { signUp, singleSignOnEnabled } = await this.getSignInExperienceData(); + + switch (verificationRecord.type) { + // TODO: username password registration + case VerificationType.VerificationCode: { + const { + identifier: { type }, + } = verificationRecord; + + assertThat( + signUp.identifiers.includes(type) && signUp.verify, + new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 }) + ); + break; + } + case VerificationType.Social: { + // No need to verify social verification method + break; + } + case VerificationType.EnterpriseSso: { + assertThat( + singleSignOnEnabled, + new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 }) + ); + break; + } + default: { + throw new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 }); + } + } + } + + /** Forgot password only supports verification code type verification record */ + private guardForgotPasswordVerificationMethod(verificationRecord: VerificationRecord) { + assertThat( + verificationRecord.type === VerificationType.VerificationCode, + new RequestError({ code: 'session.not_supported_for_forgot_password', status: 422 }) + ); + } +} diff --git a/packages/core/src/routes/experience/classes/verifications/code-verification.ts b/packages/core/src/routes/experience/classes/verifications/code-verification.ts index ccc8351af923..e87aceed9e1e 100644 --- a/packages/core/src/routes/experience/classes/verifications/code-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/code-verification.ts @@ -70,9 +70,8 @@ const getPasscodeIdentifierPayload = ( export class CodeVerification implements VerificationRecord { /** * Factory method to create a new CodeVerification record using the given identifier. - * The sendVerificationCode method will be automatically triggered. */ - static async create( + static create( libraries: Libraries, queries: Queries, identifier: VerificationCodeIdentifier, @@ -86,8 +85,6 @@ export class CodeVerification implements VerificationRecord( body: z.object({ interactionEvent: z.nativeEnum(InteractionEvent), }), - status: [204], + status: [204, 403], }), async (ctx, next) => { const { interactionEvent } = ctx.guard.body; @@ -61,7 +61,8 @@ export default function experienceApiRoutes( createLog(`Interaction.${interactionEvent}.Update`); const experienceInteraction = new ExperienceInteraction(ctx, tenant); - experienceInteraction.setInteractionEvent(interactionEvent); + + await experienceInteraction.setInteractionEvent(interactionEvent); await experienceInteraction.save(); @@ -78,7 +79,7 @@ export default function experienceApiRoutes( body: z.object({ interactionEvent: z.nativeEnum(InteractionEvent), }), - status: [204], + status: [204, 403], }), async (ctx, next) => { const { interactionEvent } = ctx.guard.body; @@ -88,7 +89,7 @@ export default function experienceApiRoutes( `Interaction.${experienceInteraction.interactionEvent ?? interactionEvent}.Update` ); - experienceInteraction.setInteractionEvent(interactionEvent); + await experienceInteraction.setInteractionEvent(interactionEvent); eventLog.append({ interactionEvent, @@ -106,16 +107,18 @@ export default function experienceApiRoutes( experienceRoutes.identification, koaGuard({ body: identificationApiPayloadGuard, - status: [204, 400, 401, 404], + status: [204, 400, 401, 404, 409], }), async (ctx, next) => { const { verificationId } = ctx.guard.body; + const { experienceInteraction } = ctx; - await ctx.experienceInteraction.identifyUser(verificationId); + await experienceInteraction.identifyUser(verificationId); - await ctx.experienceInteraction.save(); + await experienceInteraction.save(); - ctx.status = 204; + // Return 201 if a new user is created + ctx.status = experienceInteraction.interactionEvent === InteractionEvent.Register ? 201 : 204; return next(); } diff --git a/packages/core/src/routes/experience/verification-routes/backup-code-verification.ts b/packages/core/src/routes/experience/verification-routes/backup-code-verification.ts index 282197c1e7d3..b38b72cdb4e0 100644 --- a/packages/core/src/routes/experience/verification-routes/backup-code-verification.ts +++ b/packages/core/src/routes/experience/verification-routes/backup-code-verification.ts @@ -30,7 +30,7 @@ export default function backupCodeVerificationRoutes( const { experienceInteraction } = ctx; const { code } = ctx.guard.body; - assertThat(experienceInteraction.identifiedUserId, 'session.not_identified'); + assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found'); // TODO: Check if the MFA is enabled diff --git a/packages/core/src/routes/experience/verification-routes/totp-verification.ts b/packages/core/src/routes/experience/verification-routes/totp-verification.ts index d564a7bca58d..ef6a4925b002 100644 --- a/packages/core/src/routes/experience/verification-routes/totp-verification.ts +++ b/packages/core/src/routes/experience/verification-routes/totp-verification.ts @@ -31,7 +31,7 @@ export default function totpVerificationRoutes( async (ctx, next) => { const { experienceInteraction } = ctx; - assertThat(experienceInteraction.identifiedUserId, 'session.not_identified'); + assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found'); // TODO: Check if the MFA is enabled // TODO: Check if the interaction is fully verified @@ -71,7 +71,7 @@ export default function totpVerificationRoutes( const { experienceInteraction } = ctx; const { verificationId, code } = ctx.guard.body; - assertThat(experienceInteraction.identifiedUserId, 'session.not_identified'); + assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found'); // Verify new generated secret if (verificationId) { diff --git a/packages/core/src/routes/experience/verification-routes/verification-code.ts b/packages/core/src/routes/experience/verification-routes/verification-code.ts index 0ee92b68a818..1e99894b51c3 100644 --- a/packages/core/src/routes/experience/verification-routes/verification-code.ts +++ b/packages/core/src/routes/experience/verification-routes/verification-code.ts @@ -36,13 +36,15 @@ export default function verificationCodeRoutes( async (ctx, next) => { const { identifier, interactionEvent } = ctx.guard.body; - const codeVerification = await CodeVerification.create( + const codeVerification = CodeVerification.create( libraries, queries, identifier, interactionEvent ); + await codeVerification.sendVerificationCode(); + ctx.experienceInteraction.setVerificationRecord(codeVerification); await ctx.experienceInteraction.save(); diff --git a/packages/integration-tests/src/helpers/experience/index.ts b/packages/integration-tests/src/helpers/experience/index.ts index af3ebe3e660a..95082dda021d 100644 --- a/packages/integration-tests/src/helpers/experience/index.ts +++ b/packages/integration-tests/src/helpers/experience/index.ts @@ -4,7 +4,7 @@ import { InteractionEvent, - InteractionIdentifierType, + SignInIdentifier, type InteractionIdentifier, type VerificationCodeIdentifier, } from '@logto/schemas'; @@ -86,7 +86,7 @@ export const identifyUserWithUsernamePassword = async ( const { verificationId } = await client.verifyPassword({ identifier: { - type: InteractionIdentifierType.Username, + type: SignInIdentifier.Username, value: username, }, password, diff --git a/packages/integration-tests/src/tests/api/experience-api/interaction.test.ts b/packages/integration-tests/src/tests/api/experience-api/interaction.test.ts index 37be24186772..bdf0315dd824 100644 --- a/packages/integration-tests/src/tests/api/experience-api/interaction.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/interaction.test.ts @@ -1,4 +1,4 @@ -import { InteractionEvent, InteractionIdentifierType } from '@logto/schemas'; +import { InteractionEvent, SignInIdentifier } from '@logto/schemas'; import { initExperienceClient } from '#src/helpers/client.js'; import { expectRejects } from '#src/helpers/index.js'; @@ -19,7 +19,7 @@ devFeatureTest.describe('PUT /experience API', () => { const client = await initExperienceClient(); await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); const { verificationId } = await client.verifyPassword({ - identifier: { type: InteractionIdentifierType.Username, value: username }, + identifier: { type: SignInIdentifier.Username, value: username }, password, }); diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/password.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/password.test.ts index 0663ca7ffed0..fcf9a6093cbd 100644 --- a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/password.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/password.test.ts @@ -1,4 +1,4 @@ -import { InteractionIdentifierType } from '@logto/schemas'; +import { SignInIdentifier } from '@logto/schemas'; import { deleteUser } from '#src/api/admin-user.js'; import { signInWithPassword } from '#src/helpers/experience/index.js'; @@ -17,7 +17,7 @@ devFeatureTest.describe('sign-in with password verification happy path', () => { await enableAllPasswordSignInMethods(); }); - it.each(Object.values(InteractionIdentifierType))( + it.each(Object.values(SignInIdentifier))( 'should sign-in with password using %p', async (identifier) => { const { userProfile, user } = await generateNewUser({ diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts index d18eb8e5be6d..4deb166f167e 100644 --- a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts @@ -1,4 +1,4 @@ -import { InteractionIdentifierType } from '@logto/schemas'; +import { SignInIdentifier } from '@logto/schemas'; import { deleteUser } from '#src/api/admin-user.js'; import { setEmailConnector, setSmsConnector } from '#src/helpers/connector.js'; @@ -7,10 +7,8 @@ import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-exp import { generateNewUser } from '#src/helpers/user.js'; import { devFeatureTest } from '#src/utils.js'; -const verificationIdentifierType: readonly [ - InteractionIdentifierType.Email, - InteractionIdentifierType.Phone, -] = Object.freeze([InteractionIdentifierType.Email, InteractionIdentifierType.Phone]); +const verificationIdentifierType: readonly [SignInIdentifier.Email, SignInIdentifier.Phone] = + Object.freeze([SignInIdentifier.Email, SignInIdentifier.Phone]); const identifiersTypeToUserProfile = Object.freeze({ email: 'primaryEmail', diff --git a/packages/integration-tests/src/tests/api/experience-api/verifications/backup-code-verification.test.ts b/packages/integration-tests/src/tests/api/experience-api/verifications/backup-code-verification.test.ts index d5148b7e5be6..206c285fde3d 100644 --- a/packages/integration-tests/src/tests/api/experience-api/verifications/backup-code-verification.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/verifications/backup-code-verification.test.ts @@ -25,7 +25,7 @@ devFeatureTest.describe('backup code verification APIs', () => { const client = await initExperienceClient(); await expectRejects(client.verifyBackupCode({ code: '1234' }), { - code: 'session.not_identified', + code: 'session.identifier_not_found', status: 400, }); }); diff --git a/packages/integration-tests/src/tests/api/experience-api/verifications/password-verification.test.ts b/packages/integration-tests/src/tests/api/experience-api/verifications/password-verification.test.ts index 372a80a0fc75..f2c8c9cba23b 100644 --- a/packages/integration-tests/src/tests/api/experience-api/verifications/password-verification.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/verifications/password-verification.test.ts @@ -1,4 +1,4 @@ -import { InteractionIdentifierType } from '@logto/schemas'; +import { SignInIdentifier } from '@logto/schemas'; import { deleteUser } from '#src/api/admin-user.js'; import { initExperienceClient } from '#src/helpers/client.js'; @@ -12,7 +12,7 @@ const identifiersTypeToUserProfile = Object.freeze({ }); devFeatureTest.describe('password verifications', () => { - it.each(Object.values(InteractionIdentifierType))( + it.each(Object.values(SignInIdentifier))( 'should verify with password successfully using %p', async (identifier) => { const { userProfile, user } = await generateNewUser({ diff --git a/packages/integration-tests/src/tests/api/experience-api/verifications/social-verification.test.ts b/packages/integration-tests/src/tests/api/experience-api/verifications/social-verification.test.ts index 6962924e3d94..6350cdcd34da 100644 --- a/packages/integration-tests/src/tests/api/experience-api/verifications/social-verification.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/verifications/social-verification.test.ts @@ -1,5 +1,5 @@ import { ConnectorType } from '@logto/connector-kit'; -import { InteractionEvent, InteractionIdentifierType } from '@logto/schemas'; +import { InteractionEvent, SignInIdentifier } from '@logto/schemas'; import { mockEmailConnectorId, mockSocialConnectorId } from '#src/__mocks__/connectors-mock.js'; import { initExperienceClient } from '#src/helpers/client.js'; @@ -134,7 +134,7 @@ devFeatureTest.describe('social verification', () => { const { verificationId } = await client.sendVerificationCode({ identifier: { - type: InteractionIdentifierType.Email, + type: SignInIdentifier.Email, value: 'foo', }, interactionEvent: InteractionEvent.SignIn, diff --git a/packages/integration-tests/src/tests/api/experience-api/verifications/totp-verification.test.ts b/packages/integration-tests/src/tests/api/experience-api/verifications/totp-verification.test.ts index 7e77e99ea457..c1fa2a76b38a 100644 --- a/packages/integration-tests/src/tests/api/experience-api/verifications/totp-verification.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/verifications/totp-verification.test.ts @@ -31,7 +31,7 @@ devFeatureTest.describe('TOTP verification APIs', () => { const client = await initExperienceClient(); await expectRejects(client.createTotpSecret(), { - code: 'session.not_identified', + code: 'session.identifier_not_found', status: 400, }); }); @@ -50,7 +50,7 @@ devFeatureTest.describe('TOTP verification APIs', () => { const client = await initExperienceClient(); await expectRejects(client.verifyTotp({ code: '1234' }), { - code: 'session.not_identified', + code: 'session.identifier_not_found', status: 400, }); }); @@ -107,7 +107,7 @@ devFeatureTest.describe('TOTP verification APIs', () => { const client = await initExperienceClient(); await expectRejects(client.verifyTotp({ code: '1234' }), { - code: 'session.not_identified', + code: 'session.identifier_not_found', status: 400, }); }); diff --git a/packages/integration-tests/src/tests/api/experience-api/verifications/verification-code.test.ts b/packages/integration-tests/src/tests/api/experience-api/verifications/verification-code.test.ts index 169d82f81302..66dafe72b09c 100644 --- a/packages/integration-tests/src/tests/api/experience-api/verifications/verification-code.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/verifications/verification-code.test.ts @@ -1,7 +1,7 @@ import { ConnectorType } from '@logto/connector-kit'; import { InteractionEvent, - InteractionIdentifierType, + SignInIdentifier, type VerificationCodeIdentifier, } from '@logto/schemas'; @@ -25,11 +25,11 @@ devFeatureTest.describe('Verification code verification APIs', () => { const identifiers: VerificationCodeIdentifier[] = [ { - type: InteractionIdentifierType.Email, + type: SignInIdentifier.Email, value: 'foo@logto.io', }, { - type: InteractionIdentifierType.Phone, + type: SignInIdentifier.Phone, value: '+1234567890', }, ]; diff --git a/packages/phrases/src/locales/en/errors/session.ts b/packages/phrases/src/locales/en/errors/session.ts index 34c58ac1f083..ab6e00e11896 100644 --- a/packages/phrases/src/locales/en/errors/session.ts +++ b/packages/phrases/src/locales/en/errors/session.ts @@ -23,7 +23,6 @@ const session = { interaction_not_found: 'Interaction session not found. Please go back and start the session again.', not_supported_for_forgot_password: 'This operation is not supported for forgot password.', - not_identified: 'User not identified. Please sign in first.', identity_conflict: 'Identity mismatch detected. Please initiate a new session to proceed with a different identity.', mfa: { diff --git a/packages/schemas/src/types/interactions.ts b/packages/schemas/src/types/interactions.ts index 877974224726..82b77965b5ba 100644 --- a/packages/schemas/src/types/interactions.ts +++ b/packages/schemas/src/types/interactions.ts @@ -1,7 +1,12 @@ import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit'; import { z } from 'zod'; -import { MfaFactor, jsonObjectGuard, webAuthnTransportGuard } from '../foundations/index.js'; +import { + MfaFactor, + SignInIdentifier, + jsonObjectGuard, + webAuthnTransportGuard, +} from '../foundations/index.js'; import { type ToZodObject } from '../utils/zod.js'; import type { @@ -24,31 +29,26 @@ export enum InteractionEvent { } // ====== Experience API payload guards and type definitions start ====== -export enum InteractionIdentifierType { - Username = 'username', - Email = 'email', - Phone = 'phone', -} /** Identifiers that can be used to uniquely identify a user. */ export type InteractionIdentifier = { - type: InteractionIdentifierType; + type: SignInIdentifier; value: string; }; export const interactionIdentifierGuard = z.object({ - type: z.nativeEnum(InteractionIdentifierType), + type: z.nativeEnum(SignInIdentifier), value: z.string(), }) satisfies ToZodObject; /** Currently only email and phone are supported for verification code validation. */ export type VerificationCodeIdentifier = { - type: InteractionIdentifierType.Email | InteractionIdentifierType.Phone; + type: SignInIdentifier.Email | SignInIdentifier.Phone; value: string; }; export const verificationCodeIdentifierGuard = z.object({ - type: z.enum([InteractionIdentifierType.Email, InteractionIdentifierType.Phone]), + type: z.enum([SignInIdentifier.Email, SignInIdentifier.Phone]), value: z.string(), }) satisfies ToZodObject; From c637fcb2c292b2a3e8251f3fc31bef0763f6fc03 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 15 Jul 2024 11:43:48 +0800 Subject: [PATCH 016/135] feat: init elements --- packages/console/package.json | 1 + packages/console/src/pages/Profile/index.tsx | 114 ++-- packages/elements/README.md | 3 + packages/elements/package.json | 74 +++ .../elements/src/components/logto-card.ts | 23 + .../src/components/logto-form-card.ts | 55 ++ .../src/components/logto-theme-provider.ts | 22 + packages/elements/src/index.ts | 3 + packages/elements/src/locales/en.ts | 9 + packages/elements/src/locales/index.ts | 40 ++ packages/elements/src/react.ts | 23 + packages/elements/src/utils/css.ts | 37 ++ packages/elements/src/utils/string.ts | 19 + packages/elements/src/utils/theme.ts | 123 +++++ packages/elements/tsconfig.json | 12 + packages/elements/tsup.config.ts | 8 + pnpm-lock.yaml | 507 +++++++++++++----- 17 files changed, 897 insertions(+), 176 deletions(-) create mode 100644 packages/elements/README.md create mode 100644 packages/elements/package.json create mode 100644 packages/elements/src/components/logto-card.ts create mode 100644 packages/elements/src/components/logto-form-card.ts create mode 100644 packages/elements/src/components/logto-theme-provider.ts create mode 100644 packages/elements/src/index.ts create mode 100644 packages/elements/src/locales/en.ts create mode 100644 packages/elements/src/locales/index.ts create mode 100644 packages/elements/src/react.ts create mode 100644 packages/elements/src/utils/css.ts create mode 100644 packages/elements/src/utils/string.ts create mode 100644 packages/elements/src/utils/theme.ts create mode 100644 packages/elements/tsconfig.json create mode 100644 packages/elements/tsup.config.ts diff --git a/packages/console/package.json b/packages/console/package.json index 83eed75a5be5..a93c6f63e17d 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -30,6 +30,7 @@ "@logto/cloud": "0.2.5-a7eedce", "@logto/connector-kit": "workspace:^4.0.0", "@logto/core-kit": "workspace:^2.5.0", + "@logto/elements": "workspace:^0.0.0", "@logto/language-kit": "workspace:^1.1.0", "@logto/phrases": "workspace:^1.12.0", "@logto/phrases-experience": "workspace:^1.7.0", diff --git a/packages/console/src/pages/Profile/index.tsx b/packages/console/src/pages/Profile/index.tsx index cd8c680faef0..3560bab4cb9a 100644 --- a/packages/console/src/pages/Profile/index.tsx +++ b/packages/console/src/pages/Profile/index.tsx @@ -1,5 +1,6 @@ +import { createReactComponents } from '@logto/elements/react'; import type { ConnectorResponse } from '@logto/schemas'; -import { useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useRoutes } from 'react-router-dom'; import useSWRImmutable from 'swr/immutable'; @@ -8,7 +9,7 @@ import FormCard from '@/components/FormCard'; import PageMeta from '@/components/PageMeta'; import Topbar from '@/components/Topbar'; import { adminTenantEndpoint, meApi } from '@/consts'; -import { isCloud } from '@/consts/env'; +import { isCloud, isDevFeaturesEnabled } from '@/consts/env'; import AppBoundary from '@/containers/AppBoundary'; import Button from '@/ds-components/Button'; import CardTitle from '@/ds-components/CardTitle'; @@ -31,6 +32,8 @@ import Skeleton from './components/Skeleton'; import DeleteAccountModal from './containers/DeleteAccountModal'; import * as styles from './index.module.scss'; +const { LogtoFormCard, LogtoThemeProvider } = createReactComponents(React); + function Profile() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { navigate } = useTenantPathname(); @@ -62,59 +65,66 @@ function Profile() { return ( -
- - -
- -
- -
- {showLoadingSkeleton && } - {user && !showLoadingSkeleton && ( -
- - {isCloud && ( - - )} - - (value ? ******** : ), - action: { - name: 'profile.change', - handler: () => { - navigate(user.hasPassword ? 'verify-password' : 'change-password', { - state: { email: user.primaryEmail, action: 'changePassword' }, - }); + +
+ + +
+ +
+ +
+ {showLoadingSkeleton && } + {isDevFeaturesEnabled && ( + +

🚧 This section is a dev feature that is still working in progress.

+
+ )} + {user && !showLoadingSkeleton && ( +
+ + {isCloud && ( + + )} + + (value ? ******** : ), + action: { + name: 'profile.change', + handler: () => { + navigate(user.hasPassword ? 'verify-password' : 'change-password', { + state: { email: user.primaryEmail, action: 'changePassword' }, + }); + }, }, }, - }, - ]} - /> - - {isCloud && ( - -
-
- {t('profile.delete_account.description')} -
-
- + ]} + />
- )} -
- )} -
-
- {childrenRoutes} -
+ {isCloud && ( + +
+
+ {t('profile.delete_account.description')} +
+
+ +
+ )} +
+ )} +
+
+ {childrenRoutes} +
+
); } diff --git a/packages/elements/README.md b/packages/elements/README.md new file mode 100644 index 000000000000..3a7ddcdad42a --- /dev/null +++ b/packages/elements/README.md @@ -0,0 +1,3 @@ +# Logto elements + +🚧 Work in progress diff --git a/packages/elements/package.json b/packages/elements/package.json new file mode 100644 index 000000000000..3ac8d3e2dbab --- /dev/null +++ b/packages/elements/package.json @@ -0,0 +1,74 @@ +{ + "name": "@logto/elements", + "version": "0.0.0", + "description": "Logto user interface elements.", + "author": "Silverhand Inc. ", + "homepage": "https://github.com/logto-io/logto#readme", + "license": "MPL-2.0", + "type": "module", + "private": true, + "main": "dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./react": { + "import": "./dist/react.js", + "types": "./dist/react.d.ts" + } + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/logto-io/logto.git" + }, + "scripts": { + "precommit": "lint-staged", + "build": "tsup", + "dev": "tsup --watch --no-splitting", + "lint": "eslint --ext .ts src", + "lint:report": "pnpm lint --format json --output-file report.json", + "test": "echo \"No tests yet.\"", + "test:ci": "pnpm run test --silent --coverage", + "prepack": "pnpm build" + }, + "engines": { + "node": "^20.9.0" + }, + "bugs": { + "url": "https://github.com/logto-io/logto/issues" + }, + "dependencies": { + "@lit/localize": "^0.12.1", + "@lit/react": "^1.0.5", + "@silverhand/essentials": "^2.9.1", + "lit": "^3.1.4" + }, + "devDependencies": { + "@silverhand/eslint-config": "6.0.1", + "@silverhand/ts-config": "6.0.0", + "eslint": "^8.56.0", + "lint-staged": "^15.0.0", + "prettier": "^3.0.0", + "tsup": "^8.1.0" + }, + "eslintConfig": { + "extends": "@silverhand", + "rules": { + "no-console": "error", + "unicorn/prevent-abbreviations": [ + "error", + { + "replacements": { + "var": false, + "vars": false + } + } + ] + } + }, + "prettier": "@silverhand/eslint-config/.prettierrc" +} diff --git a/packages/elements/src/components/logto-card.ts b/packages/elements/src/components/logto-card.ts new file mode 100644 index 000000000000..a6bde4ad6878 --- /dev/null +++ b/packages/elements/src/components/logto-card.ts @@ -0,0 +1,23 @@ +import { LitElement, css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import { unit } from '../utils/css.js'; +import { vars } from '../utils/theme.js'; + +const tagName = 'logto-card'; + +@customElement(tagName) +export class LogtoCard extends LitElement { + static tagName = tagName; + static styles = css` + :host { + background: ${vars.colorLayer1}; + border-radius: ${unit(4)}; + padding: ${unit(6)}; + } + `; + + render() { + return html``; + } +} diff --git a/packages/elements/src/components/logto-form-card.ts b/packages/elements/src/components/logto-form-card.ts new file mode 100644 index 000000000000..4b371b980243 --- /dev/null +++ b/packages/elements/src/components/logto-form-card.ts @@ -0,0 +1,55 @@ +import { cond } from '@silverhand/essentials'; +import { LitElement, css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { type LocaleKeyOptional, type LocaleKey } from '../locales/index.js'; +import { unit } from '../utils/css.js'; +import { vars } from '../utils/theme.js'; + +const tagName = 'logto-form-card'; + +@customElement(tagName) +export class LogtoFormCard extends LitElement { + static tagName = tagName; + static styles = css` + logto-card { + display: flex; + padding: ${unit(6, 8)}; + } + + header { + flex: 7; + font: ${vars.fontSectionHeading1}; + color: ${vars.colorCardTitle}; + letter-spacing: 0.1em; + text-transform: uppercase; + } + + div.spacer { + flex: 1; + } + + ::slotted(*) { + flex: 16; + } + `; + + @property() + title: LocaleKey = 'placeholders.not_available'; + + @property() + description: LocaleKeyOptional = ''; + + render() { + return html` + +
+
${this.title}
+ ${cond(this.description && html`

${this.description}

`)} +
+
+ +
+ `; + } +} diff --git a/packages/elements/src/components/logto-theme-provider.ts b/packages/elements/src/components/logto-theme-provider.ts new file mode 100644 index 000000000000..988c6dd26fca --- /dev/null +++ b/packages/elements/src/components/logto-theme-provider.ts @@ -0,0 +1,22 @@ +import { LitElement, css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { defaultTheme, darkTheme, toLitCss } from '../utils/theme.js'; + +const tagName = 'logto-theme-provider'; + +@customElement(tagName) +export class LogtoThemeProvider extends LitElement { + static tagName = tagName; + static styles = css` + ${toLitCss(defaultTheme)} + ${toLitCss(darkTheme, 'dark')} + `; + + @property({ reflect: true }) + theme: 'default' | 'dark' = 'default'; + + render() { + return html``; + } +} diff --git a/packages/elements/src/index.ts b/packages/elements/src/index.ts new file mode 100644 index 000000000000..d4cb36992705 --- /dev/null +++ b/packages/elements/src/index.ts @@ -0,0 +1,3 @@ +export * from './components/logto-card.js'; +export * from './components/logto-form-card.js'; +export * from './components/logto-theme-provider.js'; diff --git a/packages/elements/src/locales/en.ts b/packages/elements/src/locales/en.ts new file mode 100644 index 000000000000..f8998af8bc90 --- /dev/null +++ b/packages/elements/src/locales/en.ts @@ -0,0 +1,9 @@ +export const en = Object.freeze({ + placeholders: { + not_available: 'Not available', + }, + sections: { + profile: 'Profile', + security: 'Security', + }, +}); diff --git a/packages/elements/src/locales/index.ts b/packages/elements/src/locales/index.ts new file mode 100644 index 000000000000..bfce23d867f2 --- /dev/null +++ b/packages/elements/src/locales/index.ts @@ -0,0 +1,40 @@ +import { type en } from './en.js'; + +type KeyPath = T extends Record + ? { + [K in keyof T]: K extends string + ? T[K] extends Record + ? `${K}.${KeyPath}` + : `${K}` + : never; + }[keyof T] + : ''; + +/** The type of a full locale data object. */ +export type LocaleData = typeof en; + +/** + * The type of a locale key. It is a string that represents a path to a value in the locale data + * object. + * + * @example + * With the following locale data object: + * + * ```ts + * const en = { + * profile: { + * title: 'Profile', + * description: 'Your profile', + * }, + * }; + * ``` + * + * The locale key for the title would be `'profile.title'`. + */ +export type LocaleKey = KeyPath; + +/** + * The type of a locale key that is optional. Note that it uses an empty string to represent the + * absence of a key since Web Components do not support `undefined` as a property value. + */ +export type LocaleKeyOptional = LocaleKey | ''; diff --git a/packages/elements/src/react.ts b/packages/elements/src/react.ts new file mode 100644 index 000000000000..1a3df2b52934 --- /dev/null +++ b/packages/elements/src/react.ts @@ -0,0 +1,23 @@ +import { createComponent } from '@lit/react'; + +import { LogtoThemeProvider, LogtoCard, LogtoFormCard } from './index.js'; + +export const createReactComponents = (react: Parameters[0]['react']) => { + return { + LogtoFormCard: createComponent({ + tagName: LogtoFormCard.tagName, + elementClass: LogtoFormCard, + react, + }), + LogtoCard: createComponent({ + tagName: LogtoCard.tagName, + elementClass: LogtoCard, + react, + }), + LogtoThemeProvider: createComponent({ + tagName: LogtoThemeProvider.tagName, + elementClass: LogtoThemeProvider, + react, + }), + }; +}; diff --git a/packages/elements/src/utils/css.ts b/packages/elements/src/utils/css.ts new file mode 100644 index 000000000000..6746bf2d39c4 --- /dev/null +++ b/packages/elements/src/utils/css.ts @@ -0,0 +1,37 @@ +import { type CSSResult, unsafeCSS } from 'lit'; + +type Unit = { + /** + * @example unit(1) // '4px' + */ + (value: number): CSSResult; + /** + * @example unit(1, 2) // '4px 8px' + */ + (value1: number, value2: number): CSSResult; + /** + * @example unit(1, 2, 3) // '4px 8px 12px' + */ + // eslint-disable-next-line @typescript-eslint/unified-signatures -- for better readability + (value1: number, value2: number, value3: number): CSSResult; + /** + * @example unit(1, 2, 3, 4) // '4px 8px 12px 16px' + */ + // eslint-disable-next-line @typescript-eslint/unified-signatures -- for better readability + (value1: number, value2: number, value3: number, value4: number): CSSResult; +}; + +/** + * Returns a `CSSResult` that represents the given values in pixels. The values are multiplied by 4. + */ +export const unit: Unit = (...values: number[]) => { + if (values.length === 0 || values.length > 4) { + throw new Error('unit() accepts 1 to 4 arguments'); + } + + if (values.some((value) => typeof value !== 'number')) { + throw new Error('unit() accepts only numbers'); + } + + return unsafeCSS(values.map((value) => `${value * 4}px`).join(' ')); +}; diff --git a/packages/elements/src/utils/string.ts b/packages/elements/src/utils/string.ts new file mode 100644 index 000000000000..64cfc71260da --- /dev/null +++ b/packages/elements/src/utils/string.ts @@ -0,0 +1,19 @@ +export type KebabCase = T extends `${infer L}${infer M}${infer R}` + ? L extends Lowercase + ? M extends Lowercase + ? `${L}${KebabCase<`${M}${R}`>}` + : `${L}-${KebabCase<`${M}${R}`>}` + : M extends Lowercase + ? `${Lowercase}${KebabCase<`${M}${R}`>}` + : R extends Uncapitalize + ? `${Lowercase}-${Lowercase}${KebabCase}` + : `${Lowercase}${KebabCase<`${M}${R}`>}` + : T; + +export const kebabCase = (value: T): KebabCase => { + // eslint-disable-next-line no-restricted-syntax -- `as` assertion is needed to make TS happy + return value + .replaceAll(/([^A-Z])([A-Z])/g, '$1-$2') + .replaceAll(/([A-Z])([A-Z][^A-Z])/g, '$1-$2') + .toLowerCase() as KebabCase; +}; diff --git a/packages/elements/src/utils/theme.ts b/packages/elements/src/utils/theme.ts new file mode 100644 index 000000000000..8eb806d1d7f2 --- /dev/null +++ b/packages/elements/src/utils/theme.ts @@ -0,0 +1,123 @@ +import { type CSSResult, unsafeCSS } from 'lit'; + +import { type KebabCase, kebabCase } from './string.js'; + +/** All the colors to be used in the Logto components and elements. */ +export type Color = { + colorPrimary: string; + colorText: string; + colorTextLink: string; + colorTextSecondary: string; + colorBorder: string; + colorCardTitle: string; + colorLayer1: string; + colorLayer2: string; +}; + +/** All the fonts to be used in the Logto components and elements. */ +export type Font = { + fontLabel1: string; + fontLabel2: string; + fontLabel3: string; + fontSectionHeading1: string; + fontSectionHeading2: string; +}; + +/** The complete styling properties to be used in the Logto components and elements. */ +export type Theme = Color & Font; + +export const defaultFontFamily = + '-apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji'; + +export const defaultFont: Readonly = Object.freeze({ + fontLabel1: `500 16px / 24px ${defaultFontFamily}`, + fontLabel2: `500 14px / 20px ${defaultFontFamily}`, + fontLabel3: `500 12px / 16px ${defaultFontFamily}`, + fontSectionHeading1: `700 12px / 16px ${defaultFontFamily}`, + fontSectionHeading2: `700 10px / 16px ${defaultFontFamily}`, +}); + +export const defaultTheme: Readonly = Object.freeze({ + ...defaultFont, + colorPrimary: '#5d34f2', + colorText: '#191c1d', + colorTextLink: '#5d34f2', + colorTextSecondary: '#747778', + colorBorder: '#c4c7c7', + colorCardTitle: '#928f9a', + colorLayer1: '#000', + colorLayer2: '#2d3132', +}); + +export const darkTheme: Readonly = Object.freeze({ + ...defaultFont, + colorPrimary: '#7958ff', + colorText: '#f7f8f8', + colorTextLink: '#cabeff', + colorTextSecondary: '#a9acac', + colorBorder: '#5c5f60', + colorCardTitle: '#928f9a', + colorLayer1: '#2a2c32', + colorLayer2: '#34353f', +}); + +/** + * Converts the theme object to a list of CSS custom properties entries. Each key is prefixed + * with `--logto-`. + * + * @example + * toLogtoCssEntries(defaultTheme) // [['--logto-color-primary', '#5d34f2'], ...] + */ +export const toLogtoCssEntries = (theme: Theme) => + Object.entries(theme).map(([key, value]) => + Object.freeze([`--logto-${kebabCase(key)}`, value] as const) + ); + +export type ToLogtoCssProperties> = { + [K in keyof T as K extends string ? `--logto-${KebabCase}` : never]: T[K]; +}; + +/** + * Converts the theme object to a map of CSS custom properties. Each key is prefixed with + * `--logto-`. + * + * @example + * toLogtoCssProperties(defaultTheme) // { '--logto-color-primary': '#5d34f2', ... } + */ +export const toLogtoCssProperties = (theme: Theme): ToLogtoCssProperties => { + // eslint-disable-next-line no-restricted-syntax -- `Object.fromEntries` will lose the type + return Object.fromEntries(toLogtoCssEntries(theme)) as ToLogtoCssProperties; +}; + +/** + * Converts the given value to a logto CSS custom property prefixed with `--logto-`. + * + * @example + * toVar('colorPrimary') // '--logto-color-primary' in `CSSResult` + */ +export const toVar = (value: string) => unsafeCSS(`var(--logto-${kebabCase(value)})`); + +/** + * The CSS custom properties in `CSSResult` format for a theme object. You can use this object + * to apply a custom property from the theme. + * + * @example + * css` + * p { + * color: ${vars.colorPrimary}; + * } + * ` + */ +// eslint-disable-next-line no-restricted-syntax -- `Object.fromEntries` will lose the type +export const vars = Object.freeze( + Object.fromEntries(Object.keys(defaultTheme).map((key) => [key, toVar(key)])) +) as Record; + +export const toLitCss = (theme: Theme, name?: string) => + unsafeCSS( + `:host${typeof name === 'string' ? `([theme=${name}])` : ''} {\n` + + toLogtoCssEntries(theme) + .map(([key, value]) => `${key}: ${value};`) + .join('\n') + + '\n}' + ); diff --git a/packages/elements/tsconfig.json b/packages/elements/tsconfig.json new file mode 100644 index 000000000000..8c6e9bf2b8ca --- /dev/null +++ b/packages/elements/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@silverhand/ts-config/tsconfig.base", + "compilerOptions": { + "experimentalDecorators": true, + "useDefineForClassFields": false, + "noEmit": true + }, + "include": [ + "src", + "*.config.ts" + ] +} diff --git a/packages/elements/tsup.config.ts b/packages/elements/tsup.config.ts new file mode 100644 index 000000000000..6bbc8ee650bb --- /dev/null +++ b/packages/elements/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts', 'src/react.ts'], + format: 'esm', + dts: true, + clean: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43088bf0a603..90e736f82dc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2866,6 +2866,9 @@ importers: '@logto/core-kit': specifier: workspace:^2.5.0 version: link:../toolkit/core-kit + '@logto/elements': + specifier: workspace:^0.0.0 + version: link:../elements '@logto/language-kit': specifier: workspace:^1.1.0 version: link:../toolkit/language-kit @@ -3141,7 +3144,7 @@ importers: version: 2.2.0(react@18.2.0) ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) + version: 10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3) tslib: specifier: ^2.4.1 version: 2.4.1 @@ -3549,6 +3552,40 @@ importers: specifier: ^3.23.8 version: 3.23.8 + packages/elements: + dependencies: + '@lit/localize': + specifier: ^0.12.1 + version: 0.12.1 + '@lit/react': + specifier: ^1.0.5 + version: 1.0.5(@types/react@18.0.31) + '@silverhand/essentials': + specifier: ^2.9.1 + version: 2.9.1 + lit: + specifier: ^3.1.4 + version: 3.1.4 + devDependencies: + '@silverhand/eslint-config': + specifier: 6.0.1 + version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.3.3) + '@silverhand/ts-config': + specifier: 6.0.0 + version: 6.0.0(typescript@5.3.3) + eslint: + specifier: ^8.56.0 + version: 8.57.0 + lint-staged: + specifier: ^15.0.0 + version: 15.0.2 + prettier: + specifier: ^3.0.0 + version: 3.0.0 + tsup: + specifier: ^8.1.0 + version: 8.1.0(@swc/core@1.3.52)(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3))(typescript@5.3.3) + packages/experience: devDependencies: '@jest/types': @@ -4816,140 +4853,140 @@ packages: peerDependencies: postcss-selector-parser: ^6.0.13 - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -5177,6 +5214,20 @@ packages: '@lezer/lr@0.15.8': resolution: {integrity: sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg==} + '@lit-labs/ssr-dom-shim@1.2.0': + resolution: {integrity: sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==} + + '@lit/localize@0.12.1': + resolution: {integrity: sha512-uuF6OO6fjqomCf3jXsJ5cTGf1APYuN88S4Gvo/fjt9YkG4OMaMvpEUqd5oWhyzrJfY+HcenAbLJNi2Cq3H7gdg==} + + '@lit/react@1.0.5': + resolution: {integrity: sha512-RSHhrcuSMa4vzhqiTenzXvtQ6QDq3hSPsnHHO3jaPmmvVFeoNNm4DHoQ0zLdKAUvY3wP3tTENSUf7xpyVfrDEA==} + peerDependencies: + '@types/react': 17 || 18 + + '@lit/reactive-element@2.0.4': + resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==} + '@lmdb/lmdb-darwin-arm64@2.7.11': resolution: {integrity: sha512-r6+vYq2vKzE+vgj/rNVRMwAevq0+ZR9IeMFIqcSga+wMtMdXQ27KqQ7uS99/yXASg29bos7yHP3yk4x6Iio0lw==} cpu: [arm64] @@ -6700,6 +6751,9 @@ packages: '@types/tough-cookie@4.0.2': resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/tunnel@0.0.3': resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==} @@ -6944,6 +6998,9 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -7217,6 +7274,12 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + bundle-require@4.2.1: + resolution: {integrity: sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + bytes@3.1.1: resolution: {integrity: sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==} engines: {node: '>= 0.8'} @@ -7465,6 +7528,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} @@ -8234,8 +8301,8 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true @@ -9793,6 +9860,10 @@ packages: jose@5.6.3: resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-base64@3.7.5: resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} @@ -10063,6 +10134,10 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -10075,10 +10150,23 @@ packages: resolution: {integrity: sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==} engines: {node: '>=16.0.0'} + lit-element@4.0.6: + resolution: {integrity: sha512-U4sdJ3CSQip7sLGZ/uJskO5hGiqtlpxndsLr6mt3IQIjheg93UKYeGQjWMRql1s/cXNOaRrCzC2FQwjIwSUqkg==} + + lit-html@3.1.4: + resolution: {integrity: sha512-yKKO2uVv7zYFHlWMfZmqc+4hkmSbFp8jgjdZY9vvR9jr4J8fH6FUMXhr+ljfELgmjpvlF7Z1SJ5n5/Jeqtc9YA==} + + lit@3.1.4: + resolution: {integrity: sha512-q6qKnKXHy2g1kjBaNfcoLlgbI3+aSOZ9Q4tiGa9bGYXq5RBXxkVTqTIVmP2VWMp29L4GyvCFm8ZQ2o56eUAMyA==} + lmdb@2.7.11: resolution: {integrity: sha512-x9bD4hVp7PFLUoELL8RglbNXhAMt5CYhkmss+CEau9KlNoilsTzNi9QDsPZb3KMpOGZXG6jmXhW3bBxE2XVztw==} hasBin: true + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -10655,6 +10743,9 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -11222,6 +11313,18 @@ packages: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + postcss-media-query-parser@0.2.3: resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} @@ -11296,6 +11399,10 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.4.39: + resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -12385,6 +12492,11 @@ packages: stylis@4.3.2: resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + superagent@9.0.1: resolution: {integrity: sha512-CcRSdb/P2oUVaEpQ87w9Obsl+E9FruRd6b2b7LdiBtJoyMr2DQt7a89anAfiX/EL59j9b2CbRFvf2S91DhuCww==} engines: {node: '>=14.18.0'} @@ -12481,6 +12593,13 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thirty-two@1.0.2: resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} engines: {node: '>=0.2.6'} @@ -12576,6 +12695,9 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -12609,6 +12731,25 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} + tsup@8.1.0: + resolution: {integrity: sha512-UFdfCAXukax+U6KzeTNO2kAARHcWxmKsnvSPXUcfA1D+kU05XDccCrkffCQpFaWDsZfV0jMyTsxU39VfCp6EOg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tty-table@4.1.6: resolution: {integrity: sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==} engines: {node: '>=8.0.0'} @@ -12830,8 +12971,8 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite@5.2.9: - resolution: {integrity: sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==} + vite@5.3.3: + resolution: {integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -13078,6 +13219,11 @@ packages: resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==} engines: {node: '>= 14'} + yaml@2.4.5: + resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} + engines: {node: '>= 14'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -14357,73 +14503,73 @@ snapshots: dependencies: postcss-selector-parser: 6.0.16 - '@esbuild/aix-ppc64@0.20.2': + '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/android-arm64@0.20.2': + '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm@0.20.2': + '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-x64@0.20.2': + '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.20.2': + '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-x64@0.20.2': + '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.20.2': + '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.20.2': + '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/linux-arm64@0.20.2': + '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm@0.20.2': + '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-ia32@0.20.2': + '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-loong64@0.20.2': + '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-mips64el@0.20.2': + '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-ppc64@0.20.2': + '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.20.2': + '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-s390x@0.20.2': + '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-x64@0.20.2': + '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.20.2': + '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.20.2': + '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.20.2': + '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/win32-arm64@0.20.2': + '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-ia32@0.20.2': + '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-x64@0.20.2': + '@esbuild/win32-x64@0.21.5': optional: true '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': @@ -14797,6 +14943,20 @@ snapshots: dependencies: '@lezer/common': 0.15.12 + '@lit-labs/ssr-dom-shim@1.2.0': {} + + '@lit/localize@0.12.1': + dependencies: + lit: 3.1.4 + + '@lit/react@1.0.5(@types/react@18.0.31)': + dependencies: + '@types/react': 18.0.31 + + '@lit/reactive-element@2.0.4': + dependencies: + '@lit-labs/ssr-dom-shim': 1.2.0 + '@lmdb/lmdb-darwin-arm64@2.7.11': optional: true @@ -15902,10 +16062,10 @@ snapshots: eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-config-xo: 0.44.0(eslint@8.57.0) eslint-config-xo-typescript: 4.0.0(@typescript-eslint/eslint-plugin@7.7.0(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-consistent-default-export-name: 0.0.15 eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-n: 17.2.1(eslint@8.57.0) eslint-plugin-no-use-extend-native: 0.5.0 eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.0.0) @@ -16885,6 +17045,8 @@ snapshots: '@types/tough-cookie@4.0.2': {} + '@types/trusted-types@2.0.7': {} + '@types/tunnel@0.0.3': dependencies: '@types/node': 20.12.7 @@ -17213,6 +17375,8 @@ snapshots: ansi-styles@6.2.1: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -17557,6 +17721,11 @@ snapshots: builtin-modules@3.3.0: {} + bundle-require@4.2.1(esbuild@0.21.5): + dependencies: + esbuild: 0.21.5 + load-tsconfig: 0.2.5 + bytes@3.1.1: {} bytes@3.1.2: {} @@ -17792,6 +17961,8 @@ snapshots: commander@11.1.0: {} + commander@4.1.1: {} + commander@5.1.0: {} commander@7.2.0: {} @@ -18641,31 +18812,31 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - esbuild@0.20.2: + esbuild@0.21.5: optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 escalade@3.1.1: {} @@ -18727,13 +18898,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.3 is-core-module: 2.13.1 @@ -18744,14 +18915,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -18773,7 +18944,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -18783,7 +18954,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -20357,7 +20528,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.12.7 - ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) + ts-node: 10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -20723,6 +20894,8 @@ snapshots: jose@5.6.3: {} + joycon@3.1.1: {} + js-base64@3.7.5: {} js-tokens@4.0.0: {} @@ -21064,6 +21237,8 @@ snapshots: lilconfig@2.1.0: {} + lilconfig@3.1.2: {} + lines-and-columns@1.2.4: {} lint-staged@15.0.2: @@ -21090,6 +21265,22 @@ snapshots: rfdc: 1.3.0 wrap-ansi: 8.1.0 + lit-element@4.0.6: + dependencies: + '@lit-labs/ssr-dom-shim': 1.2.0 + '@lit/reactive-element': 2.0.4 + lit-html: 3.1.4 + + lit-html@3.1.4: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.1.4: + dependencies: + '@lit/reactive-element': 2.0.4 + lit-element: 4.0.6 + lit-html: 3.1.4 + lmdb@2.7.11: dependencies: msgpackr: 1.8.5 @@ -21105,6 +21296,8 @@ snapshots: '@lmdb/lmdb-linux-x64': 2.7.11 '@lmdb/lmdb-win32-x64': 2.7.11 + load-tsconfig@0.2.5: {} + load-yaml-file@0.2.0: dependencies: graceful-fs: 4.2.11 @@ -22012,6 +22205,12 @@ snapshots: mute-stream@0.0.8: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.7: {} nanoid@4.0.2: {} @@ -22615,6 +22814,14 @@ snapshots: possible-typed-array-names@1.0.0: {} + postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3)): + dependencies: + lilconfig: 3.1.2 + yaml: 2.4.5 + optionalDependencies: + postcss: 8.4.39 + ts-node: 10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3) + postcss-media-query-parser@0.2.3: {} postcss-modules-extract-imports@3.0.0(postcss@8.4.31): @@ -22690,6 +22897,12 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.2.0 + postcss@8.4.39: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + postgres-array@2.0.0: {} postgres-array@3.0.2: {} @@ -23964,6 +24177,16 @@ snapshots: stylis@4.3.2: {} + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.2 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.5 + ts-interface-checker: 0.1.13 + superagent@9.0.1: dependencies: component-emitter: 1.3.0 @@ -24095,6 +24318,14 @@ snapshots: text-table@0.2.0: {} + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thirty-two@1.0.2: {} through2@4.0.2: @@ -24166,7 +24397,9 @@ snapshots: ts-dedent@2.2.0: {} - ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3): + ts-interface-checker@0.1.13: {} + + ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -24203,6 +24436,30 @@ snapshots: tsscmp@1.0.6: {} + tsup@8.1.0(@swc/core@1.3.52)(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3))(typescript@5.3.3): + dependencies: + bundle-require: 4.2.1(esbuild@0.21.5) + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.21.5 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3)) + resolve-from: 5.0.0 + rollup: 4.14.3 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.3.52(@swc/helpers@0.5.1) + postcss: 8.4.39 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + - ts-node + tty-table@4.1.6: dependencies: chalk: 4.1.2 @@ -24432,7 +24689,7 @@ snapshots: debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 - vite: 5.2.9(@types/node@20.10.4)(sass@1.56.1) + vite: 5.3.3(@types/node@20.10.4)(sass@1.56.1) transitivePeerDependencies: - '@types/node' - less @@ -24449,7 +24706,7 @@ snapshots: debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 - vite: 5.2.9(@types/node@20.11.20)(sass@1.56.1) + vite: 5.3.3(@types/node@20.11.20)(sass@1.56.1) transitivePeerDependencies: - '@types/node' - less @@ -24466,7 +24723,7 @@ snapshots: debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 - vite: 5.2.9(@types/node@20.12.7)(sass@1.56.1) + vite: 5.3.3(@types/node@20.12.7)(sass@1.56.1) transitivePeerDependencies: - '@types/node' - less @@ -24477,30 +24734,30 @@ snapshots: - supports-color - terser - vite@5.2.9(@types/node@20.10.4)(sass@1.56.1): + vite@5.3.3(@types/node@20.10.4)(sass@1.56.1): dependencies: - esbuild: 0.20.2 - postcss: 8.4.38 + esbuild: 0.21.5 + postcss: 8.4.39 rollup: 4.14.3 optionalDependencies: '@types/node': 20.10.4 fsevents: 2.3.3 sass: 1.56.1 - vite@5.2.9(@types/node@20.11.20)(sass@1.56.1): + vite@5.3.3(@types/node@20.11.20)(sass@1.56.1): dependencies: - esbuild: 0.20.2 - postcss: 8.4.38 + esbuild: 0.21.5 + postcss: 8.4.39 rollup: 4.14.3 optionalDependencies: '@types/node': 20.11.20 fsevents: 2.3.3 sass: 1.56.1 - vite@5.2.9(@types/node@20.12.7)(sass@1.56.1): + vite@5.3.3(@types/node@20.12.7)(sass@1.56.1): dependencies: - esbuild: 0.20.2 - postcss: 8.4.38 + esbuild: 0.21.5 + postcss: 8.4.39 rollup: 4.14.3 optionalDependencies: '@types/node': 20.12.7 @@ -24524,7 +24781,7 @@ snapshots: std-env: 3.7.0 tinybench: 2.8.0 tinypool: 1.0.0 - vite: 5.2.9(@types/node@20.10.4)(sass@1.56.1) + vite: 5.3.3(@types/node@20.10.4)(sass@1.56.1) vite-node: 2.0.0(@types/node@20.10.4)(sass@1.56.1) why-is-node-running: 2.2.2 optionalDependencies: @@ -24557,7 +24814,7 @@ snapshots: std-env: 3.7.0 tinybench: 2.8.0 tinypool: 1.0.0 - vite: 5.2.9(@types/node@20.11.20)(sass@1.56.1) + vite: 5.3.3(@types/node@20.11.20)(sass@1.56.1) vite-node: 2.0.0(@types/node@20.11.20)(sass@1.56.1) why-is-node-running: 2.2.2 optionalDependencies: @@ -24590,7 +24847,7 @@ snapshots: std-env: 3.7.0 tinybench: 2.8.0 tinypool: 1.0.0 - vite: 5.2.9(@types/node@20.12.7)(sass@1.56.1) + vite: 5.3.3(@types/node@20.12.7)(sass@1.56.1) vite-node: 2.0.0(@types/node@20.12.7)(sass@1.56.1) why-is-node-running: 2.2.2 optionalDependencies: @@ -24804,6 +25061,8 @@ snapshots: yaml@2.3.3: {} + yaml@2.4.5: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 From c5897b3893c76eda0c3eb566ea365b8d10a529ef Mon Sep 17 00:00:00 2001 From: wangsijie Date: Mon, 15 Jul 2024 15:36:14 +0800 Subject: [PATCH 017/135] refactor(core): remove subject token api prefix (#6235) --- packages/core/src/routes/init.ts | 4 ++-- .../index.openapi.json => subject-token.openapi.json} | 6 +++--- .../src/routes/{security/index.ts => subject-token.ts} | 8 +++++--- packages/core/src/routes/swagger/index.ts | 2 +- packages/core/src/routes/swagger/utils/operation-id.ts | 4 ++-- packages/integration-tests/src/api/subject-token.ts | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) rename packages/core/src/routes/{security/index.openapi.json => subject-token.openapi.json} (83%) rename packages/core/src/routes/{security/index.ts => subject-token.ts} (91%) diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 557004a2738a..f166af765f66 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -34,10 +34,10 @@ import resourceRoutes from './resource.js'; import resourceScopeRoutes from './resource.scope.js'; import roleRoutes from './role.js'; import roleScopeRoutes from './role.scope.js'; -import securityRoutes from './security/index.js'; import signInExperiencesRoutes from './sign-in-experience/index.js'; import ssoConnectors from './sso-connector/index.js'; import statusRoutes from './status.js'; +import subjectTokenRoutes from './subject-token.js'; import swaggerRoutes from './swagger/index.js'; import systemRoutes from './system.js'; import type { AnonymousRouter, ManagementApiRouter } from './types.js'; @@ -89,7 +89,7 @@ const createRouters = (tenant: TenantContext) => { organizationRoutes(managementRouter, tenant); ssoConnectors(managementRouter, tenant); systemRoutes(managementRouter, tenant); - securityRoutes(managementRouter, tenant); + subjectTokenRoutes(managementRouter, tenant); const anonymousRouter: AnonymousRouter = new Router(); wellKnownRoutes(anonymousRouter, tenant); diff --git a/packages/core/src/routes/security/index.openapi.json b/packages/core/src/routes/subject-token.openapi.json similarity index 83% rename from packages/core/src/routes/security/index.openapi.json rename to packages/core/src/routes/subject-token.openapi.json index bbee8d1666b5..6f2384f0123b 100644 --- a/packages/core/src/routes/security/index.openapi.json +++ b/packages/core/src/routes/subject-token.openapi.json @@ -1,13 +1,13 @@ { "tags": [ { - "name": "Security", - "description": "Security related endpoints." + "name": "Subject tokens", + "description": "The subject token API provides the ability to create a new subject token for the use of impersonating the user." }, { "name": "Dev feature" } ], "paths": { - "/api/security/subject-tokens": { + "/api/subject-tokens": { "post": { "summary": "Create a new subject token.", "description": "Create a new subject token for the use of impersonating the user.", diff --git a/packages/core/src/routes/security/index.ts b/packages/core/src/routes/subject-token.ts similarity index 91% rename from packages/core/src/routes/security/index.ts rename to packages/core/src/routes/subject-token.ts index 45440dc87d4d..99cbcb84a5a9 100644 --- a/packages/core/src/routes/security/index.ts +++ b/packages/core/src/routes/subject-token.ts @@ -8,9 +8,11 @@ import { EnvSet } from '#src/env-set/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaQuotaGuard from '#src/middleware/koa-quota-guard.js'; -import { type RouterInitArgs, type ManagementApiRouter } from '../types.js'; +import { type RouterInitArgs, type ManagementApiRouter } from './types.js'; -export default function securityRoutes(...args: RouterInitArgs) { +export default function subjectTokenRoutes( + ...args: RouterInitArgs +) { const [ router, { @@ -27,7 +29,7 @@ export default function securityRoutes(...args: R } router.post( - '/security/subject-tokens', + '/subject-tokens', koaQuotaGuard({ key: 'subjectTokenEnabled', quota }), koaGuard({ body: object({ diff --git a/packages/core/src/routes/swagger/index.ts b/packages/core/src/routes/swagger/index.ts index ffbc37d6626e..9973d7430c3a 100644 --- a/packages/core/src/routes/swagger/index.ts +++ b/packages/core/src/routes/swagger/index.ts @@ -155,7 +155,7 @@ const identifiableEntityNames = Object.freeze([ const additionalTags = Object.freeze( condArray( 'Organization applications', - EnvSet.values.isDevFeaturesEnabled && 'Security', + EnvSet.values.isDevFeaturesEnabled && 'Subject tokens', 'Organization users' ) ); diff --git a/packages/core/src/routes/swagger/utils/operation-id.ts b/packages/core/src/routes/swagger/utils/operation-id.ts index fe3df4002eb4..b65795657b79 100644 --- a/packages/core/src/routes/swagger/utils/operation-id.ts +++ b/packages/core/src/routes/swagger/utils/operation-id.ts @@ -25,8 +25,8 @@ const methodToVerb = Object.freeze({ type RouteDictionary = Record<`${OpenAPIV3.HttpMethods} ${string}`, string>; const devFeatureCustomRoutes: RouteDictionary = Object.freeze({ - // Security - 'post /security/subject-tokens': 'CreateSubjectToken', + // Subject tokens + 'post /subject-tokens': 'CreateSubjectToken', }); export const customRoutes: Readonly = Object.freeze({ diff --git a/packages/integration-tests/src/api/subject-token.ts b/packages/integration-tests/src/api/subject-token.ts index 8eba50195209..ec54bff45dd1 100644 --- a/packages/integration-tests/src/api/subject-token.ts +++ b/packages/integration-tests/src/api/subject-token.ts @@ -4,7 +4,7 @@ import { authedAdminApi } from './api.js'; export const createSubjectToken = async (userId: string, context?: JsonObject) => authedAdminApi - .post('security/subject-tokens', { + .post('subject-tokens', { json: { userId, context, From f94fb519f442e6a9d592d89583400f8f7dd98da7 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 15 Jul 2024 18:32:42 +0800 Subject: [PATCH 018/135] feat(core): add get available sso connectors endpoint (#6224) feat(core): implement get sso connectors implement get sso connectors endpoint --- .../sign-in-experience-validator.ts | 36 ++++++++------- .../enterprise-sso-verification.ts | 32 +++++++++++++ .../src/client/experience/index.ts | 9 ++++ .../enterprise-sso-verification.test.ts | 46 +++++++++++++++++++ 4 files changed, 106 insertions(+), 17 deletions(-) diff --git a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts index 26f095f3a911..5bd7935ce2bb 100644 --- a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts +++ b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts @@ -71,7 +71,7 @@ export class SignInExperienceValidator { } } - async verifyIdentificationMethod( + public async verifyIdentificationMethod( event: InteractionEvent, verificationRecord: VerificationRecord ) { @@ -93,7 +93,21 @@ export class SignInExperienceValidator { await this.guardSsoOnlyEmailIdentifier(verificationRecord); } - private async getSignInExperienceData() { + public async getEnabledSsoConnectorsByEmail(email: string) { + const domain = email.split('@')[1]; + const { singleSignOnEnabled } = await this.getSignInExperienceData(); + + if (!singleSignOnEnabled || !domain) { + return []; + } + + const { getAvailableSsoConnectors } = this.libraries.ssoConnectors; + const availableSsoConnectors = await getAvailableSsoConnectors(); + + return availableSsoConnectors.filter(({ domains }) => domains.includes(domain)); + } + + public async getSignInExperienceData() { this.signInExperienceDataCache ||= await this.queries.signInExperiences.findDefaultSignInExperience(); @@ -117,29 +131,17 @@ export class SignInExperienceValidator { return; } - const domain = emailIdentifier.split('@')[1]; - const { singleSignOnEnabled } = await this.getSignInExperienceData(); - - if (!singleSignOnEnabled || !domain) { - return; - } - - const { getAvailableSsoConnectors } = this.libraries.ssoConnectors; - const availableSsoConnectors = await getAvailableSsoConnectors(); - - const domainEnabledConnectors = availableSsoConnectors.filter(({ domains }) => - domains.includes(domain) - ); + const enabledSsoConnectors = await this.getEnabledSsoConnectorsByEmail(emailIdentifier); assertThat( - domainEnabledConnectors.length === 0, + enabledSsoConnectors.length === 0, new RequestError( { code: 'session.sso_enabled', status: 422, }, { - ssoConnectors: domainEnabledConnectors, + ssoConnectors: enabledSsoConnectors, } ) ); diff --git a/packages/core/src/routes/experience/verification-routes/enterprise-sso-verification.ts b/packages/core/src/routes/experience/verification-routes/enterprise-sso-verification.ts index 05464cee4c8f..bdb1d86e704e 100644 --- a/packages/core/src/routes/experience/verification-routes/enterprise-sso-verification.ts +++ b/packages/core/src/routes/experience/verification-routes/enterprise-sso-verification.ts @@ -100,4 +100,36 @@ export default function enterpriseSsoVerificationRoutes { + const { email } = ctx.guard.query; + const { + experienceInteraction: { signInExperienceValidator }, + } = ctx; + + assertThat( + email.split('@')[1], + new RequestError({ code: 'guard.invalid_input', status: 400, email }) + ); + + const connectors = await signInExperienceValidator.getEnabledSsoConnectorsByEmail(email); + + ctx.body = { + connectorIds: connectors.map(({ id }) => id), + }; + + return next(); + } + ); } diff --git a/packages/integration-tests/src/client/experience/index.ts b/packages/integration-tests/src/client/experience/index.ts index 2bacb75b6ccb..8a06e389f4f4 100644 --- a/packages/integration-tests/src/client/experience/index.ts +++ b/packages/integration-tests/src/client/experience/index.ts @@ -152,6 +152,15 @@ export class ExperienceClient extends MockClient { .json<{ verificationId: string }>(); } + public async getAvailableSsoConnectors(email: string) { + return api + .get(`${experienceRoutes.verification}/sso/connectors`, { + headers: { cookie: this.interactionCookie }, + searchParams: { email }, + }) + .json<{ connectorIds: string[] }>(); + } + public async createTotpSecret() { return api .post(`${experienceRoutes.verification}/totp/secret`, { diff --git a/packages/integration-tests/src/tests/api/experience-api/verifications/enterprise-sso-verification.test.ts b/packages/integration-tests/src/tests/api/experience-api/verifications/enterprise-sso-verification.test.ts index f1b98dfe2555..a33a05a0ae8a 100644 --- a/packages/integration-tests/src/tests/api/experience-api/verifications/enterprise-sso-verification.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/verifications/enterprise-sso-verification.test.ts @@ -198,4 +198,50 @@ devFeatureTest.describe('enterprise sso verification', () => { }); }); }); + + describe('getSsoConnectorsByEmail', () => { + const ssoConnectorApi = new SsoConnectorApi(); + const domain = `foo${randomString()}.com`; + + beforeAll(async () => { + await ssoConnectorApi.createMockOidcConnector([domain]); + + await updateSignInExperience({ + singleSignOnEnabled: true, + }); + }); + + afterAll(async () => { + await ssoConnectorApi.cleanUp(); + }); + + it('should get sso connectors with given email properly', async () => { + const client = await initExperienceClient(); + + const response = await client.getAvailableSsoConnectors('bar@' + domain); + + expect(response.connectorIds.length).toBeGreaterThan(0); + expect(response.connectorIds[0]).toBe(ssoConnectorApi.firstConnectorId); + }); + + it('should return empty array if no sso connectors found', async () => { + const client = await initExperienceClient(); + + const response = await client.getAvailableSsoConnectors('bar@invalid.com'); + + expect(response.connectorIds.length).toBe(0); + }); + + it('should return empty array if sso is not enabled', async () => { + await updateSignInExperience({ + singleSignOnEnabled: false, + }); + + const client = await initExperienceClient(); + + const response = await client.getAvailableSsoConnectors('bar@' + domain); + + expect(response.connectorIds.length).toBe(0); + }); + }); }); From bf139e5a8a17b1787fb276a75c63ac041cddc4d6 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 15 Jul 2024 19:00:18 +0800 Subject: [PATCH 019/135] feat(elements): init i18n --- commitlint.config.ts | 2 +- packages/console/src/pages/Profile/index.tsx | 14 +-- packages/elements/.gitignore | 1 + packages/elements/lit-localize.json | 15 +++ packages/elements/package.json | 12 ++- .../src/components/logto-card-section.ts | 34 +++++++ .../src/components/logto-form-card.ts | 12 ++- .../src/elements/logto-profile-card.ts | 30 ++++++ packages/elements/src/index.ts | 3 + packages/elements/src/locales/en.ts | 9 -- packages/elements/src/locales/index.ts | 40 -------- packages/elements/src/react.ts | 9 +- packages/elements/src/utils/locale.ts | 11 +++ packages/elements/xliff/de.xlf | 18 ++++ packages/elements/xliff/es.xlf | 18 ++++ packages/elements/xliff/fr.xlf | 18 ++++ packages/elements/xliff/it.xlf | 18 ++++ packages/elements/xliff/ja.xlf | 18 ++++ packages/elements/xliff/ko.xlf | 18 ++++ packages/elements/xliff/pl-PL.xlf | 18 ++++ packages/elements/xliff/pt-BR.xlf | 18 ++++ packages/elements/xliff/pt-PT.xlf | 18 ++++ packages/elements/xliff/ru.xlf | 18 ++++ packages/elements/xliff/tr-TR.xlf | 18 ++++ packages/elements/xliff/zh-CN.xlf | 18 ++++ packages/elements/xliff/zh-HK.xlf | 18 ++++ packages/elements/xliff/zh-TW.xlf | 18 ++++ pnpm-lock.yaml | 92 ++++++++++++++++--- 28 files changed, 458 insertions(+), 78 deletions(-) create mode 100644 packages/elements/.gitignore create mode 100644 packages/elements/lit-localize.json create mode 100644 packages/elements/src/components/logto-card-section.ts create mode 100644 packages/elements/src/elements/logto-profile-card.ts delete mode 100644 packages/elements/src/locales/en.ts delete mode 100644 packages/elements/src/locales/index.ts create mode 100644 packages/elements/src/utils/locale.ts create mode 100644 packages/elements/xliff/de.xlf create mode 100644 packages/elements/xliff/es.xlf create mode 100644 packages/elements/xliff/fr.xlf create mode 100644 packages/elements/xliff/it.xlf create mode 100644 packages/elements/xliff/ja.xlf create mode 100644 packages/elements/xliff/ko.xlf create mode 100644 packages/elements/xliff/pl-PL.xlf create mode 100644 packages/elements/xliff/pt-BR.xlf create mode 100644 packages/elements/xliff/pt-PT.xlf create mode 100644 packages/elements/xliff/ru.xlf create mode 100644 packages/elements/xliff/tr-TR.xlf create mode 100644 packages/elements/xliff/zh-CN.xlf create mode 100644 packages/elements/xliff/zh-HK.xlf create mode 100644 packages/elements/xliff/zh-TW.xlf diff --git a/commitlint.config.ts b/commitlint.config.ts index 5f44c75e5e8b..c89743462dc5 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -7,7 +7,7 @@ const config: UserConfig = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [...conventional.rules['type-enum'][2], 'api', 'release']], - 'scope-enum': [2, 'always', ['connector', 'console', 'core', 'demo-app', 'test', 'phrases', 'schemas', 'shared', 'experience', 'deps', 'deps-dev', 'cli', 'toolkit', 'cloud', 'app-insights']], + 'scope-enum': [2, 'always', ['connector', 'console', 'core', 'demo-app', 'test', 'phrases', 'schemas', 'shared', 'experience', 'deps', 'deps-dev', 'cli', 'toolkit', 'cloud', 'app-insights', 'elements']], // Slightly increase the tolerance to allow the appending PR number ...(isCi && { 'header-max-length': [2, 'always', 110] }), 'body-max-line-length': [2, 'always', 110], diff --git a/packages/console/src/pages/Profile/index.tsx b/packages/console/src/pages/Profile/index.tsx index 3560bab4cb9a..aa71c15acd8a 100644 --- a/packages/console/src/pages/Profile/index.tsx +++ b/packages/console/src/pages/Profile/index.tsx @@ -1,4 +1,4 @@ -import { createReactComponents } from '@logto/elements/react'; +import { createReactComponents, initLocalization } from '@logto/elements/react'; import type { ConnectorResponse } from '@logto/schemas'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -32,7 +32,11 @@ import Skeleton from './components/Skeleton'; import DeleteAccountModal from './containers/DeleteAccountModal'; import * as styles from './index.module.scss'; -const { LogtoFormCard, LogtoThemeProvider } = createReactComponents(React); +if (isDevFeaturesEnabled) { + initLocalization(); +} + +const { LogtoProfileCard, LogtoThemeProvider } = createReactComponents(React); function Profile() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); @@ -75,11 +79,7 @@ function Profile() {
{showLoadingSkeleton && } - {isDevFeaturesEnabled && ( - -

🚧 This section is a dev feature that is still working in progress.

-
- )} + {isDevFeaturesEnabled && } {user && !showLoadingSkeleton && (
diff --git a/packages/elements/.gitignore b/packages/elements/.gitignore new file mode 100644 index 000000000000..d3db8f9b18c4 --- /dev/null +++ b/packages/elements/.gitignore @@ -0,0 +1 @@ +src/generated/ diff --git a/packages/elements/lit-localize.json b/packages/elements/lit-localize.json new file mode 100644 index 000000000000..6df04a74a424 --- /dev/null +++ b/packages/elements/lit-localize.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://raw.githubusercontent.com/lit/lit/main/packages/localize-tools/config.schema.json", + "sourceLocale": "en", + "targetLocales": ["de", "es", "fr", "it", "ja", "ko", "pl-PL", "pt-BR", "pt-PT", "ru", "tr-TR", "zh-CN", "zh-HK", "zh-TW"], + "tsConfig": "./tsconfig.json", + "output": { + "mode": "runtime", + "outputDir": "./src/generated/locales", + "localeCodesModule": "./src/generated/locale-codes.ts" + }, + "interchange": { + "format": "xliff", + "xliffDir": "./xliff/" + } +} diff --git a/packages/elements/package.json b/packages/elements/package.json index 3ac8d3e2dbab..b9640c5ecdbd 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -27,13 +27,15 @@ }, "scripts": { "precommit": "lint-staged", - "build": "tsup", - "dev": "tsup --watch --no-splitting", + "build": "lit-localize build && tsup", + "dev": "lit-localize build && tsup --watch --no-splitting", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", "test": "echo \"No tests yet.\"", "test:ci": "pnpm run test --silent --coverage", - "prepack": "pnpm build" + "prepack": "pnpm check && pnpm build", + "localize": "lit-localize", + "check": "lit-localize extract && git add . -N && git diff --exit-code" }, "engines": { "node": "^20.9.0" @@ -48,6 +50,7 @@ "lit": "^3.1.4" }, "devDependencies": { + "@lit/localize-tools": "^0.7.2", "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "eslint": "^8.56.0", @@ -57,6 +60,9 @@ }, "eslintConfig": { "extends": "@silverhand", + "ignorePatterns": [ + "src/generated/" + ], "rules": { "no-console": "error", "unicorn/prevent-abbreviations": [ diff --git a/packages/elements/src/components/logto-card-section.ts b/packages/elements/src/components/logto-card-section.ts new file mode 100644 index 000000000000..de4e88be549c --- /dev/null +++ b/packages/elements/src/components/logto-card-section.ts @@ -0,0 +1,34 @@ +import { localized, msg } from '@lit/localize'; +import { LitElement, css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { unit } from '../utils/css.js'; +import { vars } from '../utils/theme.js'; + +const tagName = 'logto-card-section'; + +@customElement(tagName) +@localized() +export class LogtoCardSection extends LitElement { + static tagName = tagName; + static styles = css` + header { + font: ${vars.fontLabel2}; + color: ${vars.colorText}; + margin-bottom: ${unit(1)}; + } + `; + + @property() + heading = msg('Not available', { + id: 'form-card.fallback-title', + desc: 'The fallback title of a form card when the title is not provided.', + }); + + render() { + return html` +
${this.heading}
+ + `; + } +} diff --git a/packages/elements/src/components/logto-form-card.ts b/packages/elements/src/components/logto-form-card.ts index 4b371b980243..dc02bbd99207 100644 --- a/packages/elements/src/components/logto-form-card.ts +++ b/packages/elements/src/components/logto-form-card.ts @@ -1,14 +1,15 @@ +import { localized, msg } from '@lit/localize'; import { cond } from '@silverhand/essentials'; import { LitElement, css, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { type LocaleKeyOptional, type LocaleKey } from '../locales/index.js'; import { unit } from '../utils/css.js'; import { vars } from '../utils/theme.js'; const tagName = 'logto-form-card'; @customElement(tagName) +@localized() export class LogtoFormCard extends LitElement { static tagName = tagName; static styles = css` @@ -35,16 +36,19 @@ export class LogtoFormCard extends LitElement { `; @property() - title: LocaleKey = 'placeholders.not_available'; + heading = msg('Not available', { + id: 'form-card.fallback-title', + desc: 'The fallback title of a form card when the title is not provided.', + }); @property() - description: LocaleKeyOptional = ''; + description = ''; render() { return html`
-
${this.title}
+
${this.heading}
${cond(this.description && html`

${this.description}

`)}
diff --git a/packages/elements/src/elements/logto-profile-card.ts b/packages/elements/src/elements/logto-profile-card.ts new file mode 100644 index 000000000000..faa12273ad92 --- /dev/null +++ b/packages/elements/src/elements/logto-profile-card.ts @@ -0,0 +1,30 @@ +import { localized, msg } from '@lit/localize'; +import { LitElement, css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import { vars } from '../utils/theme.js'; + +const tagName = 'logto-profile-card'; + +@customElement(tagName) +@localized() +export class LogtoProfileCard extends LitElement { + static tagName = tagName; + static styles = css` + p { + color: ${vars.colorTextSecondary}; + } + `; + + render() { + return html` + + +

🚧 This section is a dev feature that is still working in progress.

+
+
+ `; + } +} diff --git a/packages/elements/src/index.ts b/packages/elements/src/index.ts index d4cb36992705..de8f4983214e 100644 --- a/packages/elements/src/index.ts +++ b/packages/elements/src/index.ts @@ -1,3 +1,6 @@ +export * from './components/logto-card-section.js'; export * from './components/logto-card.js'; export * from './components/logto-form-card.js'; export * from './components/logto-theme-provider.js'; +export * from './elements/logto-profile-card.js'; +export * from './utils/locale.js'; diff --git a/packages/elements/src/locales/en.ts b/packages/elements/src/locales/en.ts deleted file mode 100644 index f8998af8bc90..000000000000 --- a/packages/elements/src/locales/en.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const en = Object.freeze({ - placeholders: { - not_available: 'Not available', - }, - sections: { - profile: 'Profile', - security: 'Security', - }, -}); diff --git a/packages/elements/src/locales/index.ts b/packages/elements/src/locales/index.ts deleted file mode 100644 index bfce23d867f2..000000000000 --- a/packages/elements/src/locales/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { type en } from './en.js'; - -type KeyPath = T extends Record - ? { - [K in keyof T]: K extends string - ? T[K] extends Record - ? `${K}.${KeyPath}` - : `${K}` - : never; - }[keyof T] - : ''; - -/** The type of a full locale data object. */ -export type LocaleData = typeof en; - -/** - * The type of a locale key. It is a string that represents a path to a value in the locale data - * object. - * - * @example - * With the following locale data object: - * - * ```ts - * const en = { - * profile: { - * title: 'Profile', - * description: 'Your profile', - * }, - * }; - * ``` - * - * The locale key for the title would be `'profile.title'`. - */ -export type LocaleKey = KeyPath; - -/** - * The type of a locale key that is optional. Note that it uses an empty string to represent the - * absence of a key since Web Components do not support `undefined` as a property value. - */ -export type LocaleKeyOptional = LocaleKey | ''; diff --git a/packages/elements/src/react.ts b/packages/elements/src/react.ts index 1a3df2b52934..cacc64ea1bff 100644 --- a/packages/elements/src/react.ts +++ b/packages/elements/src/react.ts @@ -1,6 +1,8 @@ import { createComponent } from '@lit/react'; -import { LogtoThemeProvider, LogtoCard, LogtoFormCard } from './index.js'; +import { LogtoThemeProvider, LogtoCard, LogtoFormCard, LogtoProfileCard } from './index.js'; + +export * from './utils/locale.js'; export const createReactComponents = (react: Parameters[0]['react']) => { return { @@ -9,6 +11,11 @@ export const createReactComponents = (react: Parameters[ elementClass: LogtoFormCard, react, }), + LogtoProfileCard: createComponent({ + tagName: LogtoProfileCard.tagName, + elementClass: LogtoProfileCard, + react, + }), LogtoCard: createComponent({ tagName: LogtoCard.tagName, elementClass: LogtoCard, diff --git a/packages/elements/src/utils/locale.ts b/packages/elements/src/utils/locale.ts new file mode 100644 index 000000000000..7b41d4213b88 --- /dev/null +++ b/packages/elements/src/utils/locale.ts @@ -0,0 +1,11 @@ +import { configureLocalization } from '@lit/localize'; + +// Generated via output.localeCodesModule +import { sourceLocale, targetLocales } from '../generated/locale-codes.js'; + +export const initLocalization = () => + configureLocalization({ + sourceLocale, + targetLocales, + loadLocale: async (locale) => import(`/locales/${locale}.js`), + }); diff --git a/packages/elements/xliff/de.xlf b/packages/elements/xliff/de.xlf new file mode 100644 index 000000000000..29226b9e3211 --- /dev/null +++ b/packages/elements/xliff/de.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Nicht verfügbar + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/es.xlf b/packages/elements/xliff/es.xlf new file mode 100644 index 000000000000..27081c5ff4c9 --- /dev/null +++ b/packages/elements/xliff/es.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + No disponible + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/fr.xlf b/packages/elements/xliff/fr.xlf new file mode 100644 index 000000000000..d15883afb2f0 --- /dev/null +++ b/packages/elements/xliff/fr.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Non disponible + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/it.xlf b/packages/elements/xliff/it.xlf new file mode 100644 index 000000000000..891dd2a0cc7d --- /dev/null +++ b/packages/elements/xliff/it.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Non disponibile + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/ja.xlf b/packages/elements/xliff/ja.xlf new file mode 100644 index 000000000000..cd69abf18910 --- /dev/null +++ b/packages/elements/xliff/ja.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + 利用できません + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/ko.xlf b/packages/elements/xliff/ko.xlf new file mode 100644 index 000000000000..11d955e80cc0 --- /dev/null +++ b/packages/elements/xliff/ko.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + 사용할 수 없음 + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/pl-PL.xlf b/packages/elements/xliff/pl-PL.xlf new file mode 100644 index 000000000000..6773af30bcb5 --- /dev/null +++ b/packages/elements/xliff/pl-PL.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Niedostępne + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/pt-BR.xlf b/packages/elements/xliff/pt-BR.xlf new file mode 100644 index 000000000000..d13410c2f0cc --- /dev/null +++ b/packages/elements/xliff/pt-BR.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Não disponível + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/pt-PT.xlf b/packages/elements/xliff/pt-PT.xlf new file mode 100644 index 000000000000..7c4554c56122 --- /dev/null +++ b/packages/elements/xliff/pt-PT.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Não disponível + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/ru.xlf b/packages/elements/xliff/ru.xlf new file mode 100644 index 000000000000..04de783759fa --- /dev/null +++ b/packages/elements/xliff/ru.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Недоступно + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/tr-TR.xlf b/packages/elements/xliff/tr-TR.xlf new file mode 100644 index 000000000000..2a68b7f21454 --- /dev/null +++ b/packages/elements/xliff/tr-TR.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + Mevcut değil + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/zh-CN.xlf b/packages/elements/xliff/zh-CN.xlf new file mode 100644 index 000000000000..ddd4d329885c --- /dev/null +++ b/packages/elements/xliff/zh-CN.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + 不可用 + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/zh-HK.xlf b/packages/elements/xliff/zh-HK.xlf new file mode 100644 index 000000000000..010937564ab4 --- /dev/null +++ b/packages/elements/xliff/zh-HK.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + 不可用 + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/packages/elements/xliff/zh-TW.xlf b/packages/elements/xliff/zh-TW.xlf new file mode 100644 index 000000000000..baff9eed8dcb --- /dev/null +++ b/packages/elements/xliff/zh-TW.xlf @@ -0,0 +1,18 @@ + + + + + + Not available + 不可用 + The fallback title of a form card when the title is not provided. + + + Profile + + + Personal information + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90e736f82dc2..3cfa8d2677f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3144,7 +3144,7 @@ importers: version: 2.2.0(react@18.2.0) ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3) + version: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) tslib: specifier: ^2.4.1 version: 2.4.1 @@ -3567,6 +3567,9 @@ importers: specifier: ^3.1.4 version: 3.1.4 devDependencies: + '@lit/localize-tools': + specifier: ^0.7.2 + version: 0.7.2 '@silverhand/eslint-config': specifier: 6.0.1 version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.3.3) @@ -5217,6 +5220,10 @@ packages: '@lit-labs/ssr-dom-shim@1.2.0': resolution: {integrity: sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==} + '@lit/localize-tools@0.7.2': + resolution: {integrity: sha512-d5tehVao3Hz1DlTq6jy7TUYrCeyFi/E4BkzWr8MuAMjIM8aoKCR9s3cUw6m++8ZIiqGm2z7+6aHaPc/p1FGHyA==} + hasBin: true + '@lit/localize@0.12.1': resolution: {integrity: sha512-uuF6OO6fjqomCf3jXsJ5cTGf1APYuN88S4Gvo/fjt9YkG4OMaMvpEUqd5oWhyzrJfY+HcenAbLJNi2Cq3H7gdg==} @@ -5652,6 +5659,9 @@ packages: peerDependencies: '@parcel/core': ^2.9.3 + '@parse5/tools@0.3.0': + resolution: {integrity: sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==} + '@peculiar/asn1-android@2.3.10': resolution: {integrity: sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw==} @@ -8811,6 +8821,10 @@ packages: resolution: {integrity: sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==} engines: {node: '>=0.10.0'} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -9948,10 +9962,16 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonschema@1.4.1: + resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==} + jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -12243,6 +12263,9 @@ packages: source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -12890,6 +12913,10 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -14945,6 +14972,20 @@ snapshots: '@lit-labs/ssr-dom-shim@1.2.0': {} + '@lit/localize-tools@0.7.2': + dependencies: + '@lit/localize': 0.12.1 + '@parse5/tools': 0.3.0 + '@xmldom/xmldom': 0.8.7 + fast-glob: 3.3.2 + fs-extra: 10.1.0 + jsonschema: 1.4.1 + lit: 3.1.4 + minimist: 1.2.8 + parse5: 7.1.1 + source-map-support: 0.5.21 + typescript: 5.3.3 + '@lit/localize@0.12.1': dependencies: lit: 3.1.4 @@ -15721,6 +15762,10 @@ snapshots: '@parcel/utils': 2.9.3 nullthrows: 1.1.1 + '@parse5/tools@0.3.0': + dependencies: + parse5: 7.1.1 + '@peculiar/asn1-android@2.3.10': dependencies: '@peculiar/asn1-schema': 2.3.8 @@ -16062,10 +16107,10 @@ snapshots: eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-config-xo: 0.44.0(eslint@8.57.0) eslint-config-xo-typescript: 4.0.0(@typescript-eslint/eslint-plugin@7.7.0(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-consistent-default-export-name: 0.0.15 eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-n: 17.2.1(eslint@8.57.0) eslint-plugin-no-use-extend-native: 0.5.0 eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.0.0) @@ -18898,13 +18943,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.3 is-core-module: 2.13.1 @@ -18915,14 +18960,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -18944,7 +18989,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -18954,7 +18999,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -19457,6 +19502,12 @@ snapshots: fs-exists-sync@0.1.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -20528,7 +20579,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.12.7 - ts-node: 10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3) + ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -20987,8 +21038,16 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsonparse@1.3.1: {} + jsonschema@1.4.1: {} + jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -22820,7 +22879,7 @@ snapshots: yaml: 2.4.5 optionalDependencies: postcss: 8.4.39 - ts-node: 10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3) + ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) postcss-media-query-parser@0.2.3: {} @@ -23883,6 +23942,11 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.6.1: {} source-map@0.7.4: {} @@ -24399,7 +24463,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3): + ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -24609,6 +24673,8 @@ snapshots: universalify@0.2.0: {} + universalify@2.0.1: {} + unpipe@1.0.0: {} update-browserslist-db@1.0.10(browserslist@4.21.4): From 84ac935c80b6c3af378fa275940963a09a4295ae Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 15 Jul 2024 19:02:57 +0800 Subject: [PATCH 020/135] feat(core,schemas): implement the register flow (#6237) * feat(core,schemas): implement the register flow implement the register flow * refactor(core,schemas): relocate the profile type defs relocate the profile type defs * fix(core): fix the validation guard logic fix the validation guard logic * fix(core): fix social and sso identity not created bug fix social and sso identity not created bug * fix(core): fix social identities profile key fix social identities profile key * fix(core): fix sso query method fix sso query method --- .../classes/experience-interaction.ts | 137 ++++++++++++------ .../src/routes/experience/classes/utils.ts | 52 ++++++- .../classes/validators/profile-validator.ts | 85 +++++++++++ .../verifications/code-verification.ts | 21 ++- .../enterprise-sso-verification.ts | 65 ++++++++- .../verifications/password-verification.ts | 17 ++- .../verifications/social-verification.ts | 71 ++++++++- .../verifications/verification-record.ts | 20 ++- packages/core/src/routes/experience/index.ts | 4 +- packages/core/src/routes/experience/types.ts | 50 +++++++ 10 files changed, 449 insertions(+), 73 deletions(-) create mode 100644 packages/core/src/routes/experience/classes/validators/profile-validator.ts diff --git a/packages/core/src/routes/experience/classes/experience-interaction.ts b/packages/core/src/routes/experience/classes/experience-interaction.ts index 9be9636029ce..16f80f602131 100644 --- a/packages/core/src/routes/experience/classes/experience-interaction.ts +++ b/packages/core/src/routes/experience/classes/experience-interaction.ts @@ -1,5 +1,7 @@ import { type ToZodObject } from '@logto/connector-kit'; -import { InteractionEvent, VerificationType } from '@logto/schemas'; +import { InteractionEvent, type VerificationType } from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import { conditional } from '@silverhand/essentials'; import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; @@ -7,8 +9,10 @@ import { type WithLogContext } from '#src/middleware/koa-audit-log.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; -import type { Interaction } from '../types.js'; +import { interactionProfileGuard, type Interaction, type InteractionProfile } from '../types.js'; +import { getNewUserProfileFromVerificationRecord, toUserSocialIdentityData } from './utils.js'; +import { ProfileValidator } from './validators/profile-validator.js'; import { SignInExperienceValidator } from './validators/sign-in-experience-validator.js'; import { buildVerificationRecord, @@ -20,14 +24,14 @@ import { type InteractionStorage = { interactionEvent?: InteractionEvent; userId?: string; - profile?: Record; + profile?: InteractionProfile; verificationRecords?: VerificationRecordData[]; }; const interactionStorageGuard = z.object({ interactionEvent: z.nativeEnum(InteractionEvent).optional(), userId: z.string().optional(), - profile: z.object({}).optional(), + profile: interactionProfileGuard.optional(), verificationRecords: verificationRecordDataGuard.array().optional(), }) satisfies ToZodObject; @@ -39,6 +43,7 @@ const interactionStorageGuard = z.object({ */ export default class ExperienceInteraction { public readonly signInExperienceValidator: SignInExperienceValidator; + public readonly profileValidator: ProfileValidator; /** The user verification record list for the current interaction. */ private readonly verificationRecords = new Map(); @@ -65,6 +70,7 @@ export default class ExperienceInteraction { this.signInExperienceValidator = new SignInExperienceValidator(libraries, queries); if (!interactionDetails) { + this.profileValidator = new ProfileValidator(libraries, queries); return; } @@ -82,6 +88,9 @@ export default class ExperienceInteraction { this.userId = userId; this.profile = profile; + // Profile validator requires the userId for existing user profile update validation + this.profileValidator = new ProfileValidator(libraries, queries, userId); + for (const record of verificationRecords) { const instance = buildVerificationRecord(libraries, queries, record); this.verificationRecords.set(instance.type, instance); @@ -123,16 +132,16 @@ export default class ExperienceInteraction { * Identify the user using the verification record. * * - Check if the verification record exists. - * - Verify the verification record with `SignInExperienceValidator`. + * - Verify the verification record with {@link SignInExperienceValidator}. * - Create a new user using the verification record if the current interaction event is `Register`. * - Identify the user using the verification record if the current interaction event is `SignIn` or `ForgotPassword`. * - Set the user id to the current interaction. * - * @throws RequestError with 404 if the interaction event is not set - * @throws RequestError with 404 if the verification record is not found - * @throws RequestError with 400 if the verification record is not valid for the current interaction event - * @throws RequestError with 401 if the user is suspended - * @throws RequestError with 409 if the current session has already identified a different user + * @throws RequestError with 404 if the interaction event is not set. + * @throws RequestError with 404 if the verification record is not found. + * @throws RequestError with 422 if the verification record is not enabled in the SIE settings. + * @see {@link identifyExistingUser} for more exceptions that can be thrown in the SignIn and ForgotPassword events. + * @see {@link createNewUser} for more exceptions that can be thrown in the Register event. **/ public async identifyUser(verificationId: string) { const verificationRecord = this.getVerificationRecordById(verificationId); @@ -196,9 +205,20 @@ export default class ExperienceInteraction { /** Submit the current interaction result to the OIDC provider and clear the interaction data */ public async submit() { - // TODO: refine the error code assertThat(this.userId, 'session.verification_session_not_found'); + // TODO: mfa validation + // TODO: missing profile fields validation + + const { + queries: { users: userQueries }, + } = this.tenant; + + // Update user profile + await userQueries.updateUserById(this.userId, { + lastSignInAt: Date.now(), + }); + const { provider } = this.tenant; const redirectTo = await provider.interactionResult(this.ctx.req, this.ctx.res, { @@ -224,46 +244,71 @@ export default class ExperienceInteraction { return [...this.verificationRecords.values()]; } + /** + * Identify the existing user using the verification record. + * + * @throws RequestError with 400 if the verification record is not verified or not valid for identifying a user + * @throws RequestError with 404 if the user is not found + * @throws RequestError with 401 if the user is suspended + * @throws RequestError with 409 if the current session has already identified a different user + */ private async identifyExistingUser(verificationRecord: VerificationRecord) { - switch (verificationRecord.type) { - case VerificationType.Password: - case VerificationType.VerificationCode: - case VerificationType.Social: - case VerificationType.EnterpriseSso: { - // TODO: social sign-in with verified email - const { id, isSuspended } = await verificationRecord.identifyUser(); - - assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); - - // Throws an 409 error if the current session has already identified a different user - if (this.userId) { - assertThat( - this.userId === id, - new RequestError({ code: 'session.identity_conflict', status: 409 }) - ); - return; - } - - this.userId = id; - break; - } - default: { - // Unsupported verification type for identification, such as MFA verification. - throw new RequestError({ code: 'session.verification_failed', status: 400 }); - } + // Check verification record can be used to identify a user using the `identifyUser` method. + // E.g. MFA verification record does not have the `identifyUser` method, cannot be used to identify a user. + assertThat( + 'identifyUser' in verificationRecord, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { id, isSuspended } = await verificationRecord.identifyUser(); + + assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); + + // Throws an 409 error if the current session has already identified a different user + if (this.userId) { + assertThat( + this.userId === id, + new RequestError({ code: 'session.identity_conflict', status: 409 }) + ); + return; } + + this.userId = id; } private async createNewUser(verificationRecord: VerificationRecord) { - // TODO: To be implemented - switch (verificationRecord.type) { - case VerificationType.VerificationCode: { - break; - } - default: { - // Unsupported verification type for user creation, such as MFA verification. - throw new RequestError({ code: 'session.verification_failed', status: 400 }); - } + const { + libraries: { + users: { generateUserId, insertUser }, + }, + queries: { userSsoIdentities: userSsoIdentitiesQueries }, + } = this.tenant; + + const newProfile = await getNewUserProfileFromVerificationRecord(verificationRecord); + + await this.profileValidator.guardProfileUniquenessAcrossUsers(newProfile); + + // TODO: new user provisioning and hooks + + const { socialIdentity, enterpriseSsoIdentity, ...rest } = newProfile; + + const [user] = await insertUser( + { + id: await generateUserId(), + ...rest, + ...conditional(socialIdentity && { identities: toUserSocialIdentityData(socialIdentity) }), + }, + [] + ); + + if (enterpriseSsoIdentity) { + await userSsoIdentitiesQueries.insert({ + id: generateStandardId(), + userId: user.id, + ...enterpriseSsoIdentity, + }); } + + this.userId = user.id; } } diff --git a/packages/core/src/routes/experience/classes/utils.ts b/packages/core/src/routes/experience/classes/utils.ts index 575908b45fa3..4ddb0534a210 100644 --- a/packages/core/src/routes/experience/classes/utils.ts +++ b/packages/core/src/routes/experience/classes/utils.ts @@ -1,7 +1,17 @@ -import { SignInIdentifier, type InteractionIdentifier } from '@logto/schemas'; +import { + SignInIdentifier, + VerificationType, + type InteractionIdentifier, + type User, +} from '@logto/schemas'; +import RequestError from '#src/errors/RequestError/index.js'; import type Queries from '#src/tenants/Queries.js'; +import type { InteractionProfile } from '../types.js'; + +import { type VerificationRecord } from './verifications/index.js'; + export const findUserByIdentifier = async ( userQuery: Queries['users'], { type, value }: InteractionIdentifier @@ -18,3 +28,43 @@ export const findUserByIdentifier = async ( } } }; + +/** + * @throws {RequestError} -400 if the verification record type is not supported for user creation. + * @throws {RequestError} -400 if the verification record is not verified. + */ +export const getNewUserProfileFromVerificationRecord = async ( + verificationRecord: VerificationRecord +): Promise => { + switch (verificationRecord.type) { + case VerificationType.VerificationCode: { + return verificationRecord.toUserProfile(); + } + case VerificationType.EnterpriseSso: + case VerificationType.Social: { + const identityProfile = await verificationRecord.toUserProfile(); + const syncedProfile = await verificationRecord.toSyncedProfile(true); + return { ...identityProfile, ...syncedProfile }; + } + default: { + // Unsupported verification type for user creation, such as MFA verification. + throw new RequestError({ code: 'session.verification_failed', status: 400 }); + } + } +}; + +/** + * Convert the interaction profile `socialIdentity` to `User['identities']` data format + */ +export const toUserSocialIdentityData = ( + socialIdentity: Required['socialIdentity'] +): User['identities'] => { + const { target, userInfo } = socialIdentity; + + return { + [target]: { + userId: userInfo.id, + details: userInfo, + }, + }; +}; diff --git a/packages/core/src/routes/experience/classes/validators/profile-validator.ts b/packages/core/src/routes/experience/classes/validators/profile-validator.ts new file mode 100644 index 000000000000..a2c8bcb1ce0f --- /dev/null +++ b/packages/core/src/routes/experience/classes/validators/profile-validator.ts @@ -0,0 +1,85 @@ +import RequestError from '#src/errors/RequestError/index.js'; +import type Libraries from '#src/tenants/Libraries.js'; +import type Queries from '#src/tenants/Queries.js'; +import assertThat from '#src/utils/assert-that.js'; + +import type { InteractionProfile } from '../../types.js'; + +export class ProfileValidator { + constructor( + private readonly libraries: Libraries, + private readonly queries: Queries, + /** UserId is required for existing user profile update validation */ + private readonly userId?: string + ) {} + + public async guardProfileUniquenessAcrossUsers(profile: InteractionProfile) { + const { hasUser, hasUserWithEmail, hasUserWithPhone, hasUserWithIdentity } = this.queries.users; + const { userSsoIdentities } = this.queries; + + const { username, primaryEmail, primaryPhone, socialIdentity, enterpriseSsoIdentity } = profile; + + if (username) { + assertThat( + !(await hasUser(username)), + new RequestError({ + code: 'user.username_already_in_use', + status: 422, + }) + ); + } + + if (primaryEmail) { + assertThat( + !(await hasUserWithEmail(primaryEmail)), + new RequestError({ + code: 'user.email_already_in_use', + status: 422, + }) + ); + } + + if (primaryPhone) { + assertThat( + !(await hasUserWithPhone(primaryPhone)), + new RequestError({ + code: 'user.phone_already_in_use', + status: 422, + }) + ); + } + + if (socialIdentity) { + const { + target, + userInfo: { id }, + } = socialIdentity; + + assertThat( + !(await hasUserWithIdentity(target, id)), + new RequestError({ + code: 'user.identity_already_in_use', + status: 422, + }) + ); + } + + if (enterpriseSsoIdentity) { + const { issuer, identityId } = enterpriseSsoIdentity; + const userSsoIdentity = await userSsoIdentities.findUserSsoIdentityBySsoIdentityId( + issuer, + identityId + ); + + assertThat( + !userSsoIdentity, + new RequestError({ + code: 'user.identity_already_in_use', + status: 422, + }) + ); + } + + // TODO: Password validation + } +} diff --git a/packages/core/src/routes/experience/classes/verifications/code-verification.ts b/packages/core/src/routes/experience/classes/verifications/code-verification.ts index e87aceed9e1e..e536cc4d2ffb 100644 --- a/packages/core/src/routes/experience/classes/verifications/code-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/code-verification.ts @@ -18,7 +18,7 @@ import assertThat from '#src/utils/assert-that.js'; import { findUserByIdentifier } from '../utils.js'; -import { type VerificationRecord } from './verification-record.js'; +import { type IdentifierVerificationRecord } from './verification-record.js'; const eventToTemplateTypeMap: Record = { SignIn: TemplateType.SignIn, @@ -67,7 +67,9 @@ const getPasscodeIdentifierPayload = ( * * To avoid the redundant naming, the `CodeVerification` is used instead of `VerificationCodeVerification`. */ -export class CodeVerification implements VerificationRecord { +export class CodeVerification + implements IdentifierVerificationRecord +{ /** * Factory method to create a new CodeVerification record using the given identifier. */ @@ -164,10 +166,6 @@ export class CodeVerification implements VerificationRecord { assertThat( this.verified, @@ -189,6 +187,17 @@ export class CodeVerification implements VerificationRecord { + assertThat( + this.verified, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { type, value } = this.identifier; + + return type === 'email' ? { primaryEmail: value } : { primaryPhone: value }; + } + toJson(): CodeVerificationRecordData { const { id, type, identifier, interactionEvent, verified } = this; diff --git a/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts b/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts index c282d76350c5..4a8db8cdaa92 100644 --- a/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts @@ -21,7 +21,9 @@ import type Queries from '#src/tenants/Queries.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; -import { type VerificationRecord } from './verification-record.js'; +import type { InteractionProfile } from '../../types.js'; + +import { type IdentifierVerificationRecord } from './verification-record.js'; /** The JSON data type for the EnterpriseSsoVerification record stored in the interaction storage */ export type EnterpriseSsoVerificationRecordData = { @@ -44,7 +46,7 @@ export const enterPriseSsoVerificationRecordDataGuard = z.object({ }) satisfies ToZodObject; export class EnterpriseSsoVerification - implements VerificationRecord + implements IdentifierVerificationRecord { static create(libraries: Libraries, queries: Queries, connectorId: string) { return new EnterpriseSsoVerification(libraries, queries, { @@ -81,8 +83,10 @@ export class EnterpriseSsoVerification return Boolean(this.enterpriseSsoUserInfo && this.issuer); } - async getConnectorData(connectorId: string) { - this.connectorDataCache ||= await this.libraries.ssoConnectors.getSsoConnectorById(connectorId); + async getConnectorData() { + this.connectorDataCache ||= await this.libraries.ssoConnectors.getSsoConnectorById( + this.connectorId + ); return this.connectorDataCache; } @@ -104,7 +108,7 @@ export class EnterpriseSsoVerification tenantContext: TenantContext, payload: SocialAuthorizationUrlPayload ) { - const connectorData = await this.getConnectorData(this.connectorId); + const connectorData = await this.getConnectorData(); return getSsoAuthorizationUrl(ctx, tenantContext, connectorData, payload); } @@ -117,7 +121,7 @@ export class EnterpriseSsoVerification * See the above {@link createAuthorizationUrl} method for more details. */ async verify(ctx: WithLogContext, tenantContext: TenantContext, callbackData: JsonObject) { - const connectorData = await this.getConnectorData(this.connectorId); + const connectorData = await this.getConnectorData(); const { issuer, userInfo } = await verifySsoIdentity( ctx, tenantContext, @@ -129,10 +133,14 @@ export class EnterpriseSsoVerification this.enterpriseSsoUserInfo = userInfo; } + /** + * Identify the user by the enterprise SSO identity. + * If the user is not found, find the related user by the enterprise SSO identity and return the related user. + */ async identifyUser(): Promise { assertThat( this.isVerified, - new RequestError({ code: 'session.verification_failed', status: 422 }) + new RequestError({ code: 'session.verification_failed', status: 400 }) ); // TODO: sync userInfo and link sso identity @@ -152,6 +160,49 @@ export class EnterpriseSsoVerification throw new RequestError({ code: 'user.identity_not_exist', status: 404 }); } + /** + * Returns the use SSO identity as a new user profile. + */ + async toUserProfile(): Promise>> { + assertThat( + this.enterpriseSsoUserInfo && this.issuer, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + return { + enterpriseSsoIdentity: { + issuer: this.issuer, + ssoConnectorId: this.connectorId, + identityId: this.enterpriseSsoUserInfo.id, + detail: this.enterpriseSsoUserInfo, + }, + }; + } + + /** + * Returns the synced profile from the enterprise SSO identity. + * + * @param isNewUser - Whether the returned profile is for a new user. If true, the profile should contain the user's email. + */ + async toSyncedProfile( + isNewUser = false + ): Promise> { + assertThat( + this.enterpriseSsoUserInfo && this.issuer, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { name, avatar, email: primaryEmail } = this.enterpriseSsoUserInfo; + + if (isNewUser) { + return { name, avatar, primaryEmail }; + } + + const { syncProfile } = await this.getConnectorData(); + + return syncProfile ? { name, avatar } : {}; + } + toJson(): EnterpriseSsoVerificationRecordData { const { id, connectorId, type, enterpriseSsoUserInfo, issuer } = this; diff --git a/packages/core/src/routes/experience/classes/verifications/password-verification.ts b/packages/core/src/routes/experience/classes/verifications/password-verification.ts index fd9b5675124e..60d25da7ec55 100644 --- a/packages/core/src/routes/experience/classes/verifications/password-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/password-verification.ts @@ -15,7 +15,7 @@ import assertThat from '#src/utils/assert-that.js'; import { findUserByIdentifier } from '../utils.js'; -import { type VerificationRecord } from './verification-record.js'; +import { type IdentifierVerificationRecord } from './verification-record.js'; export type PasswordVerificationRecordData = { id: string; @@ -31,7 +31,9 @@ export const passwordVerificationRecordDataGuard = z.object({ verified: z.boolean(), }) satisfies ToZodObject; -export class PasswordVerification implements VerificationRecord { +export class PasswordVerification + implements IdentifierVerificationRecord +{ /** Factory method to create a new `PasswordVerification` record using an identifier */ static create(libraries: Libraries, queries: Queries, identifier: InteractionIdentifier) { return new PasswordVerification(libraries, queries, { @@ -89,7 +91,6 @@ export class PasswordVerification implements VerificationRecord { assertThat( this.verified, @@ -98,7 +99,15 @@ export class PasswordVerification implements VerificationRecord; -export class SocialVerification implements VerificationRecord { +export class SocialVerification implements IdentifierVerificationRecord { /** * Factory method to create a new SocialVerification instance */ @@ -56,6 +59,8 @@ export class SocialVerification implements VerificationRecord { assertThat( this.isVerified, - new RequestError({ code: 'session.verification_failed', status: 422 }) + new RequestError({ code: 'session.verification_failed', status: 400 }) ); // TODO: sync userInfo and link social identity @@ -143,7 +148,7 @@ export class SocialVerification implements VerificationRecord>> { + assertThat( + this.socialUserInfo, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { + metadata: { target }, + } = await this.getConnectorData(); + + return { + socialIdentity: { + target, + userInfo: this.socialUserInfo, + }, + }; + } + + /** + * Returns the synced profile from the social identity. + * + * @param isNewUser - Whether the profile is for a new user. If set to true, the primary email will be included in the profile. + */ + async toSyncedProfile( + isNewUser = false + ): Promise> { + assertThat( + this.socialUserInfo, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { name, avatar, email: primaryEmail } = this.socialUserInfo; + + if (isNewUser) { + return { name, avatar, primaryEmail }; + } + + const { + dbEntry: { syncProfile }, + } = await this.getConnectorData(); + + return syncProfile ? { name, avatar } : {}; + } + toJson(): SocialVerificationRecordData { const { id, connectorId, type, socialUserInfo } = this; @@ -166,7 +218,6 @@ export class SocialVerification implements VerificationRecord { - const { socials } = this.libraries; const { users: { findUserByIdentity }, } = this.queries; @@ -177,7 +228,7 @@ export class SocialVerification implements VerificationRecord = { id: string; @@ -17,3 +17,21 @@ export abstract class VerificationRecord< abstract toJson(): Json; } + +type IdentifierVerificationType = + | VerificationType.VerificationCode + | VerificationType.Password + | VerificationType.Social + | VerificationType.EnterpriseSso; + +/** + * The abstract class for all identifier verification records. + * + * - A `identifyUser` method must be provided to identify the user associated with the verification record. + */ +export abstract class IdentifierVerificationRecord< + T extends VerificationType = IdentifierVerificationType, + Json extends Data = Data, +> extends VerificationRecord { + abstract identifyUser(): Promise; +} diff --git a/packages/core/src/routes/experience/index.ts b/packages/core/src/routes/experience/index.ts index f020e06fada6..ec83da600bda 100644 --- a/packages/core/src/routes/experience/index.ts +++ b/packages/core/src/routes/experience/index.ts @@ -79,7 +79,7 @@ export default function experienceApiRoutes( body: z.object({ interactionEvent: z.nativeEnum(InteractionEvent), }), - status: [204, 403], + status: [204, 400, 403], }), async (ctx, next) => { const { interactionEvent } = ctx.guard.body; @@ -107,7 +107,7 @@ export default function experienceApiRoutes( experienceRoutes.identification, koaGuard({ body: identificationApiPayloadGuard, - status: [204, 400, 401, 404, 409], + status: [201, 204, 400, 401, 404, 409, 422], }), async (ctx, next) => { const { verificationId } = ctx.guard.body; diff --git a/packages/core/src/routes/experience/types.ts b/packages/core/src/routes/experience/types.ts index 8100dfcdf9e5..9e0bb69e89de 100644 --- a/packages/core/src/routes/experience/types.ts +++ b/packages/core/src/routes/experience/types.ts @@ -1,3 +1,53 @@ +import { type SocialUserInfo, socialUserInfoGuard, type ToZodObject } from '@logto/connector-kit'; +import { type CreateUser, Users, UserSsoIdentities, type UserSsoIdentity } from '@logto/schemas'; import type Provider from 'oidc-provider'; +import { z } from 'zod'; export type Interaction = Awaited>; + +export type InteractionProfile = { + socialIdentity?: { + target: string; + userInfo: SocialUserInfo; + }; + enterpriseSsoIdentity?: Pick< + UserSsoIdentity, + 'identityId' | 'ssoConnectorId' | 'issuer' | 'detail' + >; +} & Pick< + CreateUser, + | 'avatar' + | 'name' + | 'username' + | 'primaryEmail' + | 'primaryPhone' + | 'passwordEncrypted' + | 'passwordEncryptionMethod' +>; + +export const interactionProfileGuard = Users.createGuard + .pick({ + avatar: true, + name: true, + username: true, + primaryEmail: true, + primaryPhone: true, + passwordEncrypted: true, + passwordEncryptionMethod: true, + }) + .extend({ + socialIdentity: z + .object({ + target: z.string(), + userInfo: socialUserInfoGuard, + }) + .optional(), + enterpriseSsoIdentity: UserSsoIdentities.guard + .pick({ + identityId: true, + ssoConnectorId: true, + issuer: true, + detail: true, + }) + .optional(), + }) satisfies ToZodObject; From ce3a62bc7aed866cced38c37c416e91d4b59c505 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Tue, 16 Jul 2024 00:06:09 +0800 Subject: [PATCH 021/135] feat(core,schemas): add post custom ui assets api (#6118) * feat(core,schemas): add post custom ui assets api --- packages/core/package.json | 2 + .../core/src/__mocks__/sign-in-experience.ts | 2 +- .../src/queries/sign-in-experience.test.ts | 3 +- packages/core/src/routes-me/user-assets.ts | 2 +- .../custom-ui-assets/index.openapi.json | 42 +++++ .../custom-ui-assets/index.test.ts | 133 +++++++++++++ .../custom-ui-assets/index.ts | 115 ++++++++++++ .../src/routes/sign-in-experience/index.ts | 7 +- packages/core/src/routes/swagger/index.ts | 1 + .../src/routes/swagger/utils/operation-id.ts | 2 + packages/core/src/routes/user-assets.ts | 2 +- packages/core/src/tenants/SystemContext.ts | 16 ++ packages/core/src/utils/file.test.ts | 16 ++ packages/core/src/utils/file.ts | 17 ++ .../core/src/utils/storage/azure-storage.ts | 23 ++- packages/core/src/utils/storage/consts.ts | 8 - packages/experience/src/__mocks__/logto.tsx | 4 +- ...next-1720505152-update-custom-ui-assets.ts | 20 ++ .../jsonb-types/sign-in-experience.ts | 7 + .../schemas/src/seeds/sign-in-experience.ts | 2 +- packages/schemas/src/types/system.ts | 6 + packages/schemas/src/types/user-assets.ts | 7 + .../schemas/tables/sign_in_experiences.sql | 2 +- pnpm-lock.yaml | 177 ++++++++++++------ 24 files changed, 542 insertions(+), 74 deletions(-) create mode 100644 packages/core/src/routes/sign-in-experience/custom-ui-assets/index.openapi.json create mode 100644 packages/core/src/routes/sign-in-experience/custom-ui-assets/index.test.ts create mode 100644 packages/core/src/routes/sign-in-experience/custom-ui-assets/index.ts create mode 100644 packages/core/src/utils/file.test.ts create mode 100644 packages/core/src/utils/file.ts delete mode 100644 packages/core/src/utils/storage/consts.ts create mode 100644 packages/schemas/alterations/next-1720505152-update-custom-ui-assets.ts diff --git a/packages/core/package.json b/packages/core/package.json index e6eecbcd404e..70dc802a874d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -96,6 +96,7 @@ "@logto/cloud": "0.2.5-3046fa6", "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", + "@types/adm-zip": "^0.5.5", "@types/debug": "^4.1.7", "@types/etag": "^1.8.1", "@types/jest": "^29.4.0", @@ -113,6 +114,7 @@ "@types/semver": "^7.3.12", "@types/sinon": "^17.0.0", "@types/supertest": "^6.0.2", + "adm-zip": "^0.5.14", "eslint": "^8.56.0", "jest": "^29.7.0", "jest-matcher-specific-error": "^1.0.0", diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index 37cbff29ae87..f76cf869f538 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -92,7 +92,7 @@ export const mockSignInExperience: SignInExperience = { customCss: null, customContent: {}, agreeToTermsPolicy: AgreeToTermsPolicy.Automatic, - customUiAssetId: null, + customUiAssets: null, passwordPolicy: {}, mfa: { policy: MfaPolicy.UserControlled, diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index f9e5c9daaf96..7bfca4aed888 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -32,6 +32,7 @@ describe('sign-in-experience query', () => { signUp: JSON.stringify(mockSignInExperience.signUp), socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets), customContent: JSON.stringify(mockSignInExperience.customContent), + customUiAssets: JSON.stringify(mockSignInExperience.customUiAssets), passwordPolicy: JSON.stringify(mockSignInExperience.passwordPolicy), mfa: JSON.stringify(mockSignInExperience.mfa), socialSignIn: JSON.stringify(mockSignInExperience.socialSignIn), @@ -40,7 +41,7 @@ describe('sign-in-experience query', () => { it('findDefaultSignInExperience', async () => { /* eslint-disable sql/no-unsafe-query */ const expectSql = ` - select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "agree_to_terms_policy", "sign_in", "sign_up", "social_sign_in", "social_sign_in_connector_targets", "sign_in_mode", "custom_css", "custom_content", "custom_ui_asset_id", "password_policy", "mfa", "single_sign_on_enabled" + select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "agree_to_terms_policy", "sign_in", "sign_up", "social_sign_in", "social_sign_in_connector_targets", "sign_in_mode", "custom_css", "custom_content", "custom_ui_assets", "password_policy", "mfa", "single_sign_on_enabled" from "sign_in_experiences" where "id"=$1 `; diff --git a/packages/core/src/routes-me/user-assets.ts b/packages/core/src/routes-me/user-assets.ts index f80333774d9d..a667a26c5d87 100644 --- a/packages/core/src/routes-me/user-assets.ts +++ b/packages/core/src/routes-me/user-assets.ts @@ -8,6 +8,7 @@ import { type UserAssets, userAssetsGuard, adminTenantId, + uploadFileGuard, } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { format } from 'date-fns'; @@ -18,7 +19,6 @@ import koaGuard from '#src/middleware/koa-guard.js'; import type { RouterInitArgs } from '#src/routes/types.js'; import SystemContext from '#src/tenants/SystemContext.js'; import assertThat from '#src/utils/assert-that.js'; -import { uploadFileGuard } from '#src/utils/storage/consts.js'; import { buildUploadFile } from '#src/utils/storage/index.js'; import type { AuthedMeRouter } from './types.js'; diff --git a/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.openapi.json b/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.openapi.json new file mode 100644 index 000000000000..33c74c99de57 --- /dev/null +++ b/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.openapi.json @@ -0,0 +1,42 @@ +{ + "tags": [ + { + "name": "Custom UI assets", + "description": "Endpoints for uploading custom UI assets for the sign-in experience. Users can upload a zip file containing custom HTML, CSS, and JavaScript files to replace and fully customize the sign-in experience." + }, + { "name": "Cloud only" }, + { "name": "Dev feature" } + ], + "paths": { + "/api/sign-in-exp/default/custom-ui-assets": { + "post": { + "summary": "Upload custom UI assets", + "description": "Upload a zip file containing custom web assets such as HTML, CSS, and JavaScript files, then replace the default sign-in experience with the custom UI assets.", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "file": { + "description": "The zip file containing custom web assets such as HTML, CSS, and JavaScript files." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "An JSON object containing the custom UI assets ID." + }, + "400": { + "description": "Bad request. The request body is invalid." + }, + "500": { + "description": "Failed to unzip or upload the custom UI assets to storage provider." + } + } + } + } + } +} diff --git a/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.test.ts b/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.test.ts new file mode 100644 index 000000000000..ea25f642f8a9 --- /dev/null +++ b/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.test.ts @@ -0,0 +1,133 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { Readable } from 'node:stream'; +import { fileURLToPath } from 'node:url'; + +import { StorageProvider } from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import { createMockUtils, pickDefault } from '@logto/shared/esm'; +import AdmZip from 'adm-zip'; +import pRetry from 'p-retry'; +import { type Response } from 'supertest'; + +import SystemContext from '#src/tenants/SystemContext.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; +import { createRequester } from '#src/utils/test-utils.js'; + +const { jest } = import.meta; +const { mockEsmWithActual } = createMockUtils(jest); + +const experienceZipsProviderConfig = { + provider: StorageProvider.AzureStorage, + connectionString: 'connectionString', + container: 'container', +} satisfies { + provider: StorageProvider.AzureStorage; + connectionString: string; + container: string; +}; + +// eslint-disable-next-line @silverhand/fp/no-mutation +SystemContext.shared.experienceZipsProviderConfig = experienceZipsProviderConfig; + +const mockedIsFileExisted = jest.fn(async (filename: string) => false); +const mockedDownloadFile = jest.fn(); + +await mockEsmWithActual('#src/utils/storage/azure-storage.js', () => ({ + buildAzureStorage: () => ({ + uploadFile: jest.fn(async () => 'https://fake.url'), + downloadFile: mockedDownloadFile, + isFileExisted: mockedIsFileExisted, + }), +})); + +await mockEsmWithActual('#src/utils/tenant.js', () => ({ + getTenantId: jest.fn().mockResolvedValue(['default']), +})); + +await mockEsmWithActual('p-retry', () => ({ + // Stub pRetry by overriding the default "exponential backoff", + // in order to make the test run faster. + default: async (input: (retries: number) => T | PromiseLike) => + pRetry(input, { factor: 0 }), +})); + +const mockedGenerateStandardId = jest.fn(generateStandardId); + +await mockEsmWithActual('@logto/shared', () => ({ + generateStandardId: mockedGenerateStandardId, +})); + +const tenantContext = new MockTenant(); + +const signInExperiencesRoutes = await pickDefault(import('./index.js')); +const signInExperienceRequester = createRequester({ + authedRoutes: signInExperiencesRoutes, + tenantContext, +}); + +const currentPath = path.dirname(fileURLToPath(import.meta.url)); +const testFilesPath = path.join(currentPath, 'test-files'); +const pathToZip = path.join(testFilesPath, 'assets.zip'); + +const uploadCustomUiAssets = async (filePath: string): Promise => { + const response = await signInExperienceRequester + .post('/sign-in-exp/default/custom-ui-assets') + .field('name', 'file') + .attach('file', filePath); + + return response; +}; + +describe('POST /sign-in-exp/default/custom-ui-assets', () => { + beforeAll(async () => { + await fs.mkdir(testFilesPath); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + void fs.rm(testFilesPath, { force: true, recursive: true }); + }); + + it('should fail if upload file is not a zip', async () => { + const pathToTxt = path.join(testFilesPath, 'foo.txt'); + await fs.writeFile(pathToTxt, 'foo'); + const response = await uploadCustomUiAssets(pathToTxt); + + expect(response.status).toBe(400); + }); + + it('should upload custom ui assets', async () => { + mockedGenerateStandardId.mockReturnValue('custom-ui-asset-id'); + const zip = new AdmZip(); + zip.addFile('index.html', Buffer.from('')); + await zip.writeZipPromise(pathToZip); + const response = await uploadCustomUiAssets(pathToZip); + + expect(response.status).toBe(200); + expect(response.body.customUiAssetId).toBe('custom-ui-asset-id'); + }); + + it('should fail if the error.log file exists', async () => { + mockedIsFileExisted.mockImplementation(async (filename: string) => + filename.endsWith('error.log') + ); + mockedDownloadFile.mockImplementation(async () => ({ + readableStreamBody: Readable.from('Failed to unzip files!'), + })); + const response = await uploadCustomUiAssets(pathToZip); + expect(response.status).toBe(500); + expect(response.text).toBe('Failed to upload file to the storage provider.'); + }); + + it('should fail if the upload zip always persists (unzipping azure function does not trigger)', async () => { + mockedIsFileExisted.mockImplementation(async (filename) => filename.endsWith('assets.zip')); + const response = await uploadCustomUiAssets(pathToZip); + expect(response.status).toBe(500); + expect(response.text).toBe('Failed to upload file to the storage provider.'); + expect(mockedIsFileExisted).toHaveBeenCalledTimes(10); + }); +}); diff --git a/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.ts b/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.ts new file mode 100644 index 000000000000..16b7eca331ed --- /dev/null +++ b/packages/core/src/routes/sign-in-experience/custom-ui-assets/index.ts @@ -0,0 +1,115 @@ +import { readFile } from 'node:fs/promises'; + +import { uploadFileGuard, maxUploadFileSize } from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import pRetry, { AbortError } from 'p-retry'; +import { object, z } from 'zod'; + +import { EnvSet } from '#src/env-set/index.js'; +import RequestError from '#src/errors/RequestError/index.js'; +import koaGuard from '#src/middleware/koa-guard.js'; +import SystemContext from '#src/tenants/SystemContext.js'; +import assertThat from '#src/utils/assert-that.js'; +import { getConsoleLogFromContext } from '#src/utils/console.js'; +import { streamToString } from '#src/utils/file.js'; +import { buildAzureStorage } from '#src/utils/storage/azure-storage.js'; +import { getTenantId } from '#src/utils/tenant.js'; + +import { type ManagementApiRouter, type RouterInitArgs } from '../../types.js'; + +const maxRetryCount = 5; + +export default function customUiAssetsRoutes( + ...[router]: RouterInitArgs +) { + // TODO: Remove + if (!EnvSet.values.isDevFeaturesEnabled) { + return; + } + + router.post( + '/sign-in-exp/default/custom-ui-assets', + koaGuard({ + files: object({ + file: uploadFileGuard.array().min(1).max(1), + }), + response: z.object({ + customUiAssetId: z.string(), + }), + status: [200, 400, 500], + }), + async (ctx, next) => { + const { file: bodyFiles } = ctx.guard.files; + const file = bodyFiles[0]; + + assertThat(file, 'guard.invalid_input'); + assertThat(file.size <= maxUploadFileSize, 'guard.file_size_exceeded'); + assertThat(file.mimetype === 'application/zip', 'guard.mime_type_not_allowed'); + + const { experienceZipsProviderConfig } = SystemContext.shared; + assertThat( + experienceZipsProviderConfig?.provider === 'AzureStorage', + 'storage.not_configured' + ); + const { connectionString, container } = experienceZipsProviderConfig; + + const { uploadFile, downloadFile, isFileExisted } = buildAzureStorage( + connectionString, + container + ); + + const [tenantId] = await getTenantId(ctx.URL); + assertThat(tenantId, 'guard.can_not_get_tenant_id'); + + const customUiAssetId = generateStandardId(8); + const objectKey = `${tenantId}/${customUiAssetId}/assets.zip`; + const errorLogObjectKey = `${tenantId}/${customUiAssetId}/error.log`; + + try { + // Upload the zip file to `experience-zips` container, in which a blob trigger is configured, + // and an azure function will be executed automatically to unzip the file on blob received. + // If the unzipping process succeeds, the zip file will be removed and assets will be stored in + // `experience-blobs` container. If it fails, the error message will be written to `error.log` file. + await uploadFile(await readFile(file.filepath), objectKey, { + contentType: file.mimetype, + }); + + const hasUnzipCompleted = async (retryTimes: number) => { + if (retryTimes > maxRetryCount) { + throw new AbortError('Unzip timeout. Max retry count reached.'); + } + const [hasZip, hasError] = await Promise.all([ + isFileExisted(objectKey), + isFileExisted(errorLogObjectKey), + ]); + if (hasZip) { + throw new Error('Unzip in progress...'); + } + if (hasError) { + const errorLogBlob = await downloadFile(errorLogObjectKey); + const errorLog = await streamToString(errorLogBlob.readableStreamBody); + throw new AbortError(errorLog || 'Unzipping failed.'); + } + }; + + await pRetry(hasUnzipCompleted, { + retries: maxRetryCount, + }); + } catch (error: unknown) { + getConsoleLogFromContext(ctx).error(error); + throw new RequestError( + { + code: 'storage.upload_error', + status: 500, + }, + { + details: error instanceof Error ? error.message : String(error), + } + ); + } + + ctx.body = { customUiAssetId }; + return next(); + } + ); +} diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts index 3ccc2ceefbb1..9700158cdccc 100644 --- a/packages/core/src/routes/sign-in-experience/index.ts +++ b/packages/core/src/routes/sign-in-experience/index.ts @@ -8,9 +8,12 @@ import koaGuard from '#src/middleware/koa-guard.js'; import type { ManagementApiRouter, RouterInitArgs } from '../types.js'; +import customUiAssetsRoutes from './custom-ui-assets/index.js'; + export default function signInExperiencesRoutes( - ...[router, { queries, libraries, connectors }]: RouterInitArgs + ...args: RouterInitArgs ) { + const [router, { queries, libraries, connectors }] = args; const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences; const { deleteConnectorById } = queries.connectors; const { @@ -118,4 +121,6 @@ export default function signInExperiencesRoutes( return next(); } ); + + customUiAssetsRoutes(...args); } diff --git a/packages/core/src/routes/swagger/index.ts b/packages/core/src/routes/swagger/index.ts index 9973d7430c3a..f79d26a02c71 100644 --- a/packages/core/src/routes/swagger/index.ts +++ b/packages/core/src/routes/swagger/index.ts @@ -156,6 +156,7 @@ const additionalTags = Object.freeze( condArray( 'Organization applications', EnvSet.values.isDevFeaturesEnabled && 'Subject tokens', + EnvSet.values.isDevFeaturesEnabled && 'Custom UI assets', 'Organization users' ) ); diff --git a/packages/core/src/routes/swagger/utils/operation-id.ts b/packages/core/src/routes/swagger/utils/operation-id.ts index b65795657b79..b0702fdee700 100644 --- a/packages/core/src/routes/swagger/utils/operation-id.ts +++ b/packages/core/src/routes/swagger/utils/operation-id.ts @@ -27,6 +27,8 @@ type RouteDictionary = Record<`${OpenAPIV3.HttpMethods} ${string}`, string>; const devFeatureCustomRoutes: RouteDictionary = Object.freeze({ // Subject tokens 'post /subject-tokens': 'CreateSubjectToken', + // Custom UI assets + 'post /sign-in-exp/default/custom-ui-assets': 'UploadCustomUiAssets', }); export const customRoutes: Readonly = Object.freeze({ diff --git a/packages/core/src/routes/user-assets.ts b/packages/core/src/routes/user-assets.ts index cf1f7e637fd5..2867b0ad4330 100644 --- a/packages/core/src/routes/user-assets.ts +++ b/packages/core/src/routes/user-assets.ts @@ -6,6 +6,7 @@ import { userAssetsServiceStatusGuard, allowUploadMimeTypes, maxUploadFileSize, + uploadFileGuard, } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { format } from 'date-fns'; @@ -16,7 +17,6 @@ import koaGuard from '#src/middleware/koa-guard.js'; import SystemContext from '#src/tenants/SystemContext.js'; import assertThat from '#src/utils/assert-that.js'; import { getConsoleLogFromContext } from '#src/utils/console.js'; -import { uploadFileGuard } from '#src/utils/storage/consts.js'; import { buildUploadFile } from '#src/utils/storage/index.js'; import { getTenantId } from '#src/utils/tenant.js'; diff --git a/packages/core/src/tenants/SystemContext.ts b/packages/core/src/tenants/SystemContext.ts index 49bd393ca135..ed5bb17cd360 100644 --- a/packages/core/src/tenants/SystemContext.ts +++ b/packages/core/src/tenants/SystemContext.ts @@ -18,6 +18,8 @@ import { devConsole } from '#src/utils/console.js'; export default class SystemContext { static shared = new SystemContext(); public storageProviderConfig?: StorageProviderData; + public experienceBlobsProviderConfig?: StorageProviderData; + public experienceZipsProviderConfig?: StorageProviderData; public hostnameProviderConfig?: HostnameProviderData; public protectedAppConfigProviderConfig?: ProtectedAppConfigProviderData; public protectedAppHostnameProviderConfig?: HostnameProviderData; @@ -31,6 +33,20 @@ export default class SystemContext { storageProviderDataGuard ); })(), + (async () => { + this.experienceBlobsProviderConfig = await this.loadConfig( + pool, + StorageProviderKey.ExperienceBlobsProvider, + storageProviderDataGuard + ); + })(), + (async () => { + this.experienceZipsProviderConfig = await this.loadConfig( + pool, + StorageProviderKey.ExperienceZipsProvider, + storageProviderDataGuard + ); + })(), (async () => { this.hostnameProviderConfig = await this.loadConfig( pool, diff --git a/packages/core/src/utils/file.test.ts b/packages/core/src/utils/file.test.ts new file mode 100644 index 000000000000..8057a634c6d1 --- /dev/null +++ b/packages/core/src/utils/file.test.ts @@ -0,0 +1,16 @@ +import { Readable } from 'node:stream'; + +import { streamToString } from './file.js'; + +describe('streamToString()', () => { + it('should return an empty string if the stream is empty', async () => { + const result = await streamToString(); + expect(result).toBe(''); + }); + + it('should return the stream content as a string', async () => { + const stream = Readable.from(['Hello', ' ', 'world', '!']); + const result = await streamToString(stream); + expect(result).toBe('Hello world!'); + }); +}); diff --git a/packages/core/src/utils/file.ts b/packages/core/src/utils/file.ts new file mode 100644 index 000000000000..10a39584a160 --- /dev/null +++ b/packages/core/src/utils/file.ts @@ -0,0 +1,17 @@ +/** + * Read a Readable stream to a string + * @param stream - The Readable stream to read from + * @returns A promise that resolves to a string containing the stream's data + */ +export async function streamToString(stream?: NodeJS.ReadableStream): Promise { + if (!stream) { + return ''; + } + const chunks: Uint8Array[] = []; + for await (const chunk of stream) { + // eslint-disable-next-line @silverhand/fp/no-mutating-methods + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); + } + const buffer = Buffer.concat(chunks); + return buffer.toString('utf8'); +} diff --git a/packages/core/src/utils/storage/azure-storage.ts b/packages/core/src/utils/storage/azure-storage.ts index 8c763412d94c..ed03f26efa12 100644 --- a/packages/core/src/utils/storage/azure-storage.ts +++ b/packages/core/src/utils/storage/azure-storage.ts @@ -1,10 +1,17 @@ -import { BlobServiceClient } from '@azure/storage-blob'; +import { type BlobDownloadResponseParsed, BlobServiceClient } from '@azure/storage-blob'; import type { UploadFile } from './types.js'; const defaultPublicDomain = 'blob.core.windows.net'; -export const buildAzureStorage = (connectionString: string, container: string) => { +export const buildAzureStorage = ( + connectionString: string, + container: string +): { + uploadFile: UploadFile; + downloadFile: (objectKey: string) => Promise; + isFileExisted: (objectKey: string) => Promise; +} => { const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString); const containerClient = blobServiceClient.getContainerClient(container); @@ -24,5 +31,15 @@ export const buildAzureStorage = (connectionString: string, container: string) = }; }; - return { uploadFile }; + const downloadFile = async (objectKey: string) => { + const blockBlobClient = containerClient.getBlockBlobClient(objectKey); + return blockBlobClient.download(); + }; + + const isFileExisted = async (objectKey: string) => { + const blockBlobClient = containerClient.getBlockBlobClient(objectKey); + return blockBlobClient.exists(); + }; + + return { uploadFile, downloadFile, isFileExisted }; }; diff --git a/packages/core/src/utils/storage/consts.ts b/packages/core/src/utils/storage/consts.ts deleted file mode 100644 index 8e2b44fff8de..000000000000 --- a/packages/core/src/utils/storage/consts.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { number, object, string } from 'zod'; - -export const uploadFileGuard = object({ - filepath: string(), - mimetype: string(), - originalFilename: string(), - size: number(), -}); diff --git a/packages/experience/src/__mocks__/logto.tsx b/packages/experience/src/__mocks__/logto.tsx index 58219f6e6501..d7138716ef6c 100644 --- a/packages/experience/src/__mocks__/logto.tsx +++ b/packages/experience/src/__mocks__/logto.tsx @@ -106,7 +106,7 @@ export const mockSignInExperience: SignInExperience = { customCss: null, customContent: {}, agreeToTermsPolicy: AgreeToTermsPolicy.ManualRegistrationOnly, - customUiAssetId: null, + customUiAssets: null, passwordPolicy: {}, mfa: { policy: MfaPolicy.UserControlled, @@ -140,7 +140,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = { customCss: null, customContent: {}, agreeToTermsPolicy: mockSignInExperience.agreeToTermsPolicy, - customUiAssetId: null, + customUiAssets: null, passwordPolicy: {}, mfa: { policy: MfaPolicy.UserControlled, diff --git a/packages/schemas/alterations/next-1720505152-update-custom-ui-assets.ts b/packages/schemas/alterations/next-1720505152-update-custom-ui-assets.ts new file mode 100644 index 000000000000..03f95d168de9 --- /dev/null +++ b/packages/schemas/alterations/next-1720505152-update-custom-ui-assets.ts @@ -0,0 +1,20 @@ +import { sql } from '@silverhand/slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table sign_in_experiences drop column custom_ui_asset_id; + alter table sign_in_experiences add column custom_ui_assets jsonb; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table sign_in_experiences add column custom_ui_asset_id varchar(21); + alter table sign_in_experiences drop column custom_ui_assets; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/foundations/jsonb-types/sign-in-experience.ts b/packages/schemas/src/foundations/jsonb-types/sign-in-experience.ts index 56f365003ac9..1bd2897d2c72 100644 --- a/packages/schemas/src/foundations/jsonb-types/sign-in-experience.ts +++ b/packages/schemas/src/foundations/jsonb-types/sign-in-experience.ts @@ -109,3 +109,10 @@ export const mfaGuard = z.object({ }); export type Mfa = z.infer; + +export const customUiAssetsGuard = z.object({ + id: z.string(), + createdAt: z.number(), +}); + +export type CustomUiAssets = z.infer; diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index 866fe317b2f3..3a0a5ffc3df4 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -49,7 +49,7 @@ export const createDefaultSignInExperience = ( signInMode: SignInMode.SignInAndRegister, customCss: null, customContent: {}, - customUiAssetId: null, + customUiAssets: null, passwordPolicy: {}, mfa: { factors: [], diff --git a/packages/schemas/src/types/system.ts b/packages/schemas/src/types/system.ts index 5813caffdb6f..bf01b0bc1da7 100644 --- a/packages/schemas/src/types/system.ts +++ b/packages/schemas/src/types/system.ts @@ -61,16 +61,22 @@ export type StorageProviderData = z.infer; export enum StorageProviderKey { StorageProvider = 'storageProvider', + ExperienceBlobsProvider = 'experienceBlobsProvider', + ExperienceZipsProvider = 'experienceZipsProvider', } export type StorageProviderType = { [StorageProviderKey.StorageProvider]: StorageProviderData; + [StorageProviderKey.ExperienceBlobsProvider]: StorageProviderData; + [StorageProviderKey.ExperienceZipsProvider]: StorageProviderData; }; export const storageProviderGuard: Readonly<{ [key in StorageProviderKey]: ZodType; }> = Object.freeze({ [StorageProviderKey.StorageProvider]: storageProviderDataGuard, + [StorageProviderKey.ExperienceBlobsProvider]: storageProviderDataGuard, + [StorageProviderKey.ExperienceZipsProvider]: storageProviderDataGuard, }); // Email service provider diff --git a/packages/schemas/src/types/user-assets.ts b/packages/schemas/src/types/user-assets.ts index d6e2d4312c9a..99e92bc8fb46 100644 --- a/packages/schemas/src/types/user-assets.ts +++ b/packages/schemas/src/types/user-assets.ts @@ -32,3 +32,10 @@ export const userAssetsGuard = z.object({ }); export type UserAssets = z.infer; + +export const uploadFileGuard = z.object({ + filepath: z.string(), + mimetype: z.string(), + originalFilename: z.string(), + size: z.number(), +}); diff --git a/packages/schemas/tables/sign_in_experiences.sql b/packages/schemas/tables/sign_in_experiences.sql index b9f078103608..8e34aafd4043 100644 --- a/packages/schemas/tables/sign_in_experiences.sql +++ b/packages/schemas/tables/sign_in_experiences.sql @@ -19,7 +19,7 @@ create table sign_in_experiences ( sign_in_mode sign_in_mode not null default 'SignInAndRegister', custom_css text, custom_content jsonb /* @use CustomContent */ not null default '{}'::jsonb, - custom_ui_asset_id varchar(21), + custom_ui_assets jsonb /* @use CustomUiAssets */, password_policy jsonb /* @use PartialPasswordPolicy */ not null default '{}'::jsonb, mfa jsonb /* @use Mfa */ not null default '{}'::jsonb, single_sign_on_enabled boolean not null default false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43088bf0a603..591ce09c7819 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3365,6 +3365,9 @@ importers: '@silverhand/ts-config': specifier: 6.0.0 version: 6.0.0(typescript@5.3.3) + '@types/adm-zip': + specifier: ^0.5.5 + version: 0.5.5 '@types/debug': specifier: ^4.1.7 version: 4.1.7 @@ -3416,12 +3419,15 @@ importers: '@types/supertest': specifier: ^6.0.2 version: 6.0.2 + adm-zip: + specifier: ^0.5.14 + version: 0.5.14 eslint: specifier: ^8.56.0 version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.10.4) + version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) jest-matcher-specific-error: specifier: ^1.0.0 version: 1.0.0 @@ -3676,7 +3682,7 @@ importers: version: 3.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.12.7) + version: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -3685,7 +3691,7 @@ importers: version: 2.0.0 jest-transformer-svg: specifier: ^2.0.0 - version: 2.0.0(jest@29.7.0(@types/node@20.12.7))(react@18.2.0) + version: 2.0.0(jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)))(react@18.2.0) js-base64: specifier: ^3.7.5 version: 3.7.5 @@ -3824,7 +3830,7 @@ importers: version: 10.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.10.4) + version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) jest-matcher-specific-error: specifier: ^1.0.0 version: 1.0.0 @@ -6403,6 +6409,9 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/adm-zip@0.5.5': + resolution: {integrity: sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==} + '@types/aria-query@5.0.1': resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} @@ -6874,6 +6883,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.14: + resolution: {integrity: sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg==} + engines: {node: '>=12.0'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -14581,6 +14594,41 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.12.7 + 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.7.0 + jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/create-cache-key-function@27.5.1': dependencies: '@jest/types': 27.5.1 @@ -15902,10 +15950,10 @@ snapshots: eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-config-xo: 0.44.0(eslint@8.57.0) eslint-config-xo-typescript: 4.0.0(@typescript-eslint/eslint-plugin@7.7.0(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-consistent-default-export-name: 0.0.15 eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-n: 17.2.1(eslint@8.57.0) eslint-plugin-no-use-extend-native: 0.5.0 eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.0.0) @@ -16518,6 +16566,10 @@ snapshots: dependencies: '@types/estree': 1.0.5 + '@types/adm-zip@0.5.5': + dependencies: + '@types/node': 20.12.7 + '@types/aria-query@5.0.1': {} '@types/babel__core@7.1.19': @@ -17149,6 +17201,8 @@ snapshots: acorn@8.11.3: {} + adm-zip@0.5.14: {} + agent-base@6.0.2: dependencies: debug: 4.3.4 @@ -17913,13 +17967,13 @@ snapshots: dependencies: lodash.get: 4.4.2 - create-jest@29.7.0(@types/node@20.10.4): + create-jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.10.4) + jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -18727,13 +18781,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.3 is-core-module: 2.13.1 @@ -18744,14 +18798,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -18773,7 +18827,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -18783,7 +18837,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -20244,16 +20298,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.10.4): + jest-cli@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.10.4) + create-jest: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.4) + jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -20263,7 +20317,7 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@20.12.7): + jest-cli@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) '@jest/test-result': 29.7.0 @@ -20282,26 +20336,38 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): + jest-config@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) - '@jest/test-result': 29.7.0 + '@babel/core': 7.24.4 + '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.4) chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) - exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - yargs: 17.7.2 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.10.4 + ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3) transitivePeerDependencies: - - '@types/node' - babel-plugin-macros - supports-color - - ts-node - jest-config@29.7.0(@types/node@20.10.4): + jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): dependencies: '@babel/core': 7.24.4 '@jest/test-sequencer': 29.7.0 @@ -20326,12 +20392,13 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.10.4 + '@types/node': 20.12.7 + ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)): + jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): dependencies: '@babel/core': 7.24.4 '@jest/test-sequencer': 29.7.0 @@ -20357,7 +20424,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.12.7 - ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -20618,11 +20685,6 @@ snapshots: jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) react: 18.2.0 - jest-transformer-svg@2.0.0(jest@29.7.0(@types/node@20.12.7))(react@18.2.0): - dependencies: - jest: 29.7.0(@types/node@20.12.7) - react: 18.2.0 - jest-util@29.5.0: dependencies: '@jest/types': 29.6.3 @@ -20675,24 +20737,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.10.4): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) - '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.10.4) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest@29.7.0(@types/node@20.12.7): + jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.12.7) + jest-cli: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -24186,6 +24236,25 @@ snapshots: optionalDependencies: '@swc/core': 1.3.52(@swc/helpers@0.5.1) + ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.10.4 + acorn: 8.10.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 From ae4a12757ad0136b904ebe8b866608c5e8a57387 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Tue, 16 Jul 2024 10:55:48 +0800 Subject: [PATCH 022/135] test(core): add register integration tests (#6248) * test(core): add register integration tests add register integration tests * test: add enterprise sso integration tests add enterprise sso integration tests --- packages/core/src/routes/experience/index.ts | 2 +- .../src/client/experience/index.ts | 10 +- .../src/helpers/experience/index.ts | 131 ++++++++++++++++++ .../verification-code.test.ts | 101 ++++++++++++++ .../enterprise-sso.test.ts | 48 +++++++ .../sign-in-interaction/social.test.ts | 48 +++++++ .../verification-code.test.ts | 70 +++++++++- 7 files changed, 398 insertions(+), 12 deletions(-) create mode 100644 packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts create mode 100644 packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts create mode 100644 packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts diff --git a/packages/core/src/routes/experience/index.ts b/packages/core/src/routes/experience/index.ts index ec83da600bda..850ed02ef9ed 100644 --- a/packages/core/src/routes/experience/index.ts +++ b/packages/core/src/routes/experience/index.ts @@ -127,7 +127,7 @@ export default function experienceApiRoutes( router.post( `${experienceRoutes.prefix}/submit`, koaGuard({ - status: [200], + status: [200, 400], response: z.object({ redirectTo: z.string(), }), diff --git a/packages/integration-tests/src/client/experience/index.ts b/packages/integration-tests/src/client/experience/index.ts index 8a06e389f4f4..1b5217616ed5 100644 --- a/packages/integration-tests/src/client/experience/index.ts +++ b/packages/integration-tests/src/client/experience/index.ts @@ -26,12 +26,10 @@ export const identifyUser = async (cookie: string, payload: IdentificationApiPay export class ExperienceClient extends MockClient { public async identifyUser(payload: IdentificationApiPayload) { - return api - .post(experienceRoutes.identification, { - headers: { cookie: this.interactionCookie }, - json: payload, - }) - .json(); + return api.post(experienceRoutes.identification, { + headers: { cookie: this.interactionCookie }, + json: payload, + }); } public async updateInteractionEvent(payload: { interactionEvent: InteractionEvent }) { diff --git a/packages/integration-tests/src/helpers/experience/index.ts b/packages/integration-tests/src/helpers/experience/index.ts index 95082dda021d..71926a4986ca 100644 --- a/packages/integration-tests/src/helpers/experience/index.ts +++ b/packages/integration-tests/src/helpers/experience/index.ts @@ -2,6 +2,7 @@ * @fileoverview This file contains the successful interaction flow helper functions that use the experience APIs. */ +import { type SocialUserInfo } from '@logto/connector-kit'; import { InteractionEvent, SignInIdentifier, @@ -12,7 +13,12 @@ import { import { type ExperienceClient } from '#src/client/experience/index.js'; import { initExperienceClient, logoutClient, processSession } from '../client.js'; +import { expectRejects } from '../index.js'; +import { + successFullyCreateSocialVerification, + successFullyVerifySocialAuthorization, +} from './social-verification.js'; import { successfullySendVerificationCode, successfullyVerifyVerificationCode, @@ -96,3 +102,128 @@ export const identifyUserWithUsernamePassword = async ( return { verificationId }; }; + +export const registerNewUserWithVerificationCode = async ( + identifier: VerificationCodeIdentifier +) => { + const client = await initExperienceClient(); + + await client.initInteraction({ interactionEvent: InteractionEvent.Register }); + + const { verificationId, code } = await successfullySendVerificationCode(client, { + identifier, + interactionEvent: InteractionEvent.Register, + }); + + const verifiedVerificationId = await successfullyVerifyVerificationCode(client, { + identifier, + verificationId, + code, + }); + + await client.identifyUser({ + verificationId: verifiedVerificationId, + }); + + const { redirectTo } = await client.submitInteraction(); + + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + return userId; +}; + +/** + * + * @param socialUserInfo The social user info that will be returned by the social connector. + * @param registerNewUser Optional. If true, the user will be registered if the user does not exist, otherwise a error will be thrown if the user does not exist. + */ +export const signInWithSocial = async ( + connectorId: string, + socialUserInfo: SocialUserInfo, + registerNewUser = false +) => { + const state = 'state'; + const redirectUri = 'http://localhost:3000'; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + + const { verificationId } = await successFullyCreateSocialVerification(client, connectorId, { + redirectUri, + state, + }); + + await successFullyVerifySocialAuthorization(client, connectorId, { + verificationId, + connectorData: { + state, + redirectUri, + code: 'fake_code', + userId: socialUserInfo.id, + email: socialUserInfo.email, + }, + }); + + if (registerNewUser) { + await expectRejects(client.identifyUser({ verificationId }), { + code: 'user.identity_not_exist', + status: 404, + }); + + await client.updateInteractionEvent({ interactionEvent: InteractionEvent.Register }); + await client.identifyUser({ verificationId }); + } else { + await client.identifyUser({ + verificationId, + }); + } + + const { redirectTo } = await client.submitInteraction(); + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + return userId; +}; + +export const signInWithEnterpriseSso = async ( + connectorId: string, + enterpriseUserInfo: Record, + registerNewUser = false +) => { + const state = 'state'; + const redirectUri = 'http://localhost:3000'; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + + const { verificationId } = await client.getEnterpriseSsoAuthorizationUri(connectorId, { + redirectUri, + state, + }); + + await client.verifyEnterpriseSsoAuthorization(connectorId, { + verificationId, + connectorData: enterpriseUserInfo, + }); + + if (registerNewUser) { + await expectRejects(client.identifyUser({ verificationId }), { + code: 'user.identity_not_exist', + status: 404, + }); + + await client.updateInteractionEvent({ interactionEvent: InteractionEvent.Register }); + await client.identifyUser({ verificationId }); + } else { + await client.identifyUser({ + verificationId, + }); + } + + const { redirectTo } = await client.submitInteraction(); + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + return userId; +}; diff --git a/packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts b/packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts new file mode 100644 index 000000000000..c210797d41eb --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts @@ -0,0 +1,101 @@ +import { + InteractionEvent, + SignInIdentifier, + type VerificationCodeIdentifier, +} from '@logto/schemas'; + +import { deleteUser } from '#src/api/admin-user.js'; +import { initExperienceClient, logoutClient, processSession } from '#src/helpers/client.js'; +import { setEmailConnector, setSmsConnector } from '#src/helpers/connector.js'; +import { registerNewUserWithVerificationCode } from '#src/helpers/experience/index.js'; +import { + successfullySendVerificationCode, + successfullyVerifyVerificationCode, +} from '#src/helpers/experience/verification-code.js'; +import { expectRejects } from '#src/helpers/index.js'; +import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateNewUser } from '#src/helpers/user.js'; +import { devFeatureTest, generateEmail, generatePhone } from '#src/utils.js'; + +const verificationIdentifierType: readonly [SignInIdentifier.Email, SignInIdentifier.Phone] = + Object.freeze([SignInIdentifier.Email, SignInIdentifier.Phone]); + +const identifiersTypeToUserProfile = Object.freeze({ + email: 'primaryEmail', + phone: 'primaryPhone', +}); + +devFeatureTest.describe('Register interaction with verification code happy path', () => { + beforeAll(async () => { + await Promise.all([setEmailConnector(), setSmsConnector()]); + await enableAllVerificationCodeSignInMethods({ + identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone], + password: false, + verify: true, + }); + }); + + it.each(verificationIdentifierType)( + 'Should register with verification code using %p successfully', + async (identifier) => { + const userId = await registerNewUserWithVerificationCode({ + type: identifier, + value: identifier === SignInIdentifier.Email ? generateEmail() : generatePhone(), + }); + + await deleteUser(userId); + } + ); + + it.each(verificationIdentifierType)( + 'Should fail to sign-up with existing %p identifier and directly sign-in instead ', + async (identifierType) => { + const { userProfile, user } = await generateNewUser({ + [identifiersTypeToUserProfile[identifierType]]: true, + }); + + const identifier: VerificationCodeIdentifier = { + type: identifierType, + value: userProfile[identifiersTypeToUserProfile[identifierType]]!, + }; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.Register }); + + const { verificationId, code } = await successfullySendVerificationCode(client, { + identifier, + interactionEvent: InteractionEvent.Register, + }); + + await successfullyVerifyVerificationCode(client, { + identifier, + verificationId, + code, + }); + + await expectRejects( + client.identifyUser({ + verificationId, + }), + { + code: `user.${identifierType}_already_in_use`, + status: 422, + } + ); + + await client.updateInteractionEvent({ + interactionEvent: InteractionEvent.SignIn, + }); + + await client.identifyUser({ + verificationId, + }); + + const { redirectTo } = await client.submitInteraction(); + await processSession(client, redirectTo); + await logoutClient(client); + + await deleteUser(user.id); + } + ); +}); diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts new file mode 100644 index 000000000000..e86cc54ec461 --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts @@ -0,0 +1,48 @@ +import { generateStandardId } from '@logto/shared'; + +import { deleteUser, getUser } from '#src/api/admin-user.js'; +import { updateSignInExperience } from '#src/api/sign-in-experience.js'; +import { SsoConnectorApi } from '#src/api/sso-connector.js'; +import { signInWithEnterpriseSso } from '#src/helpers/experience/index.js'; +import { devFeatureTest, generateEmail } from '#src/utils.js'; + +devFeatureTest.describe('enterprise sso sign-in and sign-up', () => { + const ssoConnectorApi = new SsoConnectorApi(); + const domain = 'foo.com'; + const socialUserId = generateStandardId(); + const email = generateEmail(domain); + + beforeAll(async () => { + await ssoConnectorApi.createMockOidcConnector([domain]); + await updateSignInExperience({ singleSignOnEnabled: true }); + }); + + afterAll(async () => { + await ssoConnectorApi.cleanUp(); + }); + + it('should successfully sign-up with enterprise sso ans sync email', async () => { + const userId = await signInWithEnterpriseSso( + ssoConnectorApi.firstConnectorId!, + { + sub: socialUserId, + email, + email_verified: true, + }, + true + ); + + const { primaryEmail } = await getUser(userId); + expect(primaryEmail).toBe(email); + }); + + it('should successfully sign-in with enterprise sso', async () => { + const userId = await signInWithEnterpriseSso(ssoConnectorApi.firstConnectorId!, { + sub: socialUserId, + email, + email_verified: true, + }); + + await deleteUser(userId); + }); +}); diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts new file mode 100644 index 000000000000..6ae61112025a --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts @@ -0,0 +1,48 @@ +import { ConnectorType } from '@logto/connector-kit'; +import { generateStandardId } from '@logto/shared'; + +import { mockSocialConnectorId } from '#src/__mocks__/connectors-mock.js'; +import { deleteUser, getUser } from '#src/api/admin-user.js'; +import { clearConnectorsByTypes, setSocialConnector } from '#src/helpers/connector.js'; +import { signInWithSocial } from '#src/helpers/experience/index.js'; +import { devFeatureTest, generateEmail } from '#src/utils.js'; + +devFeatureTest.describe('social sign-in and sign-up', () => { + const connectorIdMap = new Map(); + const socialUserId = generateStandardId(); + const email = generateEmail(); + + beforeAll(async () => { + await clearConnectorsByTypes([ConnectorType.Social]); + + const { id: socialConnectorId } = await setSocialConnector(); + connectorIdMap.set(mockSocialConnectorId, socialConnectorId); + }); + + afterAll(async () => { + await clearConnectorsByTypes([ConnectorType.Social]); + }); + + it('should successfully sign-up with social and sync email', async () => { + const userId = await signInWithSocial( + connectorIdMap.get(mockSocialConnectorId)!, + { + id: socialUserId, + email, + }, + true + ); + + const { primaryEmail } = await getUser(userId); + expect(primaryEmail).toBe(email); + }); + + it('should successfully sign-in with social', async () => { + const userId = await signInWithSocial(connectorIdMap.get(mockSocialConnectorId)!, { + id: socialUserId, + email, + }); + + await deleteUser(userId); + }); +}); diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts index 4deb166f167e..5c2b92807f69 100644 --- a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts @@ -1,11 +1,21 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { + InteractionEvent, + SignInIdentifier, + type VerificationCodeIdentifier, +} from '@logto/schemas'; import { deleteUser } from '#src/api/admin-user.js'; +import { initExperienceClient, logoutClient, processSession } from '#src/helpers/client.js'; import { setEmailConnector, setSmsConnector } from '#src/helpers/connector.js'; import { signInWithVerificationCode } from '#src/helpers/experience/index.js'; +import { + successfullySendVerificationCode, + successfullyVerifyVerificationCode, +} from '#src/helpers/experience/verification-code.js'; +import { expectRejects } from '#src/helpers/index.js'; import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; import { generateNewUser } from '#src/helpers/user.js'; -import { devFeatureTest } from '#src/utils.js'; +import { devFeatureTest, generateEmail, generatePhone } from '#src/utils.js'; const verificationIdentifierType: readonly [SignInIdentifier.Email, SignInIdentifier.Phone] = Object.freeze([SignInIdentifier.Email, SignInIdentifier.Phone]); @@ -15,14 +25,18 @@ const identifiersTypeToUserProfile = Object.freeze({ phone: 'primaryPhone', }); -devFeatureTest.describe('Sign-in with verification code happy path', () => { +devFeatureTest.describe('Sign-in with verification code', () => { beforeAll(async () => { await Promise.all([setEmailConnector(), setSmsConnector()]); - await enableAllVerificationCodeSignInMethods(); + await enableAllVerificationCodeSignInMethods({ + identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone], + password: false, + verify: true, + }); }); it.each(verificationIdentifierType)( - 'Should sign-in with verification code using %p', + 'should sign-in with verification code using %p', async (identifier) => { const { userProfile, user } = await generateNewUser({ [identifiersTypeToUserProfile[identifier]]: true, @@ -37,4 +51,50 @@ devFeatureTest.describe('Sign-in with verification code happy path', () => { await deleteUser(user.id); } ); + + it.each(verificationIdentifierType)( + 'should fail to sign-in with non-existing %p identifier and directly sign-up instead', + async (type) => { + const identifier: VerificationCodeIdentifier = { + type, + value: type === SignInIdentifier.Email ? generateEmail() : generatePhone(), + }; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + + const { verificationId, code } = await successfullySendVerificationCode(client, { + identifier, + interactionEvent: InteractionEvent.SignIn, + }); + + await successfullyVerifyVerificationCode(client, { + identifier, + verificationId, + code, + }); + + await expectRejects( + client.identifyUser({ + verificationId, + }), + { + code: 'user.user_not_exist', + status: 404, + } + ); + + await client.updateInteractionEvent({ interactionEvent: InteractionEvent.Register }); + + await client.identifyUser({ + verificationId, + }); + + const { redirectTo } = await client.submitInteraction(); + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + await deleteUser(userId); + } + ); }); From 0fec4556f9b684634d3716d074f68d09b29ace4f Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 16 Jul 2024 14:55:47 +0800 Subject: [PATCH 023/135] feat(elements): add components --- packages/elements/index.html | 14 + packages/elements/package.json | 7 +- .../elements/src/components/logto-avatar.ts | 53 ++ .../elements/src/components/logto-button.ts | 67 ++ .../src/components/logto-card-section.ts | 3 +- .../elements/src/components/logto-card.ts | 10 + .../src/components/logto-form-card.ts | 23 +- .../elements/src/components/logto-list-row.ts | 50 ++ .../elements/src/components/logto-list.ts | 23 + .../src/elements/logto-profile-card.ts | 36 +- packages/elements/src/index.ts | 4 + packages/elements/src/react.ts | 13 +- packages/elements/src/utils/theme.ts | 29 +- packages/elements/web-dev-server.config.js | 21 + pnpm-lock.yaml | 683 +++++++++++++++++- 15 files changed, 1024 insertions(+), 12 deletions(-) create mode 100644 packages/elements/index.html create mode 100644 packages/elements/src/components/logto-avatar.ts create mode 100644 packages/elements/src/components/logto-button.ts create mode 100644 packages/elements/src/components/logto-list-row.ts create mode 100644 packages/elements/src/components/logto-list.ts create mode 100644 packages/elements/web-dev-server.config.js diff --git a/packages/elements/index.html b/packages/elements/index.html new file mode 100644 index 000000000000..93d3782ab4bb --- /dev/null +++ b/packages/elements/index.html @@ -0,0 +1,14 @@ + + + + + + Logto elements dev page + + + + + + + + diff --git a/packages/elements/package.json b/packages/elements/package.json index b9640c5ecdbd..a9bcfd3cdd54 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -8,6 +8,7 @@ "type": "module", "private": true, "main": "dist/index.js", + "module": "dist/index.js", "exports": { ".": { "import": "./dist/index.js", @@ -28,12 +29,14 @@ "scripts": { "precommit": "lint-staged", "build": "lit-localize build && tsup", + "start": "web-dev-server", "dev": "lit-localize build && tsup --watch --no-splitting", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", "test": "echo \"No tests yet.\"", "test:ci": "pnpm run test --silent --coverage", - "prepack": "pnpm check && pnpm build", + "prepublishOnly": "pnpm check", + "prepack": "pnpm build", "localize": "lit-localize", "check": "lit-localize extract && git add . -N && git diff --exit-code" }, @@ -53,6 +56,8 @@ "@lit/localize-tools": "^0.7.2", "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", + "@web/dev-server": "^0.4.6", + "@web/dev-server-esbuild": "^1.0.2", "eslint": "^8.56.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", diff --git a/packages/elements/src/components/logto-avatar.ts b/packages/elements/src/components/logto-avatar.ts new file mode 100644 index 000000000000..7ab7a82a5738 --- /dev/null +++ b/packages/elements/src/components/logto-avatar.ts @@ -0,0 +1,53 @@ +import { msg } from '@lit/localize'; +import { LitElement, css } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { unit } from '../utils/css.js'; + +const tagName = 'logto-avatar'; + +const sizes = Object.freeze({ + medium: unit(8), + large: unit(10), +}); + +@customElement(tagName) +export class LogtoAvatar extends LitElement { + static tagName = tagName; + static styles = css` + :host { + display: block; + border-radius: ${unit(2)}; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } + `; + + @property({ reflect: true }) + size: 'medium' | 'large' = 'medium'; + + @property({ reflect: true }) + src = ''; + + @property({ reflect: true }) + alt = msg('Avatar', { + id: 'account.profile.personal-info.avatar', + desc: 'The avatar of the user.', + }); + + connectedCallback(): void { + super.connectedCallback(); + this.style.setProperty('width', sizes[this.size].cssText); + this.style.setProperty('height', sizes[this.size].cssText); + + if (this.src) { + // Show the image holder with the provided image. + this.style.setProperty('background-color', '#adaab422'); + this.style.setProperty('background-image', `url(${this.src})`); + } else { + // A temporary default fallback color. Need to implement the relevant logic in `` later. + this.style.setProperty('background-color', '#e74c3c'); + } + } +} diff --git a/packages/elements/src/components/logto-button.ts b/packages/elements/src/components/logto-button.ts new file mode 100644 index 000000000000..78972cfbe7e0 --- /dev/null +++ b/packages/elements/src/components/logto-button.ts @@ -0,0 +1,67 @@ +import { LitElement, css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { unit } from '../utils/css.js'; +import { vars } from '../utils/theme.js'; + +const tagName = 'logto-button'; + +@customElement(tagName) +export class LogtoButton extends LitElement { + static tagName = tagName; + static styles = css` + :host { + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + outline: none; + font: ${vars.fontLabel2}; + transition: background-color 0.2s ease-in-out; + white-space: nowrap; + user-select: none; + position: relative; + text-decoration: none; + gap: ${unit(2)}; + cursor: pointer; + } + + :host(:disabled) { + cursor: not-allowed; + } + + :host([type='text']) { + background: none; + border-color: none; + font: ${vars.fontLabel2}; + color: ${vars.colorTextLink}; + padding: ${unit(0.5, 1)}; + border-radius: ${unit(1)}; + } + + :host([type='text']:disabled) { + color: ${vars.colorDisabled}; + } + + :host([type='text']:focus-visible) { + outline: 2px solid ${vars.colorFocusedVariant}; + } + + :host([type='text']:not(:disabled):hover) { + background: ${vars.colorHoverVariant}; + } + `; + + @property() + type: 'default' | 'text' = 'default'; + + connectedCallback(): void { + super.connectedCallback(); + this.role = 'button'; + this.tabIndex = 0; + } + + render() { + return html``; + } +} diff --git a/packages/elements/src/components/logto-card-section.ts b/packages/elements/src/components/logto-card-section.ts index de4e88be549c..3ce9da6af11f 100644 --- a/packages/elements/src/components/logto-card-section.ts +++ b/packages/elements/src/components/logto-card-section.ts @@ -7,6 +7,7 @@ import { vars } from '../utils/theme.js'; const tagName = 'logto-card-section'; +/** A section in a form card with a heading. It is used to group related content. */ @customElement(tagName) @localized() export class LogtoCardSection extends LitElement { @@ -14,7 +15,7 @@ export class LogtoCardSection extends LitElement { static styles = css` header { font: ${vars.fontLabel2}; - color: ${vars.colorText}; + color: ${vars.colorTextPrimary}; margin-bottom: ${unit(1)}; } `; diff --git a/packages/elements/src/components/logto-card.ts b/packages/elements/src/components/logto-card.ts index a6bde4ad6878..da67b41038fb 100644 --- a/packages/elements/src/components/logto-card.ts +++ b/packages/elements/src/components/logto-card.ts @@ -6,6 +6,16 @@ import { vars } from '../utils/theme.js'; const tagName = 'logto-card'; +/** + * A card with background, padding, and border radius. + * + * @example + * ```html + * + * + * + * ``` + */ @customElement(tagName) export class LogtoCard extends LitElement { static tagName = tagName; diff --git a/packages/elements/src/components/logto-form-card.ts b/packages/elements/src/components/logto-form-card.ts index dc02bbd99207..6252d8393a31 100644 --- a/packages/elements/src/components/logto-form-card.ts +++ b/packages/elements/src/components/logto-form-card.ts @@ -8,6 +8,26 @@ import { vars } from '../utils/theme.js'; const tagName = 'logto-form-card'; +/** + * A card that contains a form or form-like content. A heading and an optional description can be + * provided to describe the purpose of the content. + * + * To group related content in a form card, use the `logto-card-section` element. + * + * @example + * ```tsx + * html` + * + * + * + * + * + * + * + * + * ` + * ``` + */ @customElement(tagName) @localized() export class LogtoFormCard extends LitElement { @@ -30,7 +50,8 @@ export class LogtoFormCard extends LitElement { flex: 1; } - ::slotted(*) { + slot { + display: block; flex: 16; } `; diff --git a/packages/elements/src/components/logto-list-row.ts b/packages/elements/src/components/logto-list-row.ts new file mode 100644 index 000000000000..5faf2c45f994 --- /dev/null +++ b/packages/elements/src/components/logto-list-row.ts @@ -0,0 +1,50 @@ +import { localized, msg } from '@lit/localize'; +import { LitElement, css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import { unit } from '../utils/css.js'; +import { vars } from '../utils/theme.js'; + +const tagName = 'logto-list-row'; + +@customElement(tagName) +@localized() +export class LogtoListRow extends LitElement { + static tagName = tagName; + static styles = css` + :host { + box-sizing: border-box; + display: grid; + height: ${unit(16)}; + padding: ${unit(2, 6)}; + grid-template-columns: 1fr 2fr 1fr; + align-items: center; + color: ${vars.colorTextPrimary}; + font: ${vars.fontBody2}; + } + + :host(:not(:last-child)) { + border-bottom: 1px solid ${vars.colorLineDivider}; + } + + slot { + display: block; + } + + slot[name='title'] { + font: ${vars.fontLabel2}; + } + + slot[name='actions'] { + text-align: right; + } + `; + + render() { + return html` + {${msg('Title', { id: 'general.title' })}} + {${msg('Content', { id: 'general.content' })}} + {${msg('Actions', { id: 'general.actions' })}} + `; + } +} diff --git a/packages/elements/src/components/logto-list.ts b/packages/elements/src/components/logto-list.ts new file mode 100644 index 000000000000..a26ab1174732 --- /dev/null +++ b/packages/elements/src/components/logto-list.ts @@ -0,0 +1,23 @@ +import { LitElement, css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import { unit } from '../utils/css.js'; +import { vars } from '../utils/theme.js'; + +const tagName = 'logto-list'; + +@customElement(tagName) +export class LogtoList extends LitElement { + static tagName = tagName; + static styles = css` + :host { + display: block; + border-radius: ${unit(2)}; + border: 1px solid ${vars.colorLineDivider}; + } + `; + + render() { + return html``; + } +} diff --git a/packages/elements/src/elements/logto-profile-card.ts b/packages/elements/src/elements/logto-profile-card.ts index faa12273ad92..31fcc9bd3cf7 100644 --- a/packages/elements/src/elements/logto-profile-card.ts +++ b/packages/elements/src/elements/logto-profile-card.ts @@ -11,7 +11,7 @@ const tagName = 'logto-profile-card'; export class LogtoProfileCard extends LitElement { static tagName = tagName; static styles = css` - p { + p.dev { color: ${vars.colorTextSecondary}; } `; @@ -19,10 +19,42 @@ export class LogtoProfileCard extends LitElement { render() { return html` +

🚧 This section is a dev feature that is still working in progress.

-

🚧 This section is a dev feature that is still working in progress.

+ + +
+ ${msg('Avatar', { + id: 'account.profile.personal-info.avatar', + desc: 'The avatar of the user.', + })} +
+
+ +
+
+ + ${msg('Change', { id: 'general.change' })} + +
+
+ +
+ ${msg('Name', { + id: 'account.profile.personal-info.name', + desc: 'The name of the user.', + })} +
+
John Doe
+
+ + ${msg('Change', { id: 'general.change' })} + +
+
+
`; diff --git a/packages/elements/src/index.ts b/packages/elements/src/index.ts index de8f4983214e..9d2edf45a92e 100644 --- a/packages/elements/src/index.ts +++ b/packages/elements/src/index.ts @@ -1,6 +1,10 @@ +export * from './components/logto-avatar.js'; +export * from './components/logto-button.js'; export * from './components/logto-card-section.js'; export * from './components/logto-card.js'; export * from './components/logto-form-card.js'; +export * from './components/logto-list-row.js'; +export * from './components/logto-list.js'; export * from './components/logto-theme-provider.js'; export * from './elements/logto-profile-card.js'; export * from './utils/locale.js'; diff --git a/packages/elements/src/react.ts b/packages/elements/src/react.ts index cacc64ea1bff..28b363ae474b 100644 --- a/packages/elements/src/react.ts +++ b/packages/elements/src/react.ts @@ -1,6 +1,12 @@ import { createComponent } from '@lit/react'; -import { LogtoThemeProvider, LogtoCard, LogtoFormCard, LogtoProfileCard } from './index.js'; +import { + LogtoThemeProvider, + LogtoCard, + LogtoFormCard, + LogtoProfileCard, + LogtoList, +} from './index.js'; export * from './utils/locale.js'; @@ -11,6 +17,11 @@ export const createReactComponents = (react: Parameters[ elementClass: LogtoFormCard, react, }), + LogtoList: createComponent({ + tagName: LogtoList.tagName, + elementClass: LogtoList, + react, + }), LogtoProfileCard: createComponent({ tagName: LogtoProfileCard.tagName, elementClass: LogtoProfileCard, diff --git a/packages/elements/src/utils/theme.ts b/packages/elements/src/utils/theme.ts index 8eb806d1d7f2..41a72810fd5f 100644 --- a/packages/elements/src/utils/theme.ts +++ b/packages/elements/src/utils/theme.ts @@ -5,13 +5,18 @@ import { type KebabCase, kebabCase } from './string.js'; /** All the colors to be used in the Logto components and elements. */ export type Color = { colorPrimary: string; - colorText: string; + colorTextPrimary: string; colorTextLink: string; colorTextSecondary: string; colorBorder: string; colorCardTitle: string; colorLayer1: string; colorLayer2: string; + colorLineDivider: string; + colorDisabled: string; + colorHover: string; + colorHoverVariant: string; + colorFocusedVariant: string; }; /** All the fonts to be used in the Logto components and elements. */ @@ -19,6 +24,9 @@ export type Font = { fontLabel1: string; fontLabel2: string; fontLabel3: string; + fontBody1: string; + fontBody2: string; + fontBody3: string; fontSectionHeading1: string; fontSectionHeading2: string; }; @@ -33,6 +41,9 @@ export const defaultFont: Readonly = Object.freeze({ fontLabel1: `500 16px / 24px ${defaultFontFamily}`, fontLabel2: `500 14px / 20px ${defaultFontFamily}`, fontLabel3: `500 12px / 16px ${defaultFontFamily}`, + fontBody1: `400 16px / 24px ${defaultFontFamily}`, + fontBody2: `400 14px / 20px ${defaultFontFamily}`, + fontBody3: `400 12px / 16px ${defaultFontFamily}`, fontSectionHeading1: `700 12px / 16px ${defaultFontFamily}`, fontSectionHeading2: `700 10px / 16px ${defaultFontFamily}`, }); @@ -40,25 +51,35 @@ export const defaultFont: Readonly = Object.freeze({ export const defaultTheme: Readonly = Object.freeze({ ...defaultFont, colorPrimary: '#5d34f2', - colorText: '#191c1d', + colorTextPrimary: '#191c1d', colorTextLink: '#5d34f2', colorTextSecondary: '#747778', colorBorder: '#c4c7c7', colorCardTitle: '#928f9a', colorLayer1: '#000', colorLayer2: '#2d3132', + colorLineDivider: '#191c1d1f', + colorDisabled: '#5c5f60', + colorHover: '#191c1d14', + colorHoverVariant: '#5d34f214', + colorFocusedVariant: '#5d34f229', }); export const darkTheme: Readonly = Object.freeze({ ...defaultFont, colorPrimary: '#7958ff', - colorText: '#f7f8f8', + colorTextPrimary: '#f7f8f8', colorTextLink: '#cabeff', colorTextSecondary: '#a9acac', colorBorder: '#5c5f60', colorCardTitle: '#928f9a', colorLayer1: '#2a2c32', colorLayer2: '#34353f', + colorLineDivider: '#f7f8f824', + colorDisabled: '#5c5f60', + colorHover: '#f7f8f814', + colorHoverVariant: '#cabeff14', + colorFocusedVariant: '#cabeff29', }); /** @@ -109,7 +130,7 @@ export const toVar = (value: string) => unsafeCSS(`var(--logto-${kebabCase(value * ` */ // eslint-disable-next-line no-restricted-syntax -- `Object.fromEntries` will lose the type -export const vars = Object.freeze( +export const vars: Record = Object.freeze( Object.fromEntries(Object.keys(defaultTheme).map((key) => [key, toVar(key)])) ) as Record; diff --git a/packages/elements/web-dev-server.config.js b/packages/elements/web-dev-server.config.js new file mode 100644 index 000000000000..ea305a4b8494 --- /dev/null +++ b/packages/elements/web-dev-server.config.js @@ -0,0 +1,21 @@ +// eslint-disable-next-line unicorn/prevent-abbreviations +import { fileURLToPath } from 'node:url'; + +import { esbuildPlugin } from '@web/dev-server-esbuild'; + +const config = { + open: true, + watch: true, + appIndex: 'index.html', + nodeResolve: { + exportConditions: ['development'], + }, + plugins: [ + esbuildPlugin({ + ts: true, + tsconfig: fileURLToPath(new URL('tsconfig.json', import.meta.url)), + }), + ], +}; + +export default config; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cfa8d2677f6..cbbc3abc25c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3576,6 +3576,12 @@ importers: '@silverhand/ts-config': specifier: 6.0.0 version: 6.0.0(typescript@5.3.3) + '@web/dev-server': + specifier: ^0.4.6 + version: 0.4.6 + '@web/dev-server-esbuild': + specifier: ^1.0.2 + version: 1.0.2 eslint: specifier: ^8.56.0 version: 8.57.0 @@ -4243,6 +4249,10 @@ importers: packages: + '@75lb/deep-merge@1.1.1': + resolution: {integrity: sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==} + engines: {node: '>=12.17'} + '@aashutoshrathi/word-wrap@1.2.6': resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} @@ -4856,138 +4866,276 @@ packages: peerDependencies: postcss-selector-parser: ^6.0.13 + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -5300,6 +5448,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mdn/browser-compat-data@4.2.1': + resolution: {integrity: sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==} + '@mdx-js/mdx@3.0.1': resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} @@ -6497,6 +6648,9 @@ packages: '@types/color@3.0.3': resolution: {integrity: sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==} + '@types/command-line-args@5.2.3': + resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} + '@types/connect@3.4.35': resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} @@ -6662,6 +6816,9 @@ packages: '@types/parse-json@4.0.0': resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + '@types/parse5@6.0.3': + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + '@types/pg@8.11.2': resolution: {integrity: sha512-G2Mjygf2jFMU/9hCaTYxJrwdObdcnuQde1gndooZSOHsNSaCehAuwc7EIuSA34Do8Jx2yZ19KtvW8P0j4EuUXw==} @@ -6773,6 +6930,9 @@ packages: '@types/unist@3.0.0': resolution: {integrity: sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==} + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + '@types/yargs-parser@21.0.0': resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -6866,6 +7026,31 @@ packages: '@vitest/utils@2.0.0': resolution: {integrity: sha512-t0jbx8VugWEP6A29NbyfQKVU68Vo6oUw0iX3a8BwO3nrZuivfHcFO4Y5UsqXlplX+83P9UaqEvC2YQhspC0JSA==} + '@web/config-loader@0.3.1': + resolution: {integrity: sha512-IYjHXUgSGGNpO3YJQ9foLcazbJlAWDdJGRe9be7aOhon0Nd6Na5JIOJAej7jsMu76fKHr4b4w2LfIdNQ4fJ8pA==} + engines: {node: '>=18.0.0'} + + '@web/dev-server-core@0.7.2': + resolution: {integrity: sha512-Q/0jpF13Ipk+qGGQ+Yx/FW1TQBYazpkfgYHHo96HBE7qv4V4KKHqHglZcSUxti/zd4bToxX1cFTz8dmbTlb8JA==} + engines: {node: '>=18.0.0'} + + '@web/dev-server-esbuild@1.0.2': + resolution: {integrity: sha512-ak5mKt7L0H/Fa470Ku7p9A1eI32DNyFGM83jDkJviBO8r3lM00O5hVFW1K+UIYNC5EyanLyPxTqgtIuQEyMYcQ==} + engines: {node: '>=18.0.0'} + + '@web/dev-server-rollup@0.6.4': + resolution: {integrity: sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==} + engines: {node: '>=18.0.0'} + + '@web/dev-server@0.4.6': + resolution: {integrity: sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==} + engines: {node: '>=18.0.0'} + hasBin: true + + '@web/parse5-utils@2.1.0': + resolution: {integrity: sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==} + engines: {node: '>=18.0.0'} + '@withtyped/client@0.8.7': resolution: {integrity: sha512-qK+Tsczvko8mBRACtHGYj0CdMZFaBmosMGUahTAr544Jb183INPZPn/NpUFtTEpl5g3e4lUjMc5jPH0V78D0+g==} @@ -7036,6 +7221,14 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + + array-back@6.2.2: + resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} + engines: {node: '>=12.17'} + array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -7129,6 +7322,9 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -7359,6 +7555,10 @@ packages: resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} engines: {node: '>=12'} + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -7534,6 +7734,14 @@ packages: comma-separated-tokens@2.0.2: resolution: {integrity: sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==} + command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + + command-line-usage@7.0.2: + resolution: {integrity: sha512-MwNFB8nxi3IVnzir+nkSIbDTU4H6ne26zqicO2eTt1wPrvdOAphPhnYqWOjxXKWYLNYDu4Z/r2ESEziEqKuOVg==} + engines: {node: '>=12.20.0'} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -7945,6 +8153,9 @@ packages: dayjs@1.11.6: resolution: {integrity: sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==} + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -8033,6 +8244,10 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -8044,6 +8259,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + define-properties@1.1.4: resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} engines: {node: '>= 0.4'} @@ -8293,6 +8512,9 @@ packages: resolution: {integrity: sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==} engines: {node: '>= 0.4'} + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -8311,6 +8533,11 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -8741,6 +8968,10 @@ packages: resolution: {integrity: sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==} hasBin: true + find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -9373,6 +9604,10 @@ packages: resolution: {integrity: sha512-9hiJxE5gkK/cM2d1mTEnuurGTAoHebbkX0BYl3h7iEg7FYfuNIom+nDfBCSWtvSnoSrWCeBxqqBZu26xdlJlXA==} engines: {node: '>=12.0.0'} + internal-ip@6.2.0: + resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==} + engines: {node: '>=10'} + internal-slot@1.0.3: resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} engines: {node: '>= 0.4'} @@ -9384,12 +9619,20 @@ packages: internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + ip@1.1.9: resolution: {integrity: sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==} ip@2.0.1: resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipaddr.js@2.1.0: resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} engines: {node: '>= 10'} @@ -9463,6 +9706,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -9503,6 +9751,10 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} + is-ip@3.1.0: + resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==} + engines: {node: '>=8'} + is-js-type@2.0.0: resolution: {integrity: sha512-Aj13l47+uyTjlQNHtXBV8Cji3jb037vxwMWCgopRR8h6xocgBGW3qG8qGlIOEmbXQtkKShKuBM9e8AA1OeQ+xw==} @@ -9637,12 +9889,20 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isarray@0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbinaryfile@5.0.2: + resolution: {integrity: sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==} + engines: {node: '>= 18.0.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -10041,6 +10301,9 @@ packages: resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} engines: {node: '>= 10'} + koa-etag@4.0.0: + resolution: {integrity: sha512-1cSdezCkBWlyuB9l6c/IFoe1ANCDdPBxkDkRiaIup40xpUub6U/wwRXoKBZw/O5BifX9OlqAjYnDyzM6+l+TAg==} + koa-is-json@1.0.0: resolution: {integrity: sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==} @@ -10065,6 +10328,10 @@ packages: resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} engines: {node: '>= 8'} + koa-static@5.0.0: + resolution: {integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==} + engines: {node: '>= 7.6.0'} + koa@2.13.4: resolution: {integrity: sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==} engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} @@ -10210,6 +10477,9 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.assignwith@4.2.0: + resolution: {integrity: sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -10334,6 +10604,10 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru-cache@8.0.5: + resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} + engines: {node: '>=16.14'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -10732,6 +11006,10 @@ packages: resolution: {integrity: sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==} engines: {node: '>= 8.0.0'} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -10766,6 +11044,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanocolors@0.2.13: + resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -11000,6 +11281,10 @@ packages: only@0.0.2: resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + openapi-schema-validator@12.1.3: resolution: {integrity: sha512-xTHOmxU/VQGUgo7Cm0jhwbklOKobXby+/237EG967+3TQEYJztMgX9Q5UE2taZKwyKPUq0j11dngpGjUuxz1hQ==} @@ -11056,10 +11341,18 @@ packages: resolution: {integrity: sha512-Vb3QRvQ0Y5XnF40ZUWW7JfLogicVh/EnA5gBIvKDJoYpeI82+1E3AlB9yOcKFS0AhHrWVnAQO39fbR0G99IVEQ==} engines: {node: '>=12'} + p-event@4.2.0: + resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} + engines: {node: '>=8'} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -11104,6 +11397,10 @@ packages: resolution: {integrity: sha512-6NuuXu8Upembd4sNdo4PRbs+M6aHgBTrFE6lkH0YKjVzne3cDW4gkncB98ty/bkMxLxLVNeD5bX9FyWjM7WZ+A==} engines: {node: '>=16.17'} + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + p-timeout@6.1.2: resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} engines: {node: '>=14.16'} @@ -11158,6 +11455,9 @@ packages: resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} engines: {node: '>=0.10.0'} + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.1.1: resolution: {integrity: sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==} @@ -11329,6 +11629,10 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + portfinder@1.0.32: + resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} + engines: {node: '>= 0.12.0'} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -11568,6 +11872,10 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + puppeteer-core@22.6.5: resolution: {integrity: sha512-s0/5XkAWe0/dWISiljdrybjwDCHhgN31Nu/wznOZPKeikgcJtZtbvPKBz0t802XWqfSQnQDt3L6xiAE5JLlfuw==} engines: {node: '>=18'} @@ -12360,6 +12668,10 @@ packages: stream-events@1.0.5: resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + stream-read-all@3.0.1: + resolution: {integrity: sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==} + engines: {node: '>=10'} + stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} @@ -12575,6 +12887,11 @@ packages: resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} engines: {node: ^14.18.0 || >=16.0.0} + table-layout@3.0.2: + resolution: {integrity: sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==} + engines: {node: '>=12.17'} + hasBin: true + table@6.8.1: resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} engines: {node: '>=10.0.0'} @@ -12690,6 +13007,10 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -12859,6 +13180,14 @@ packages: engines: {node: '>=14.17'} hasBin: true + typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + + typical@7.1.1: + resolution: {integrity: sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==} + engines: {node: '>=12.17'} + ua-parser-js@1.0.37: resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} @@ -13101,6 +13430,10 @@ packages: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -13143,6 +13476,10 @@ packages: engines: {node: '>=8'} hasBin: true + wordwrapjs@5.1.0: + resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} + engines: {node: '>=12.17'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -13170,6 +13507,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'} @@ -13315,6 +13664,11 @@ packages: snapshots: + '@75lb/deep-merge@1.1.1': + dependencies: + lodash.assignwith: 4.2.0 + typical: 7.1.1 + '@aashutoshrathi/word-wrap@1.2.6': {} '@ampproject/remapping@2.3.0': @@ -14019,7 +14373,7 @@ snapshots: '@babel/code-frame@7.24.2': dependencies: '@babel/highlight': 7.24.2 - picocolors: 1.0.0 + picocolors: 1.0.1 '@babel/compat-data@7.24.4': {} @@ -14123,7 +14477,7 @@ snapshots: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.0 + picocolors: 1.0.1 '@babel/parser@7.24.4': dependencies: @@ -14530,72 +14884,141 @@ snapshots: dependencies: postcss-selector-parser: 6.0.16 + '@esbuild/aix-ppc64@0.19.12': + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/android-arm64@0.19.12': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm@0.19.12': + optional: true + '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-x64@0.19.12': + optional: true + '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.19.12': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-x64@0.19.12': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.19.12': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.19.12': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/linux-arm64@0.19.12': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm@0.19.12': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-ia32@0.19.12': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-loong64@0.19.12': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-mips64el@0.19.12': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-ppc64@0.19.12': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.19.12': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-s390x@0.19.12': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-x64@0.19.12': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.19.12': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.19.12': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.19.12': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/win32-arm64@0.19.12': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-ia32@0.19.12': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-x64@0.19.12': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true @@ -15081,6 +15504,8 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@mdn/browser-compat-data@4.2.1': {} + '@mdx-js/mdx@3.0.1': dependencies: '@types/estree': 1.0.5 @@ -16768,6 +17193,8 @@ snapshots: dependencies: '@types/color-convert': 2.0.0 + '@types/command-line-args@5.2.3': {} + '@types/connect@3.4.35': dependencies: '@types/node': 20.12.7 @@ -16968,6 +17395,8 @@ snapshots: '@types/parse-json@4.0.0': {} + '@types/parse5@6.0.3': {} + '@types/pg@8.11.2': dependencies: '@types/node': 20.12.7 @@ -17100,6 +17529,10 @@ snapshots: '@types/unist@3.0.0': {} + '@types/ws@7.4.7': + dependencies: + '@types/node': 20.12.7 + '@types/yargs-parser@21.0.0': {} '@types/yargs@16.0.4': @@ -17288,6 +17721,85 @@ snapshots: loupe: 3.1.1 pretty-format: 29.7.0 + '@web/config-loader@0.3.1': {} + + '@web/dev-server-core@0.7.2': + dependencies: + '@types/koa': 2.15.0 + '@types/ws': 7.4.7 + '@web/parse5-utils': 2.1.0 + chokidar: 3.5.3 + clone: 2.1.2 + es-module-lexer: 1.5.4 + get-stream: 6.0.1 + is-stream: 2.0.1 + isbinaryfile: 5.0.2 + koa: 2.15.3 + koa-etag: 4.0.0 + koa-send: 5.0.1 + koa-static: 5.0.0 + lru-cache: 8.0.5 + mime-types: 2.1.35 + parse5: 6.0.1 + picomatch: 2.3.1 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@web/dev-server-esbuild@1.0.2': + dependencies: + '@mdn/browser-compat-data': 4.2.1 + '@web/dev-server-core': 0.7.2 + esbuild: 0.19.12 + get-tsconfig: 4.7.3 + parse5: 6.0.1 + ua-parser-js: 1.0.37 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@web/dev-server-rollup@0.6.4': + dependencies: + '@rollup/plugin-node-resolve': 15.2.3(rollup@4.14.3) + '@web/dev-server-core': 0.7.2 + nanocolors: 0.2.13 + parse5: 6.0.1 + rollup: 4.14.3 + whatwg-url: 14.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@web/dev-server@0.4.6': + dependencies: + '@babel/code-frame': 7.24.2 + '@types/command-line-args': 5.2.3 + '@web/config-loader': 0.3.1 + '@web/dev-server-core': 0.7.2 + '@web/dev-server-rollup': 0.6.4 + camelcase: 6.3.0 + command-line-args: 5.2.1 + command-line-usage: 7.0.2 + debounce: 1.2.1 + deepmerge: 4.3.1 + internal-ip: 6.2.0 + nanocolors: 0.2.13 + open: 8.4.2 + portfinder: 1.0.32 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@web/parse5-utils@2.1.0': + dependencies: + '@types/parse5': 6.0.3 + parse5: 6.0.1 + '@withtyped/client@0.8.7(zod@3.23.8)': dependencies: '@withtyped/server': 0.13.6(zod@3.23.8) @@ -17457,6 +17969,10 @@ snapshots: dependencies: dequal: 2.0.3 + array-back@3.1.0: {} + + array-back@6.2.2: {} + array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -17581,6 +18097,10 @@ snapshots: dependencies: retry: 0.13.1 + async@2.6.4: + dependencies: + lodash: 4.17.21 + asynckit@0.4.0: {} attr-accept@2.2.2: {} @@ -17844,6 +18364,10 @@ snapshots: loupe: 3.1.1 pathval: 2.0.0 + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -18004,6 +18528,20 @@ snapshots: comma-separated-tokens@2.0.2: {} + command-line-args@5.2.1: + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + + command-line-usage@7.0.2: + dependencies: + array-back: 6.2.2 + chalk-template: 0.4.0 + table-layout: 3.0.2 + typical: 7.1.1 + commander@11.1.0: {} commander@4.1.1: {} @@ -18470,6 +19008,8 @@ snapshots: dayjs@1.11.6: {} + debounce@1.2.1: {} + debug@3.2.7(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -18523,6 +19063,10 @@ snapshots: deepmerge@4.3.1: {} + default-gateway@6.0.3: + dependencies: + execa: 5.1.1 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -18535,6 +19079,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.0.1 + define-lazy-prop@2.0.0: {} + define-properties@1.1.4: dependencies: has-property-descriptors: 1.0.0 @@ -18833,6 +19379,8 @@ snapshots: iterator.prototype: 1.1.2 safe-array-concat: 1.1.2 + es-module-lexer@1.5.4: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -18857,6 +19405,32 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -19421,6 +19995,10 @@ snapshots: transitivePeerDependencies: - supports-color + find-replace@3.0.0: + dependencies: + array-back: 3.1.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -20134,6 +20712,13 @@ snapshots: through: 2.3.8 wrap-ansi: 8.0.1 + internal-ip@6.2.0: + dependencies: + default-gateway: 6.0.3 + ipaddr.js: 1.9.1 + is-ip: 3.1.0 + p-event: 4.2.0 + internal-slot@1.0.3: dependencies: get-intrinsic: 1.2.4 @@ -20148,10 +20733,14 @@ snapshots: internmap@1.0.1: {} + ip-regex@4.3.0: {} + ip@1.1.9: {} ip@2.0.1: {} + ipaddr.js@1.9.1: {} + ipaddr.js@2.1.0: {} is-alphabetical@1.0.4: {} @@ -20224,6 +20813,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.0.2: @@ -20255,6 +20846,10 @@ snapshots: is-interactive@2.0.0: {} + is-ip@3.1.0: + dependencies: + ip-regex: 4.3.0 + is-js-type@2.0.0: dependencies: js-types: 1.0.0 @@ -20363,10 +20958,16 @@ snapshots: is-windows@1.0.2: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isarray@0.0.1: {} isarray@2.0.5: {} + isbinaryfile@5.0.2: {} + isexe@2.0.0: {} iso8601-duration@2.1.2: {} @@ -21139,6 +21740,10 @@ snapshots: co: 4.6.0 koa-compose: 4.1.0 + koa-etag@4.0.0: + dependencies: + etag: 1.8.1 + koa-is-json@1.0.0: {} koa-logger@3.2.1: @@ -21182,6 +21787,13 @@ snapshots: transitivePeerDependencies: - supports-color + koa-static@5.0.0: + dependencies: + debug: 3.2.7(supports-color@5.5.0) + koa-send: 5.0.1 + transitivePeerDependencies: + - supports-color + koa@2.13.4: dependencies: accepts: 1.3.7 @@ -21380,6 +21992,8 @@ snapshots: lodash-es@4.17.21: {} + lodash.assignwith@4.2.0: {} + lodash.camelcase@4.3.0: {} lodash.get@4.4.2: {} @@ -21484,6 +22098,8 @@ snapshots: lru-cache@7.18.3: {} + lru-cache@8.0.5: {} + lz-string@1.5.0: {} magic-string@0.30.10: @@ -22234,6 +22850,10 @@ snapshots: mixme@0.5.4: {} + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mkdirp@3.0.1: {} module-details-from-path@1.0.3: {} @@ -22270,6 +22890,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanocolors@0.2.13: {} + nanoid@3.3.7: {} nanoid@4.0.2: {} @@ -22516,6 +23138,12 @@ snapshots: only@0.0.2: {} + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + openapi-schema-validator@12.1.3: dependencies: ajv: 8.12.0 @@ -22585,10 +23213,16 @@ snapshots: p-defer@4.0.0: {} + p-event@4.2.0: + dependencies: + p-timeout: 3.2.0 + p-filter@2.1.0: dependencies: p-map: 2.1.0 + p-finally@1.0.0: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -22631,6 +23265,10 @@ snapshots: '@types/retry': 0.12.2 retry: 0.13.1 + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + p-timeout@6.1.2: {} p-try@2.2.0: {} @@ -22723,6 +23361,8 @@ snapshots: parse-passwd@1.0.0: {} + parse5@6.0.1: {} + parse5@7.1.1: dependencies: entities: 4.5.0 @@ -22871,6 +23511,14 @@ snapshots: pngjs@5.0.0: {} + portfinder@1.0.32: + dependencies: + async: 2.6.4 + debug: 3.2.7(supports-color@5.5.0) + mkdirp: 0.5.6 + transitivePeerDependencies: + - supports-color + possible-typed-array-names@1.0.0: {} postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.3.3)): @@ -23089,6 +23737,8 @@ snapshots: punycode@2.3.0: {} + punycode@2.3.1: {} + puppeteer-core@22.6.5: dependencies: '@puppeteer/browsers': 2.2.2 @@ -24021,6 +24671,8 @@ snapshots: dependencies: stubs: 3.0.0 + stream-read-all@3.0.1: {} + stream-shift@1.0.3: {} stream-transform@2.1.3: @@ -24320,6 +24972,16 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.6.2 + table-layout@3.0.2: + dependencies: + '@75lb/deep-merge': 1.1.1 + array-back: 6.2.2 + command-line-args: 5.2.1 + command-line-usage: 7.0.2 + stream-read-all: 3.0.1 + typical: 7.1.1 + wordwrapjs: 5.1.0 + table@6.8.1: dependencies: ajv: 8.12.0 @@ -24445,6 +25107,10 @@ snapshots: dependencies: punycode: 2.3.0 + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -24603,6 +25269,10 @@ snapshots: typescript@5.3.3: {} + typical@4.0.0: {} + + typical@7.1.1: {} + ua-parser-js@1.0.37: {} unbox-primitive@1.0.2: @@ -24978,6 +25648,11 @@ snapshots: tr46: 3.0.0 webidl-conversions: 7.0.0 + whatwg-url@14.0.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -25047,6 +25722,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wordwrapjs@5.1.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -25083,6 +25760,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@7.5.10: {} + ws@8.16.0: {} xml-crypto@3.0.1: From 0a9da5245b91ed1eae0f2e03fdb835e3ff803f99 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Tue, 16 Jul 2024 16:18:30 +0800 Subject: [PATCH 024/135] feat(core,schemas): implement the username password registration flow (#6249) * feat(core,schemas): implement the username password registration flow implement the username password registration flow * chore(core): update some comments update some comments * fix(test): fix integration tests fix integration tests * fix(test): fix lint fix lint --- packages/core/src/libraries/user.utils.ts | 2 +- .../classes/experience-interaction.ts | 6 + .../routes/experience/classes/utils.test.ts | 52 +++++++ .../src/routes/experience/classes/utils.ts | 36 +++++ .../classes/validators/profile-validator.ts | 21 ++- .../sign-in-experience-validator.test.ts | 15 +- .../sign-in-experience-validator.ts | 26 +++- .../verifications/code-verification.ts | 2 +- .../experience/classes/verifications/index.ts | 15 +- .../new-password-identity-verification.ts | 142 ++++++++++++++++++ .../verifications/password-verification.ts | 4 +- packages/core/src/routes/experience/index.ts | 2 + .../new-password-identity-verification.ts | 52 +++++++ .../src/client/experience/index.ts | 12 ++ .../src/helpers/experience/index.ts | 21 +++ .../username-password.test.ts | 40 +++++ ...new-password-identity-verification.test.ts | 127 ++++++++++++++++ packages/schemas/src/types/interactions.ts | 25 ++- 18 files changed, 589 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/routes/experience/classes/utils.test.ts create mode 100644 packages/core/src/routes/experience/classes/verifications/new-password-identity-verification.ts create mode 100644 packages/core/src/routes/experience/verification-routes/new-password-identity-verification.ts create mode 100644 packages/integration-tests/src/tests/api/experience-api/register-interaction/username-password.test.ts create mode 100644 packages/integration-tests/src/tests/api/experience-api/verifications/new-password-identity-verification.test.ts diff --git a/packages/core/src/libraries/user.utils.ts b/packages/core/src/libraries/user.utils.ts index be7a7d3cbc15..a6d73167fe6d 100644 --- a/packages/core/src/libraries/user.utils.ts +++ b/packages/core/src/libraries/user.utils.ts @@ -8,7 +8,7 @@ export const encryptUserPassword = async ( password: string ): Promise<{ passwordEncrypted: string; - passwordEncryptionMethod: UsersPasswordEncryptionMethod; + passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i; }> => { const passwordEncryptionMethod = UsersPasswordEncryptionMethod.Argon2i; const passwordEncrypted = await encryptPassword(password, passwordEncryptionMethod); diff --git a/packages/core/src/routes/experience/classes/experience-interaction.ts b/packages/core/src/routes/experience/classes/experience-interaction.ts index 16f80f602131..a5632b99679c 100644 --- a/packages/core/src/routes/experience/classes/experience-interaction.ts +++ b/packages/core/src/routes/experience/classes/experience-interaction.ts @@ -276,6 +276,12 @@ export default class ExperienceInteraction { this.userId = id; } + /** + * Create a new user using the verification record. + * + * @throws {RequestError} with 422 if the profile data is not unique across users + * @throws {RequestError} with 400 if the verification record is invalid for creating a new user or not verified + */ private async createNewUser(verificationRecord: VerificationRecord) { const { libraries: { diff --git a/packages/core/src/routes/experience/classes/utils.test.ts b/packages/core/src/routes/experience/classes/utils.test.ts new file mode 100644 index 000000000000..dbe1f3507546 --- /dev/null +++ b/packages/core/src/routes/experience/classes/utils.test.ts @@ -0,0 +1,52 @@ +import { type InteractionIdentifier, SignInIdentifier } from '@logto/schemas'; + +import { type InteractionProfile } from '../types.js'; + +import { interactionIdentifierToUserProfile, profileToUserInfo } from './utils.js'; + +const identifierToProfileTestCase = [ + { + identifier: { + type: SignInIdentifier.Username, + value: 'username', + }, + expected: { username: 'username' }, + }, + { + identifier: { + type: SignInIdentifier.Email, + value: 'email', + }, + expected: { primaryEmail: 'email' }, + }, + { + identifier: { + type: SignInIdentifier.Phone, + value: 'phone', + }, + expected: { primaryPhone: 'phone' }, + }, +] satisfies Array<{ identifier: InteractionIdentifier; expected: InteractionProfile }>; + +describe('experience utils tests', () => { + it.each(identifierToProfileTestCase)( + `interactionIdentifierToUserProfile %p`, + ({ identifier, expected }) => { + expect(interactionIdentifierToUserProfile(identifier)).toEqual(expected); + } + ); + it('profileToUserInfo', () => { + expect( + profileToUserInfo({ + username: 'username', + primaryEmail: 'email', + primaryPhone: 'phone', + }) + ).toEqual({ + name: undefined, + username: 'username', + email: 'email', + phoneNumber: 'phone', + }); + }); +}); diff --git a/packages/core/src/routes/experience/classes/utils.ts b/packages/core/src/routes/experience/classes/utils.ts index 4ddb0534a210..0a918ec8a5ec 100644 --- a/packages/core/src/routes/experience/classes/utils.ts +++ b/packages/core/src/routes/experience/classes/utils.ts @@ -1,3 +1,4 @@ +import { type UserInfo } from '@logto/core-kit'; import { SignInIdentifier, VerificationType, @@ -37,6 +38,7 @@ export const getNewUserProfileFromVerificationRecord = async ( verificationRecord: VerificationRecord ): Promise => { switch (verificationRecord.type) { + case VerificationType.NewPasswordIdentity: case VerificationType.VerificationCode: { return verificationRecord.toUserProfile(); } @@ -68,3 +70,37 @@ export const toUserSocialIdentityData = ( }, }; }; + +export function interactionIdentifierToUserProfile( + identifier: InteractionIdentifier +): { username: string } | { primaryEmail: string } | { primaryPhone: string } { + const { type, value } = identifier; + switch (type) { + case SignInIdentifier.Username: { + return { username: value }; + } + case SignInIdentifier.Email: { + return { primaryEmail: value }; + } + case SignInIdentifier.Phone: { + return { primaryPhone: value }; + } + } +} + +/** + * This function is used to convert the interaction profile to the UserInfo format. + * It will be used by the PasswordPolicyChecker to check the password policy against the user profile. + */ +export function profileToUserInfo( + profile: Pick +): UserInfo { + const { name, username, primaryEmail, primaryPhone } = profile; + + return { + name: name ?? undefined, + username: username ?? undefined, + email: primaryEmail ?? undefined, + phoneNumber: primaryPhone ?? undefined, + }; +} diff --git a/packages/core/src/routes/experience/classes/validators/profile-validator.ts b/packages/core/src/routes/experience/classes/validators/profile-validator.ts index a2c8bcb1ce0f..dc6a5848981b 100644 --- a/packages/core/src/routes/experience/classes/validators/profile-validator.ts +++ b/packages/core/src/routes/experience/classes/validators/profile-validator.ts @@ -1,3 +1,5 @@ +import { type PasswordPolicyChecker, type UserInfo } from '@logto/core-kit'; + import RequestError from '#src/errors/RequestError/index.js'; import type Libraries from '#src/tenants/Libraries.js'; import type Queries from '#src/tenants/Queries.js'; @@ -79,7 +81,24 @@ export class ProfileValidator { }) ); } + } - // TODO: Password validation + /** + * Validate password against the given password policy + * throw a {@link RequestError} -422 if the password is invalid; otherwise, do nothing. + */ + public async validatePassword( + password: string, + passwordPolicyChecker: PasswordPolicyChecker, + userInfo: UserInfo = {} + ) { + const issues = await passwordPolicyChecker.check( + password, + passwordPolicyChecker.policy.rejects.userInfo ? userInfo : {} + ); + + if (issues.length > 0) { + throw new RequestError({ code: 'password.rejected', status: 422 }, { issues }); + } } } diff --git a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts index f7f49f296cef..83adb6998428 100644 --- a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts +++ b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.test.ts @@ -14,6 +14,7 @@ import { MockTenant } from '#src/test-utils/tenant.js'; import { CodeVerification } from '../verifications/code-verification.js'; import { EnterpriseSsoVerification } from '../verifications/enterprise-sso-verification.js'; import { type VerificationRecord } from '../verifications/index.js'; +import { NewPasswordIdentityVerification } from '../verifications/new-password-identity-verification.js'; import { PasswordVerification } from '../verifications/password-verification.js'; import { SocialVerification } from '../verifications/social-verification.js'; @@ -32,6 +33,15 @@ const ssoConnectors = { const mockTenant = new MockTenant(undefined, { signInExperiences }, undefined, { ssoConnectors }); +const newPasswordIdentityVerificationRecord = NewPasswordIdentityVerification.create( + mockTenant.libraries, + mockTenant.queries, + { + type: SignInIdentifier.Username, + value: 'username', + } +); + const passwordVerificationRecords = Object.fromEntries( Object.values(SignInIdentifier).map((identifier) => [ identifier, @@ -326,7 +336,10 @@ describe('SignInExperienceValidator', () => { 'only username is enabled for sign-up': { signInExperience: mockSignInExperience, cases: [ - // TODO: username password registration + { + verificationRecord: newPasswordIdentityVerificationRecord, + accepted: true, + }, { verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email], accepted: false, diff --git a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts index 5bd7935ce2bb..25de8357eaf1 100644 --- a/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts +++ b/packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts @@ -1,3 +1,6 @@ +import crypto from 'node:crypto'; + +import { PasswordPolicyChecker } from '@logto/core-kit'; import { InteractionEvent, type SignInExperience, @@ -41,6 +44,7 @@ const getEmailIdentifierFromVerificationRecord = (verificationRecord: Verificati */ export class SignInExperienceValidator { private signInExperienceDataCache?: SignInExperience; + #passwordPolicyChecker?: PasswordPolicyChecker; constructor( private readonly libraries: Libraries, @@ -114,6 +118,15 @@ export class SignInExperienceValidator { return this.signInExperienceDataCache; } + public async getPasswordPolicyChecker() { + if (!this.#passwordPolicyChecker) { + const { passwordPolicy } = await this.getSignInExperienceData(); + this.#passwordPolicyChecker = new PasswordPolicyChecker(passwordPolicy, crypto.subtle); + } + + return this.#passwordPolicyChecker; + } + /** * Guard the verification records contains email identifier with SSO enabled * @@ -193,7 +206,18 @@ export class SignInExperienceValidator { const { signUp, singleSignOnEnabled } = await this.getSignInExperienceData(); switch (verificationRecord.type) { - // TODO: username password registration + // Username and password registration + case VerificationType.NewPasswordIdentity: { + const { + identifier: { type }, + } = verificationRecord; + + assertThat( + signUp.identifiers.includes(type) && signUp.password, + new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 }) + ); + break; + } case VerificationType.VerificationCode: { const { identifier: { type }, diff --git a/packages/core/src/routes/experience/classes/verifications/code-verification.ts b/packages/core/src/routes/experience/classes/verifications/code-verification.ts index e536cc4d2ffb..e9879c9726cd 100644 --- a/packages/core/src/routes/experience/classes/verifications/code-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/code-verification.ts @@ -187,7 +187,7 @@ export class CodeVerification return user; } - async toUserProfile(): Promise<{ primaryEmail: string } | { primaryPhone: string }> { + toUserProfile(): { primaryEmail: string } | { primaryPhone: string } { assertThat( this.verified, new RequestError({ code: 'session.verification_failed', status: 400 }) diff --git a/packages/core/src/routes/experience/classes/verifications/index.ts b/packages/core/src/routes/experience/classes/verifications/index.ts index a82cce2ec479..8522de9a9cdf 100644 --- a/packages/core/src/routes/experience/classes/verifications/index.ts +++ b/packages/core/src/routes/experience/classes/verifications/index.ts @@ -19,6 +19,11 @@ import { enterPriseSsoVerificationRecordDataGuard, type EnterpriseSsoVerificationRecordData, } from './enterprise-sso-verification.js'; +import { + NewPasswordIdentityVerification, + newPasswordIdentityVerificationRecordDataGuard, + type NewPasswordIdentityVerificationRecordData, +} from './new-password-identity-verification.js'; import { PasswordVerification, passwordVerificationRecordDataGuard, @@ -41,7 +46,8 @@ export type VerificationRecordData = | SocialVerificationRecordData | EnterpriseSsoVerificationRecordData | TotpVerificationRecordData - | BackupCodeVerificationRecordData; + | BackupCodeVerificationRecordData + | NewPasswordIdentityVerificationRecordData; /** * Union type for all verification record types @@ -57,7 +63,8 @@ export type VerificationRecord = | SocialVerification | EnterpriseSsoVerification | TotpVerification - | BackupCodeVerification; + | BackupCodeVerification + | NewPasswordIdentityVerification; export const verificationRecordDataGuard = z.discriminatedUnion('type', [ passwordVerificationRecordDataGuard, @@ -66,6 +73,7 @@ export const verificationRecordDataGuard = z.discriminatedUnion('type', [ enterPriseSsoVerificationRecordDataGuard, totpVerificationRecordDataGuard, backupCodeVerificationRecordDataGuard, + newPasswordIdentityVerificationRecordDataGuard, ]); /** @@ -95,5 +103,8 @@ export const buildVerificationRecord = ( case VerificationType.BackupCode: { return new BackupCodeVerification(libraries, queries, data); } + case VerificationType.NewPasswordIdentity: { + return new NewPasswordIdentityVerification(libraries, queries, data); + } } }; diff --git a/packages/core/src/routes/experience/classes/verifications/new-password-identity-verification.ts b/packages/core/src/routes/experience/classes/verifications/new-password-identity-verification.ts new file mode 100644 index 000000000000..bb40c3b1284d --- /dev/null +++ b/packages/core/src/routes/experience/classes/verifications/new-password-identity-verification.ts @@ -0,0 +1,142 @@ +import { type ToZodObject } from '@logto/connector-kit'; +import { type PasswordPolicyChecker } from '@logto/core-kit'; +import { + type InteractionIdentifier, + interactionIdentifierGuard, + UsersPasswordEncryptionMethod, + VerificationType, +} from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import { z } from 'zod'; + +import RequestError from '#src/errors/RequestError/index.js'; +import { encryptUserPassword } from '#src/libraries/user.utils.js'; +import type Libraries from '#src/tenants/Libraries.js'; +import type Queries from '#src/tenants/Queries.js'; +import assertThat from '#src/utils/assert-that.js'; + +import { interactionIdentifierToUserProfile, profileToUserInfo } from '../utils.js'; +import { ProfileValidator } from '../validators/profile-validator.js'; + +import { type VerificationRecord } from './verification-record.js'; + +export type NewPasswordIdentityVerificationRecordData = { + id: string; + type: VerificationType.NewPasswordIdentity; + /** + * For now we only support username identifier for new password identity registration. + * For email and phone new identity registration, a `CodeVerification` record is required. + */ + identifier: InteractionIdentifier; + passwordEncrypted?: string; + passwordEncryptionMethod?: UsersPasswordEncryptionMethod.Argon2i; +}; + +export const newPasswordIdentityVerificationRecordDataGuard = z.object({ + id: z.string(), + type: z.literal(VerificationType.NewPasswordIdentity), + identifier: interactionIdentifierGuard, + passwordEncrypted: z.string().optional(), + passwordEncryptionMethod: z.literal(UsersPasswordEncryptionMethod.Argon2i).optional(), +}) satisfies ToZodObject; + +/** + * NewPasswordIdentityVerification class is used for creating a new user using password + identifier. + * + * @remarks This verification record can only be used for new user registration. + * By default this verification record allows all types of identifiers, username, email, and phone. + * But in our current product design, only username + password registration is supported. The identifier type + * will be guarded at the API level. + */ +export class NewPasswordIdentityVerification + implements VerificationRecord +{ + /** Factory method to create a new `NewPasswordIdentityVerification` record using an identifier */ + static create(libraries: Libraries, queries: Queries, identifier: InteractionIdentifier) { + return new NewPasswordIdentityVerification(libraries, queries, { + id: generateStandardId(), + type: VerificationType.NewPasswordIdentity, + identifier, + }); + } + + readonly type = VerificationType.NewPasswordIdentity; + readonly id: string; + readonly identifier: InteractionIdentifier; + + private passwordEncrypted?: string; + private passwordEncryptionMethod?: UsersPasswordEncryptionMethod.Argon2i; + + private readonly profileValidator: ProfileValidator; + + constructor( + private readonly libraries: Libraries, + private readonly queries: Queries, + data: NewPasswordIdentityVerificationRecordData + ) { + const { id, identifier, passwordEncrypted, passwordEncryptionMethod } = data; + + this.id = id; + this.identifier = identifier; + this.passwordEncrypted = passwordEncrypted; + this.passwordEncryptionMethod = passwordEncryptionMethod; + this.profileValidator = new ProfileValidator(libraries, queries); + } + + get isVerified() { + return Boolean(this.passwordEncrypted) && Boolean(this.passwordEncryptionMethod); + } + + /** + * Verify the new password identity + * + * - Check if the identifier is unique across users + * - Validate the password against the password policy + */ + async verify(password: string, passwordPolicyChecker: PasswordPolicyChecker) { + const { identifier } = this; + const identifierProfile = interactionIdentifierToUserProfile(identifier); + + await this.profileValidator.guardProfileUniquenessAcrossUsers(identifierProfile); + + await this.profileValidator.validatePassword( + password, + passwordPolicyChecker, + profileToUserInfo(identifierProfile) + ); + + const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); + + this.passwordEncrypted = passwordEncrypted; + this.passwordEncryptionMethod = passwordEncryptionMethod; + } + + toUserProfile() { + assertThat( + this.passwordEncrypted && this.passwordEncryptionMethod, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { identifier, passwordEncrypted, passwordEncryptionMethod } = this; + + const identifierProfile = interactionIdentifierToUserProfile(identifier); + + return { + ...identifierProfile, + passwordEncrypted, + passwordEncryptionMethod, + }; + } + + toJson(): NewPasswordIdentityVerificationRecordData { + const { id, type, identifier, passwordEncrypted, passwordEncryptionMethod } = this; + + return { + id, + type, + identifier, + passwordEncrypted, + passwordEncryptionMethod, + }; + } +} diff --git a/packages/core/src/routes/experience/classes/verifications/password-verification.ts b/packages/core/src/routes/experience/classes/verifications/password-verification.ts index 60d25da7ec55..edfc5e953e82 100644 --- a/packages/core/src/routes/experience/classes/verifications/password-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/password-verification.ts @@ -45,8 +45,8 @@ export class PasswordVerification } readonly type = VerificationType.Password; - public readonly identifier: InteractionIdentifier; - public readonly id: string; + readonly identifier: InteractionIdentifier; + readonly id: string; private verified: boolean; /** diff --git a/packages/core/src/routes/experience/index.ts b/packages/core/src/routes/experience/index.ts index 850ed02ef9ed..7a2fab5a508a 100644 --- a/packages/core/src/routes/experience/index.ts +++ b/packages/core/src/routes/experience/index.ts @@ -26,6 +26,7 @@ import koaExperienceInteraction, { } from './middleware/koa-experience-interaction.js'; import backupCodeVerificationRoutes from './verification-routes/backup-code-verification.js'; import enterpriseSsoVerificationRoutes from './verification-routes/enterprise-sso-verification.js'; +import newPasswordIdentityVerificationRoutes from './verification-routes/new-password-identity-verification.js'; import passwordVerificationRoutes from './verification-routes/password-verification.js'; import socialVerificationRoutes from './verification-routes/social-verification.js'; import totpVerificationRoutes from './verification-routes/totp-verification.js'; @@ -145,4 +146,5 @@ export default function experienceApiRoutes( enterpriseSsoVerificationRoutes(router, tenant); totpVerificationRoutes(router, tenant); backupCodeVerificationRoutes(router, tenant); + newPasswordIdentityVerificationRoutes(router, tenant); } diff --git a/packages/core/src/routes/experience/verification-routes/new-password-identity-verification.ts b/packages/core/src/routes/experience/verification-routes/new-password-identity-verification.ts new file mode 100644 index 000000000000..d6def4b28ab3 --- /dev/null +++ b/packages/core/src/routes/experience/verification-routes/new-password-identity-verification.ts @@ -0,0 +1,52 @@ +import { newPasswordIdentityVerificationPayloadGuard } from '@logto/schemas'; +import type Router from 'koa-router'; +import { z } from 'zod'; + +import { type WithLogContext } from '#src/middleware/koa-audit-log.js'; +import koaGuard from '#src/middleware/koa-guard.js'; +import type TenantContext from '#src/tenants/TenantContext.js'; + +import { NewPasswordIdentityVerification } from '../classes/verifications/new-password-identity-verification.js'; +import { experienceRoutes } from '../const.js'; +import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js'; + +export default function newPasswordIdentityVerificationRoutes( + router: Router>, + { libraries, queries }: TenantContext +) { + router.post( + `${experienceRoutes.verification}/new-password-identity`, + koaGuard({ + body: newPasswordIdentityVerificationPayloadGuard, + status: [200, 400, 422], + response: z.object({ + verificationId: z.string(), + }), + }), + async (ctx, next) => { + const { identifier, password } = ctx.guard.body; + const { experienceInteraction } = ctx; + + const newPasswordIdentityVerification = NewPasswordIdentityVerification.create( + libraries, + queries, + identifier + ); + + const policyChecker = + await experienceInteraction.signInExperienceValidator.getPasswordPolicyChecker(); + + await newPasswordIdentityVerification.verify(password, policyChecker); + + ctx.experienceInteraction.setVerificationRecord(newPasswordIdentityVerification); + + await ctx.experienceInteraction.save(); + + ctx.body = { verificationId: newPasswordIdentityVerification.id }; + + ctx.status = 200; + + return next(); + } + ); +} diff --git a/packages/integration-tests/src/client/experience/index.ts b/packages/integration-tests/src/client/experience/index.ts index 1b5217616ed5..b52713e55232 100644 --- a/packages/integration-tests/src/client/experience/index.ts +++ b/packages/integration-tests/src/client/experience/index.ts @@ -2,6 +2,7 @@ import { type CreateExperienceApiPayload, type IdentificationApiPayload, type InteractionEvent, + type NewPasswordIdentityVerificationPayload, type PasswordVerificationPayload, type VerificationCodeIdentifier, } from '@logto/schemas'; @@ -184,4 +185,15 @@ export class ExperienceClient extends MockClient { }) .json<{ verificationId: string }>(); } + + public async createNewPasswordIdentityVerification( + payload: NewPasswordIdentityVerificationPayload + ) { + return api + .post(`${experienceRoutes.verification}/new-password-identity`, { + headers: { cookie: this.interactionCookie }, + json: payload, + }) + .json<{ verificationId: string }>(); + } } diff --git a/packages/integration-tests/src/helpers/experience/index.ts b/packages/integration-tests/src/helpers/experience/index.ts index 71926a4986ca..8aa7f6523b5e 100644 --- a/packages/integration-tests/src/helpers/experience/index.ts +++ b/packages/integration-tests/src/helpers/experience/index.ts @@ -227,3 +227,24 @@ export const signInWithEnterpriseSso = async ( return userId; }; + +export const registerNewUserUsernamePassword = async (username: string, password: string) => { + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.Register }); + + const { verificationId } = await client.createNewPasswordIdentityVerification({ + identifier: { + type: SignInIdentifier.Username, + value: username, + }, + password, + }); + + await client.identifyUser({ verificationId }); + + const { redirectTo } = await client.submitInteraction(); + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + return userId; +}; diff --git a/packages/integration-tests/src/tests/api/experience-api/register-interaction/username-password.test.ts b/packages/integration-tests/src/tests/api/experience-api/register-interaction/username-password.test.ts new file mode 100644 index 000000000000..076cad733cd3 --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/register-interaction/username-password.test.ts @@ -0,0 +1,40 @@ +import { SignInIdentifier } from '@logto/schemas'; + +import { deleteUser } from '#src/api/admin-user.js'; +import { updateSignInExperience } from '#src/api/sign-in-experience.js'; +import { + registerNewUserUsernamePassword, + signInWithPassword, +} from '#src/helpers/experience/index.js'; +import { generateNewUserProfile } from '#src/helpers/user.js'; +import { devFeatureTest } from '#src/utils.js'; + +devFeatureTest.describe('register new user with username and password', () => { + const { username, password } = generateNewUserProfile({ username: true, password: true }); + + beforeAll(async () => { + // Disable password policy here to make sure the test is not affected by the password policy. + await updateSignInExperience({ + signUp: { + identifiers: [SignInIdentifier.Username], + password: true, + verify: false, + }, + passwordPolicy: {}, + }); + }); + + it('should register new user with username and password and able to sign-in using the same credentials', async () => { + const userId = await registerNewUserUsernamePassword(username, password); + + await signInWithPassword({ + identifier: { + type: SignInIdentifier.Username, + value: username, + }, + password, + }); + + await deleteUser(userId); + }); +}); diff --git a/packages/integration-tests/src/tests/api/experience-api/verifications/new-password-identity-verification.test.ts b/packages/integration-tests/src/tests/api/experience-api/verifications/new-password-identity-verification.test.ts new file mode 100644 index 000000000000..48187b208bb8 --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/verifications/new-password-identity-verification.test.ts @@ -0,0 +1,127 @@ +import { SignInIdentifier } from '@logto/schemas'; + +import { updateSignInExperience } from '#src/api/sign-in-experience.js'; +import { initExperienceClient } from '#src/helpers/client.js'; +import { expectRejects } from '#src/helpers/index.js'; +import { generateNewUser } from '#src/helpers/user.js'; +import { devFeatureTest, randomString } from '#src/utils.js'; + +const invalidIdentifiers = Object.freeze([ + { + type: SignInIdentifier.Email, + value: 'email', + }, + { + type: SignInIdentifier.Phone, + value: 'phone', + }, +]); + +devFeatureTest.describe('password verifications', () => { + const username = 'test_' + randomString(); + + beforeAll(async () => { + await updateSignInExperience({ + passwordPolicy: { + length: { min: 8, max: 32 }, + characterTypes: { min: 3 }, + rejects: { + pwned: true, + repetitionAndSequence: true, + userInfo: true, + words: [username], + }, + }, + }); + }); + + afterAll(async () => { + await updateSignInExperience({ + // Need to reset password policy to default value otherwise it will affect other tests. + passwordPolicy: {}, + }); + }); + + it.each(invalidIdentifiers)( + 'should fail to verify with password using %p', + async (identifier) => { + const client = await initExperienceClient(); + + await expectRejects( + client.createNewPasswordIdentityVerification({ + // @ts-expect-error + identifier, + password: 'password', + }), + { + code: 'guard.invalid_input', + status: 400, + } + ); + } + ); + + it('should throw error if username is registered', async () => { + const { userProfile } = await generateNewUser({ username: true, password: true }); + const { username } = userProfile; + + const client = await initExperienceClient(); + + await expectRejects( + client.createNewPasswordIdentityVerification({ + identifier: { + type: SignInIdentifier.Username, + value: username, + }, + password: 'password', + }), + { + code: 'user.username_already_in_use', + status: 422, + } + ); + }); + + describe('password policy check', () => { + const invalidPasswords: Array<[string, string | RegExp]> = [ + ['123', 'minimum length'], + ['12345678', 'at least 3 types'], + ['123456aA', 'simple password'], + ['defghiZ@', 'sequential characters'], + ['TTTTTT@z', 'repeated characters'], + [username, 'userInfo'], + ]; + + it.each(invalidPasswords)('should reject invalid password %p', async (password) => { + const client = await initExperienceClient(); + + await expectRejects( + client.createNewPasswordIdentityVerification({ + identifier: { + type: SignInIdentifier.Username, + value: username, + }, + password, + }), + { + code: `password.rejected`, + status: 422, + } + ); + }); + }); + + it('should create new password identity verification successfully', async () => { + const client = await initExperienceClient(); + + const { verificationId } = await client.createNewPasswordIdentityVerification({ + identifier: { + type: SignInIdentifier.Username, + value: username, + }, + password: '?sy8Q3z3_G', + }); + + expect(verificationId).toBeTruthy(); + }); +}); diff --git a/packages/schemas/src/types/interactions.ts b/packages/schemas/src/types/interactions.ts index 82b77965b5ba..e7934eed7349 100644 --- a/packages/schemas/src/types/interactions.ts +++ b/packages/schemas/src/types/interactions.ts @@ -31,8 +31,8 @@ export enum InteractionEvent { // ====== Experience API payload guards and type definitions start ====== /** Identifiers that can be used to uniquely identify a user. */ -export type InteractionIdentifier = { - type: SignInIdentifier; +export type InteractionIdentifier = { + type: T; value: string; }; @@ -61,6 +61,7 @@ export enum VerificationType { TOTP = 'Totp', WebAuthn = 'WebAuthn', BackupCode = 'BackupCode', + NewPasswordIdentity = 'NewPasswordIdentity', } // REMARK: API payload guard @@ -115,6 +116,26 @@ export const backupCodeVerificationVerifyPayloadGuard = z.object({ code: z.string().min(1), }) satisfies ToZodObject; +/** + * Payload type for `POST /api/experience/verification/new-password-identity`. + * @remarks Currently we only support username identifier for new password identity registration. + * For email and phone new identity registration, a `CodeVerification` record is required. + */ +export type NewPasswordIdentityVerificationPayload = { + identifier: { + type: SignInIdentifier.Username; + value: string; + }; + password: string; +}; +export const newPasswordIdentityVerificationPayloadGuard = z.object({ + identifier: z.object({ + type: z.literal(SignInIdentifier.Username), + value: z.string(), + }), + password: z.string().min(1), +}) satisfies ToZodObject; + /** Payload type for `POST /api/experience/identification`. */ export type IdentificationApiPayload = { /** The ID of the verification record that is used to identify the user. */ From a84389da13c9f8c00e6c502177877bd5226a115a Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 16 Jul 2024 21:59:39 +0800 Subject: [PATCH 025/135] fix(experience): correct active state for input field (#6255) --- .../InputFields/InputField/index.tsx | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/experience/src/components/InputFields/InputField/index.tsx b/packages/experience/src/components/InputFields/InputField/index.tsx index bdf5faf3ced9..78845f2b80aa 100644 --- a/packages/experience/src/components/InputFields/InputField/index.tsx +++ b/packages/experience/src/components/InputFields/InputField/index.tsx @@ -1,7 +1,15 @@ import { type Nullable } from '@silverhand/essentials'; import classNames from 'classnames'; -import type { HTMLProps, ReactElement, Ref } from 'react'; -import { forwardRef, cloneElement, useState, useImperativeHandle, useRef, useEffect } from 'react'; +import type { HTMLProps, ReactElement, Ref, AnimationEvent } from 'react'; +import { + forwardRef, + cloneElement, + useState, + useImperativeHandle, + useRef, + useEffect, + useCallback, +} from 'react'; import ErrorMessage from '@/components/ErrorMessage'; @@ -47,33 +55,25 @@ const InputField = ( const [isFocused, setIsFocused] = useState(false); const [hasValue, setHasValue] = useState(false); - const hasContent = - Boolean(isPrefixVisible) || - hasValue || - // Handle the case when this filed have a default value - Boolean(value); - - const isActive = hasContent || isFocused; useEffect(() => { - const inputDom = innerRef.current; - if (!inputDom) { - return; - } - /** - * Use a timeout to check if the input field has autofill value. - * Fix the issue that the input field doesn't have the active style when the autofill value is set. - * see https://github.com/facebook/react/issues/1159#issuecomment-1025423604 + * Should listen to the value prop to update the hasValue state. */ - const checkAutoFillTimeout = setTimeout(() => { - setHasValue(inputDom.matches('*:-webkit-autofill')); - }, 200); + setHasValue(Boolean(value)); + }, [value]); + + /** + * Fix the issue that the input field doesn't have the active style when the autofill value is set. + * Modern browsers will trigger an animation event when the input field is autofilled. + */ + const handleAnimationStart = useCallback((event: AnimationEvent) => { + if (event.animationName === 'onautofillstart') { + setHasValue(true); + } + }, []); - return () => { - clearTimeout(checkAutoFillTimeout); - }; - }, [innerRef]); + const isActive = Boolean(isPrefixVisible) || hasValue || isFocused; return (
@@ -97,6 +97,7 @@ const InputField = ( {...props} ref={innerRef} value={value} + onAnimationStart={handleAnimationStart} onFocus={(event) => { setIsFocused(true); return onFocus?.(event); From bc2ccf671e0dd04d633f3d7b43840633b080c434 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 17 Jul 2024 11:08:01 +0800 Subject: [PATCH 026/135] refactor(console): use button loading in experience flow if possible (#6234) --- .../Providers/LoadingLayerProvider/index.tsx | 6 +- .../components/LoadingLayer/index.module.scss | 7 --- .../src/components/LoadingLayer/index.tsx | 6 +- .../components/LoadingMask/index.module.scss | 8 +++ .../src/components/LoadingMask/index.tsx | 13 +++++ .../src/containers/SetPassword/Lite.tsx | 15 +++-- .../containers/SetPassword/SetPassword.tsx | 15 +++-- .../src/containers/SetPassword/index.tsx | 2 +- .../TotpCodeVerification/index.module.scss | 4 ++ .../containers/TotpCodeVerification/index.tsx | 57 ++++++++++++++----- .../use-totp-code-verification.ts | 4 +- .../VerificationCode/index.module.scss | 3 +- .../src/containers/VerificationCode/index.tsx | 53 +++++++++++++---- .../experience/src/pages/Consent/index.tsx | 6 +- .../Continue/IdentifierProfileForm/index.tsx | 6 +- .../ForgotPasswordForm/index.tsx | 7 ++- .../MfaBinding/BackupCodeBinding/index.tsx | 9 ++- .../MfaBinding/WebAuthnBinding/index.tsx | 9 ++- .../BackupCodeVerification/index.tsx | 10 +++- .../WebAuthnVerification/index.tsx | 9 ++- .../Register/IdentifierRegisterForm/index.tsx | 3 +- .../SignIn/IdentifierSignInForm/index.tsx | 3 +- .../pages/SignIn/PasswordSignInForm/index.tsx | 3 +- .../SignInPassword/PasswordForm/index.tsx | 4 +- .../src/pages/SingleSignOnEmail/index.tsx | 9 ++- 25 files changed, 194 insertions(+), 77 deletions(-) create mode 100644 packages/experience/src/components/LoadingMask/index.module.scss create mode 100644 packages/experience/src/components/LoadingMask/index.tsx diff --git a/packages/experience/src/Providers/LoadingLayerProvider/index.tsx b/packages/experience/src/Providers/LoadingLayerProvider/index.tsx index eccc54c9bebd..2c3601ee0d04 100644 --- a/packages/experience/src/Providers/LoadingLayerProvider/index.tsx +++ b/packages/experience/src/Providers/LoadingLayerProvider/index.tsx @@ -1,18 +1,16 @@ import { useContext } from 'react'; import { Outlet } from 'react-router-dom'; -import { useDebouncedLoader } from 'use-debounced-loader'; import PageContext from '@/Providers/PageContextProvider/PageContext'; -import LoadingLayer from '@/components/LoadingLayer'; +import LoadingMask from '@/components/LoadingMask'; const LoadingLayerProvider = () => { const { loading } = useContext(PageContext); - const debouncedLoading = useDebouncedLoader(loading, 500); return ( <> - {debouncedLoading && } + {loading && } ); }; diff --git a/packages/experience/src/components/LoadingLayer/index.module.scss b/packages/experience/src/components/LoadingLayer/index.module.scss index 4f24e5e8b485..115c477e44a9 100644 --- a/packages/experience/src/components/LoadingLayer/index.module.scss +++ b/packages/experience/src/components/LoadingLayer/index.module.scss @@ -1,12 +1,5 @@ @use '@/scss/underscore' as _; -.overlay { - position: fixed; - inset: 0; - @include _.flex-column; - z-index: 300; -} - .loadingIcon { color: var(--color-type-primary); animation: rotating 1s steps(12, end) infinite; diff --git a/packages/experience/src/components/LoadingLayer/index.tsx b/packages/experience/src/components/LoadingLayer/index.tsx index 4db64f3a1e19..52b43e236bcf 100644 --- a/packages/experience/src/components/LoadingLayer/index.tsx +++ b/packages/experience/src/components/LoadingLayer/index.tsx @@ -1,14 +1,16 @@ +import LoadingMask from '../LoadingMask'; + import LoadingIcon from './LoadingIcon'; import * as styles from './index.module.scss'; export { default as LoadingIcon } from './LoadingIcon'; const LoadingLayer = () => ( -
+
-
+
); export default LoadingLayer; diff --git a/packages/experience/src/components/LoadingMask/index.module.scss b/packages/experience/src/components/LoadingMask/index.module.scss new file mode 100644 index 000000000000..30e65e81855c --- /dev/null +++ b/packages/experience/src/components/LoadingMask/index.module.scss @@ -0,0 +1,8 @@ +@use '@/scss/underscore' as _; + +.overlay { + position: fixed; + inset: 0; + @include _.flex-column; + z-index: 300; +} diff --git a/packages/experience/src/components/LoadingMask/index.tsx b/packages/experience/src/components/LoadingMask/index.tsx new file mode 100644 index 000000000000..b77e4435113e --- /dev/null +++ b/packages/experience/src/components/LoadingMask/index.tsx @@ -0,0 +1,13 @@ +import { type ReactNode } from 'react'; + +import * as styles from './index.module.scss'; + +type Props = { + readonly children?: ReactNode; +}; + +const LoadingMask = ({ children }: Props) => { + return
{children}
; +}; + +export default LoadingMask; diff --git a/packages/experience/src/containers/SetPassword/Lite.tsx b/packages/experience/src/containers/SetPassword/Lite.tsx index aa1abd3b8a06..e3c831d76135 100644 --- a/packages/experience/src/containers/SetPassword/Lite.tsx +++ b/packages/experience/src/containers/SetPassword/Lite.tsx @@ -14,7 +14,7 @@ type Props = { readonly className?: string; // eslint-disable-next-line react/boolean-prop-naming readonly autoFocus?: boolean; - readonly onSubmit: (password: string) => void; + readonly onSubmit: (password: string) => Promise; readonly errorMessage?: string; readonly clearErrorMessage?: () => void; }; @@ -29,7 +29,7 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage const { register, handleSubmit, - formState: { errors, isValid }, + formState: { errors, isValid, isSubmitting }, } = useForm({ reValidateMode: 'onBlur', defaultValues: { newPassword: '' }, @@ -45,8 +45,8 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage (event?: React.FormEvent) => { clearErrorMessage?.(); - void handleSubmit((data, event) => { - onSubmit(data.newPassword); + void handleSubmit(async (data) => { + await onSubmit(data.newPassword); })(event); }, [clearErrorMessage, handleSubmit, onSubmit] @@ -70,7 +70,12 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage {errorMessage && {errorMessage}} -
{!showTerms && (
diff --git a/packages/experience/src/pages/Continue/IdentifierProfileForm/index.tsx b/packages/experience/src/pages/Continue/IdentifierProfileForm/index.tsx index 19e3b94486d3..b26484ed6493 100644 --- a/packages/experience/src/pages/Continue/IdentifierProfileForm/index.tsx +++ b/packages/experience/src/pages/Continue/IdentifierProfileForm/index.tsx @@ -43,7 +43,7 @@ const IdentifierProfileForm = ({ const { handleSubmit, control, - formState: { errors, isValid }, + formState: { errors, isValid, isSubmitting }, } = useForm({ reValidateMode: 'onBlur', defaultValues: { @@ -61,7 +61,7 @@ const IdentifierProfileForm = ({ }, [clearErrorMessage, isValid]); const onSubmitHandler = useCallback( - async (event?: React.FormEvent) => { + (event?: React.FormEvent) => { clearErrorMessage?.(); void handleSubmit(async ({ identifier: { type, value } }) => { @@ -115,7 +115,7 @@ const IdentifierProfileForm = ({ {errorMessage && {errorMessage}} -
diff --git a/packages/experience/src/pages/MfaBinding/WebAuthnBinding/index.tsx b/packages/experience/src/pages/MfaBinding/WebAuthnBinding/index.tsx index 1678b42b8b50..9eca7bc9889c 100644 --- a/packages/experience/src/pages/MfaBinding/WebAuthnBinding/index.tsx +++ b/packages/experience/src/pages/MfaBinding/WebAuthnBinding/index.tsx @@ -1,4 +1,5 @@ import { conditional } from '@silverhand/essentials'; +import { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { validate } from 'superstruct'; @@ -19,6 +20,7 @@ const WebAuthnBinding = () => { const [, webAuthnState] = validate(state, webAuthnStateGuard); const handleWebAuthn = useWebAuthnOperation(); const skipMfa = useSkipMfa(); + const [isCreatingPasskey, setIsCreatingPasskey] = useState(false); if (!webAuthnState) { return ; @@ -38,8 +40,11 @@ const WebAuthnBinding = () => { > ; }; -describe('confirm modal provider', () => { - it('render confirm modal', async () => { - const { queryByText, getByText } = render( - - - - ); - - const trigger = getByText('show modal'); +const CallbackConfirmModalTestComponent = () => { + const { show } = useConfirmModal(); - act(() => { - fireEvent.click(trigger); + const onClick = () => { + show({ + ModalContent: 'confirm modal content', + onConfirm: confirmHandler, + onCancel: cancelHandler, }); + }; + + return ; +}; - await waitFor(() => { - expect(queryByText('confirm modal content')).not.toBeNull(); - expect(queryByText('action.confirm')).not.toBeNull(); - expect(queryByText('action.cancel')).not.toBeNull(); +describe('confirm modal provider', () => { + describe('promise confirm modal', () => { + it('render confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); }); - }); - it('confirm callback of confirm modal', async () => { - const { queryByText, getByText } = render( - - - - ); + it('confirm callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); - const trigger = getByText('show modal'); + const trigger = getByText('show modal'); - act(() => { - fireEvent.click(trigger); - }); + act(() => { + fireEvent.click(trigger); + }); - await waitFor(() => { - expect(queryByText('confirm modal content')).not.toBeNull(); - expect(queryByText('action.confirm')).not.toBeNull(); - }); + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + }); - const confirm = getByText('action.confirm'); + const confirm = getByText('action.confirm'); - act(() => { - fireEvent.click(confirm); - }); + act(() => { + fireEvent.click(confirm); + }); - await waitFor(() => { - expect(confirmHandler).toBeCalled(); + await waitFor(() => { + expect(confirmHandler).toBeCalled(); + }); }); - }); - it('cancel callback of confirm modal', async () => { - const { queryByText, getByText } = render( - - - - ); + it('cancel callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); - const trigger = getByText('show modal'); + act(() => { + fireEvent.click(trigger); + }); - act(() => { - fireEvent.click(trigger); + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); + + const cancel = getByText('action.cancel'); + + act(() => { + fireEvent.click(cancel); + }); + + await waitFor(() => { + expect(cancelHandler).toBeCalled(); + }); }); + }); - await waitFor(() => { - expect(queryByText('confirm modal content')).not.toBeNull(); - expect(queryByText('action.cancel')).not.toBeNull(); + describe('callback confirm modal', () => { + it('render confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); }); - const cancel = getByText('action.cancel'); + it('confirm callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + }); + + const confirm = getByText('action.confirm'); - act(() => { - fireEvent.click(cancel); + act(() => { + fireEvent.click(confirm); + }); + + await waitFor(() => { + expect(confirmHandler).toBeCalled(); + }); }); - await waitFor(() => { - expect(cancelHandler).toBeCalled(); + it('cancel callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); + + const cancel = getByText('action.cancel'); + + act(() => { + fireEvent.click(cancel); + }); + + await waitFor(() => { + expect(cancelHandler).toBeCalled(); + }); }); }); }); diff --git a/packages/experience/src/components/ConfirmModal/AcModal.tsx b/packages/experience/src/components/ConfirmModal/AcModal.tsx index 0f7ce45977ef..17c454783d68 100644 --- a/packages/experience/src/components/ConfirmModal/AcModal.tsx +++ b/packages/experience/src/components/ConfirmModal/AcModal.tsx @@ -16,6 +16,8 @@ import type { ModalProps } from './type'; const AcModal = ({ className, isOpen = false, + isConfirmLoading = false, + isCancelLoading = false, children, cancelText = 'action.cancel', confirmText = 'action.confirm', @@ -62,6 +64,7 @@ const AcModal = ({ type="secondary" i18nProps={cancelTextI18nProps} size="small" + isLoading={isCancelLoading} onClick={onClose} /> {onConfirm && ( @@ -69,6 +72,7 @@ const AcModal = ({ title={confirmText} i18nProps={confirmTextI18nProps} size="small" + isLoading={isConfirmLoading} onClick={onConfirm} /> )} diff --git a/packages/experience/src/components/ConfirmModal/MobileModal.tsx b/packages/experience/src/components/ConfirmModal/MobileModal.tsx index 9b398da3cb7c..0e888fa5db29 100644 --- a/packages/experience/src/components/ConfirmModal/MobileModal.tsx +++ b/packages/experience/src/components/ConfirmModal/MobileModal.tsx @@ -11,6 +11,8 @@ import type { ModalProps } from './type'; const MobileModal = ({ className, isOpen = false, + isConfirmLoading = false, + isCancelLoading = false, children, cancelText = 'action.cancel', confirmText = 'action.confirm', @@ -34,11 +36,17 @@ const MobileModal = ({
diff --git a/packages/experience/src/components/ConfirmModal/type.ts b/packages/experience/src/components/ConfirmModal/type.ts index a9a00577def7..5f10d301c92c 100644 --- a/packages/experience/src/components/ConfirmModal/type.ts +++ b/packages/experience/src/components/ConfirmModal/type.ts @@ -4,6 +4,8 @@ import type { ReactNode } from 'react'; export type ModalProps = { className?: string; isOpen?: boolean; + isConfirmLoading?: boolean; + isCancelLoading?: boolean; children: ReactNode; cancelText?: TFuncKey; confirmText?: TFuncKey; diff --git a/packages/experience/src/containers/VerificationCode/use-identifier-error-alert.ts b/packages/experience/src/containers/VerificationCode/use-identifier-error-alert.ts index d92038ea3055..eadd214e8640 100644 --- a/packages/experience/src/containers/VerificationCode/use-identifier-error-alert.ts +++ b/packages/experience/src/containers/VerificationCode/use-identifier-error-alert.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { usePromiseConfirmModal } from '@/hooks/use-confirm-modal'; import type { VerificationCodeIdentifier } from '@/types'; import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code'; @@ -13,7 +13,7 @@ export enum IdentifierErrorType { } const useIdentifierErrorAlert = () => { - const { show } = useConfirmModal(); + const { show } = usePromiseConfirmModal(); const navigate = useNavigate(); const { t } = useTranslation(); diff --git a/packages/experience/src/containers/VerificationCode/use-link-social-confirm-modal.ts b/packages/experience/src/containers/VerificationCode/use-link-social-confirm-modal.ts index 39fb7dc04aa1..210ea554115e 100644 --- a/packages/experience/src/containers/VerificationCode/use-link-social-confirm-modal.ts +++ b/packages/experience/src/containers/VerificationCode/use-link-social-confirm-modal.ts @@ -16,7 +16,7 @@ const useLinkSocialConfirmModal = () => { return useCallback( async (method: VerificationCodeIdentifier, target: string, connectorId: string) => { - const [confirm] = await show({ + show({ confirmText: 'action.bind_and_continue', cancelText: 'action.change', cancelTextI18nProps: { @@ -29,15 +29,13 @@ const useLinkSocialConfirmModal = () => { ? formatPhoneNumberWithCountryCallingCode(target) : target, }), + onConfirm: async () => { + await linkWithSocial(connectorId); + }, + onCancel: () => { + navigate(-1); + }, }); - - if (!confirm) { - navigate(-1); - - return; - } - - await linkWithSocial(connectorId); }, [linkWithSocial, navigate, show, t] ); diff --git a/packages/experience/src/containers/VerificationCode/use-register-flow-code-verification.ts b/packages/experience/src/containers/VerificationCode/use-register-flow-code-verification.ts index 1608c2336b00..0dec679a8466 100644 --- a/packages/experience/src/containers/VerificationCode/use-register-flow-code-verification.ts +++ b/packages/experience/src/containers/VerificationCode/use-register-flow-code-verification.ts @@ -50,7 +50,7 @@ const useRegisterFlowCodeVerification = ( return; } - const [confirm] = await show({ + show({ confirmText: 'action.sign_in', ModalContent: t('description.create_account_id_exists', { type: t(`description.${method === SignInIdentifier.Email ? 'email' : 'phone_number'}`), @@ -59,25 +59,23 @@ const useRegisterFlowCodeVerification = ( ? formatPhoneNumberWithCountryCallingCode(target) : target, }), + onConfirm: async () => { + const [error, result] = await signInWithIdentifierAsync(); + + if (error) { + await handleError(error, preSignInErrorHandler); + + return; + } + + if (result?.redirectTo) { + redirectTo(result.redirectTo); + } + }, + onCancel: () => { + navigate(-1); + }, }); - - if (!confirm) { - navigate(-1); - - return; - } - - const [error, result] = await signInWithIdentifierAsync(); - - if (error) { - await handleError(error, preSignInErrorHandler); - - return; - } - - if (result?.redirectTo) { - redirectTo(result.redirectTo); - } }, [ handleError, method, diff --git a/packages/experience/src/containers/VerificationCode/use-sign-in-flow-code-verification.ts b/packages/experience/src/containers/VerificationCode/use-sign-in-flow-code-verification.ts index 4c410e91f3c6..16a6e0f7f995 100644 --- a/packages/experience/src/containers/VerificationCode/use-sign-in-flow-code-verification.ts +++ b/packages/experience/src/containers/VerificationCode/use-sign-in-flow-code-verification.ts @@ -51,7 +51,7 @@ const useSignInFlowCodeVerification = ( return; } - const [confirm] = await show({ + show({ confirmText: 'action.create', ModalContent: t('description.sign_in_id_does_not_exist', { type: t(`description.${method === SignInIdentifier.Email ? 'email' : 'phone_number'}`), @@ -60,27 +60,25 @@ const useSignInFlowCodeVerification = ( ? formatPhoneNumberWithCountryCallingCode(target) : target, }), + onConfirm: async () => { + const [error, result] = await registerWithIdentifierAsync( + method === SignInIdentifier.Email ? { email: target } : { phone: target } + ); + + if (error) { + await handleError(error, preSignInErrorHandler); + + return; + } + + if (result?.redirectTo) { + redirectTo(result.redirectTo); + } + }, + onCancel: () => { + navigate(-1); + }, }); - - if (!confirm) { - navigate(-1); - - return; - } - - const [error, result] = await registerWithIdentifierAsync( - method === SignInIdentifier.Email ? { email: target } : { phone: target } - ); - - if (error) { - await handleError(error, preSignInErrorHandler); - - return; - } - - if (result?.redirectTo) { - redirectTo(result.redirectTo); - } }, [ signInMode, show, diff --git a/packages/experience/src/hooks/use-confirm-modal.ts b/packages/experience/src/hooks/use-confirm-modal.ts index 81521b481307..6ae139e77f26 100644 --- a/packages/experience/src/hooks/use-confirm-modal.ts +++ b/packages/experience/src/hooks/use-confirm-modal.ts @@ -2,6 +2,49 @@ import { useContext } from 'react'; import { ConfirmModalContext } from '@/Providers/ConfirmModalProvider'; -export type { ModalContentRenderProps } from '@/Providers/ConfirmModalProvider'; +/** + * Hook for using the promise-based confirm modal + * + * @returns An object with a `show` method that returns a promise + * + * Example: + * ```ts + * const { show } = usePromiseConfirmModal(); + * const [result] = await show({ ModalContent: 'Are you sure?' }); + * if (result) { + * // User confirmed + * } + *``` + */ +export const usePromiseConfirmModal = () => { + const { showPromise } = useContext(ConfirmModalContext); -export const useConfirmModal = () => useContext(ConfirmModalContext); + return { show: showPromise }; +}; + +/** + * Hook for using the callback-based confirm modal + * + * @returns An object with a `show` method that accepts callbacks + * + * Example: + * ```ts + * const { show } = useConfirmModal(); + * show({ + * ModalContent: 'Are you sure?', + * onConfirm: async () => { + * // This will automatically set the confirm button to loading state + * await someAsyncOperation(); + * }, + * onCancel: async () => { + * // This will automatically set the cancel button to loading state + * await someAsyncOperation(); + * } + * }); + * ``` + */ +export const useConfirmModal = () => { + const { showCallback } = useContext(ConfirmModalContext); + + return { show: showCallback }; +}; diff --git a/packages/experience/src/hooks/use-terms.ts b/packages/experience/src/hooks/use-terms.ts index dd95c03aef99..c94ae23083d9 100644 --- a/packages/experience/src/hooks/use-terms.ts +++ b/packages/experience/src/hooks/use-terms.ts @@ -5,11 +5,11 @@ import { useCallback, useContext, useMemo } from 'react'; import PageContext from '@/Providers/PageContextProvider/PageContext'; import TermsAndPrivacyConfirmModalContent from '@/containers/TermsAndPrivacyConfirmModalContent'; -import { useConfirmModal } from './use-confirm-modal'; +import { usePromiseConfirmModal } from './use-confirm-modal'; const useTerms = () => { const { termsAgreement, setTermsAgreement, experienceSettings } = useContext(PageContext); - const { show } = useConfirmModal(); + const { show } = usePromiseConfirmModal(); const { termsOfUseUrl, privacyPolicyUrl, isTermsDisabled, agreeToTermsPolicy } = useMemo(() => { const { termsOfUseUrl, privacyPolicyUrl, agreeToTermsPolicy } = experienceSettings ?? {}; diff --git a/packages/experience/src/pages/Continue/SetPassword/index.tsx b/packages/experience/src/pages/Continue/SetPassword/index.tsx index a76f374d8e8b..64530f584ee7 100644 --- a/packages/experience/src/pages/Continue/SetPassword/index.tsx +++ b/packages/experience/src/pages/Continue/SetPassword/index.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import SecondaryPageLayout from '@/Layout/SecondaryPageLayout'; import { addProfile } from '@/apis/interaction'; import SetPasswordForm from '@/containers/SetPassword'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { usePromiseConfirmModal } from '@/hooks/use-confirm-modal'; import type { ErrorHandlers } from '@/hooks/use-error-handler'; import useGlobalRedirectTo from '@/hooks/use-global-redirect-to'; import usePasswordAction, { type SuccessHandler } from '@/hooks/use-password-action'; @@ -18,7 +18,7 @@ const SetPassword = () => { }, []); const navigate = useNavigate(); - const { show } = useConfirmModal(); + const { show } = usePromiseConfirmModal(); const redirectTo = useGlobalRedirectTo(); const preSignInErrorHandler = usePreSignInErrorHandler(); diff --git a/packages/experience/src/pages/RegisterPassword/index.tsx b/packages/experience/src/pages/RegisterPassword/index.tsx index 0a9443c3fb3a..b276b2c4ea9f 100644 --- a/packages/experience/src/pages/RegisterPassword/index.tsx +++ b/packages/experience/src/pages/RegisterPassword/index.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import SecondaryPageLayout from '@/Layout/SecondaryPageLayout'; import { setUserPassword } from '@/apis/interaction'; import SetPassword from '@/containers/SetPassword'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { usePromiseConfirmModal } from '@/hooks/use-confirm-modal'; import { type ErrorHandlers } from '@/hooks/use-error-handler'; import useGlobalRedirectTo from '@/hooks/use-global-redirect-to'; import useMfaErrorHandler from '@/hooks/use-mfa-error-handler'; @@ -19,7 +19,7 @@ const RegisterPassword = () => { const navigate = useNavigate(); const redirectTo = useGlobalRedirectTo(); - const { show } = useConfirmModal(); + const { show } = usePromiseConfirmModal(); const [errorMessage, setErrorMessage] = useState(); const clearErrorMessage = useCallback(() => { setErrorMessage(undefined); diff --git a/packages/experience/src/pages/ResetPassword/index.tsx b/packages/experience/src/pages/ResetPassword/index.tsx index 26b415705d21..397566861502 100644 --- a/packages/experience/src/pages/ResetPassword/index.tsx +++ b/packages/experience/src/pages/ResetPassword/index.tsx @@ -6,7 +6,7 @@ import SecondaryPageLayout from '@/Layout/SecondaryPageLayout'; import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext'; import { setUserPassword } from '@/apis/interaction'; import SetPassword from '@/containers/SetPassword'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { usePromiseConfirmModal } from '@/hooks/use-confirm-modal'; import { type ErrorHandlers } from '@/hooks/use-error-handler'; import usePasswordAction, { type SuccessHandler } from '@/hooks/use-password-action'; import { usePasswordPolicy } from '@/hooks/use-sie'; @@ -20,7 +20,7 @@ const ResetPassword = () => { const { t } = useTranslation(); const { setToast } = useToast(); const navigate = useNavigate(); - const { show } = useConfirmModal(); + const { show } = usePromiseConfirmModal(); const { setForgotPasswordIdentifierInputValue } = useContext(UserInteractionContext); const errorHandlers: ErrorHandlers = useMemo( () => ({ From 49d357d9ecc90d6a4e91b1346497aae41a2effb7 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 17 Jul 2024 13:58:40 +0800 Subject: [PATCH 028/135] chore(elements): update readme --- packages/elements/README.md | 40 ++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/elements/README.md b/packages/elements/README.md index 3a7ddcdad42a..81b2c64644c6 100644 --- a/packages/elements/README.md +++ b/packages/elements/README.md @@ -1,3 +1,41 @@ # Logto elements -🚧 Work in progress +A collection of Web Components for building better applications with Logto. + +> [!Warning] +> This package is still under development and not yet published to npm. + +## Development + +- The standard `dev` script is useful for testing the Logto integration when you are working with the workspace's `dev` script. How ever, the dev integration has some issues like duplicate registration and stale element cache. It's not easy to overcome them at the moment. +- Run the `start` script to start a quick development server that serves the elements via `index.html`. + +## Internationalization + +The elements are using `@lit/localize` for internationalization. The translations are stored in the `xliff` directory. There's no need to update that directory for new phrases, unless you can add the translations at the same time. See [Localization](https://lit.dev/docs/localization/overview/) for more information. + +### Update translations + +1. Run `lit-localize extract` to extract and sync the translations from the source code to the `xliff` directory. +2. Translate the phrases in the `xliff` directory. +3. Run `lit-localize build` to build the translations into the `src/generated` directory. + +> [!Important] +> `lit-localize build` is required to build the bundle with the translations. + +### Convention + +Although `@lit/localize` gives us the flexibility to write localized strings in a casual way, we should follow a convention to keep the translations consistent. + +When using `msg()`, a human-readable ID should be used, and it is highly recommended to also write the description to give the translators (or LLMs) more context. + +```ts +// ✅ Good +msg('Not available', { + id: 'form-card.fallback-title', + desc: 'The fallback title of a form card when the title is not provided.', +}) + +// ❌ Bad +msg('Not available') +``` From 0a92bd2fdcb27cd81059f6ab5456caaab653a781 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 17 Jul 2024 14:39:20 +0800 Subject: [PATCH 029/135] feat(core): add the new user provision (#6253) add the new user provision --- .../classes/experience-interaction.test.ts | 119 +++++++++++ .../classes/experience-interaction.ts | 23 +- .../experience/classes/provision-library.ts | 196 ++++++++++++++++++ .../organization-jti.test.ts | 175 ++++++++++++++++ .../api/interaction/organization-jit.test.ts | 2 +- 5 files changed, 509 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/routes/experience/classes/experience-interaction.test.ts create mode 100644 packages/core/src/routes/experience/classes/provision-library.ts create mode 100644 packages/integration-tests/src/tests/api/experience-api/register-interaction/organization-jti.test.ts diff --git a/packages/core/src/routes/experience/classes/experience-interaction.test.ts b/packages/core/src/routes/experience/classes/experience-interaction.test.ts new file mode 100644 index 000000000000..561df47d981d --- /dev/null +++ b/packages/core/src/routes/experience/classes/experience-interaction.test.ts @@ -0,0 +1,119 @@ +import { + adminConsoleApplicationId, + adminTenantId, + type CreateUser, + InteractionEvent, + SignInIdentifier, + SignInMode, + type User, + VerificationType, +} from '@logto/schemas'; +import { createMockUtils, pickDefault } from '@logto/shared/esm'; + +import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; +import { type InsertUserResult } from '#src/libraries/user.js'; +import { createMockLogContext } from '#src/test-utils/koa-audit-log.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; +import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; + +import { CodeVerification } from './verifications/code-verification.js'; + +const { jest } = import.meta; +const { mockEsm } = createMockUtils(jest); + +mockEsm('#src/utils/tenant.js', () => ({ + getTenantId: () => [adminTenantId], +})); + +const mockEmail = 'foo@bar.com'; +const userQueries = { + hasActiveUsers: jest.fn().mockResolvedValue(false), + hasUserWithEmail: jest.fn().mockResolvedValue(false), + hasUserWithPhone: jest.fn().mockResolvedValue(false), + hasUserWithIdentity: jest.fn().mockResolvedValue(false), +}; +const userLibraries = { + generateUserId: jest.fn().mockResolvedValue('uid'), + insertUser: jest.fn(async (user: CreateUser): Promise => [user as User]), + provisionOrganizations: jest.fn(), +}; +const ssoConnectors = { + getAvailableSsoConnectors: jest.fn().mockResolvedValue([]), +}; +const signInExperiences = { + findDefaultSignInExperience: jest.fn().mockResolvedValue({ + ...mockSignInExperience, + signUp: { + identifiers: [SignInIdentifier.Email], + password: false, + verify: true, + }, + }), + updateDefaultSignInExperience: jest.fn(), +}; + +const mockProviderInteractionDetails = jest + .fn() + .mockResolvedValue({ params: { client_id: adminConsoleApplicationId } }); + +const ExperienceInteraction = await pickDefault(import('./experience-interaction.js')); + +describe('ExperienceInteraction class', () => { + const tenant = new MockTenant( + createMockProvider(mockProviderInteractionDetails), + { + users: userQueries, + signInExperiences, + }, + undefined, + { users: userLibraries, ssoConnectors } + ); + const ctx = { + ...createContextWithRouteParameters(), + ...createMockLogContext(), + }; + const { libraries, queries } = tenant; + + const emailVerificationRecord = new CodeVerification(libraries, queries, { + id: 'mock_email_verification_id', + type: VerificationType.VerificationCode, + identifier: { + type: SignInIdentifier.Email, + value: mockEmail, + }, + interactionEvent: InteractionEvent.Register, + verified: true, + }); + + beforeAll(() => { + jest.clearAllMocks(); + }); + + describe('new user registration', () => { + it('First admin user provisioning', async () => { + const experienceInteraction = new ExperienceInteraction(ctx, tenant); + + await experienceInteraction.setInteractionEvent(InteractionEvent.Register); + experienceInteraction.setVerificationRecord(emailVerificationRecord); + await experienceInteraction.identifyUser(emailVerificationRecord.id); + + expect(userLibraries.insertUser).toHaveBeenCalledWith( + { + id: 'uid', + primaryEmail: mockEmail, + }, + ['user', 'default:admin'] + ); + + expect(signInExperiences.updateDefaultSignInExperience).toHaveBeenCalledWith({ + signInMode: SignInMode.SignIn, + }); + + expect(userLibraries.provisionOrganizations).toHaveBeenCalledWith({ + userId: 'uid', + email: mockEmail, + }); + }); + }); +}); diff --git a/packages/core/src/routes/experience/classes/experience-interaction.ts b/packages/core/src/routes/experience/classes/experience-interaction.ts index a5632b99679c..210b1d79c025 100644 --- a/packages/core/src/routes/experience/classes/experience-interaction.ts +++ b/packages/core/src/routes/experience/classes/experience-interaction.ts @@ -11,6 +11,7 @@ import assertThat from '#src/utils/assert-that.js'; import { interactionProfileGuard, type Interaction, type InteractionProfile } from '../types.js'; +import { ProvisionLibrary } from './provision-library.js'; import { getNewUserProfileFromVerificationRecord, toUserSocialIdentityData } from './utils.js'; import { ProfileValidator } from './validators/profile-validator.js'; import { SignInExperienceValidator } from './validators/sign-in-experience-validator.js'; @@ -44,13 +45,14 @@ const interactionStorageGuard = z.object({ export default class ExperienceInteraction { public readonly signInExperienceValidator: SignInExperienceValidator; public readonly profileValidator: ProfileValidator; + public readonly provisionLibrary: ProvisionLibrary; /** The user verification record list for the current interaction. */ private readonly verificationRecords = new Map(); /** The userId of the user for the current interaction. Only available once the user is identified. */ private userId?: string; /** The user provided profile data in the current interaction that needs to be stored to database. */ - private readonly profile?: Record; // TODO: Fix the type + private readonly profile?: InteractionProfile; /** The interaction event for the current interaction. */ #interactionEvent?: InteractionEvent; @@ -63,11 +65,12 @@ export default class ExperienceInteraction { constructor( private readonly ctx: WithLogContext, private readonly tenant: TenantContext, - public interactionDetails?: Interaction + interactionDetails?: Interaction ) { const { libraries, queries } = tenant; this.signInExperienceValidator = new SignInExperienceValidator(libraries, queries); + this.provisionLibrary = new ProvisionLibrary(tenant, ctx); if (!interactionDetails) { this.profileValidator = new ProfileValidator(libraries, queries); @@ -294,19 +297,25 @@ export default class ExperienceInteraction { await this.profileValidator.guardProfileUniquenessAcrossUsers(newProfile); - // TODO: new user provisioning and hooks - const { socialIdentity, enterpriseSsoIdentity, ...rest } = newProfile; + const { isCreatingFirstAdminUser, initialUserRoles, customData } = + await this.provisionLibrary.getUserProvisionContext(newProfile); + const [user] = await insertUser( { id: await generateUserId(), ...rest, ...conditional(socialIdentity && { identities: toUserSocialIdentityData(socialIdentity) }), + ...conditional(customData && { customData }), }, - [] + initialUserRoles ); + if (isCreatingFirstAdminUser) { + await this.provisionLibrary.adminUserProvision(user); + } + if (enterpriseSsoIdentity) { await userSsoIdentitiesQueries.insert({ id: generateStandardId(), @@ -315,6 +324,10 @@ export default class ExperienceInteraction { }); } + await this.provisionLibrary.newUserJtiOrganizationProvision(user.id, newProfile); + + // TODO: new user hooks + this.userId = user.id; } } diff --git a/packages/core/src/routes/experience/classes/provision-library.ts b/packages/core/src/routes/experience/classes/provision-library.ts new file mode 100644 index 000000000000..3a978bc56089 --- /dev/null +++ b/packages/core/src/routes/experience/classes/provision-library.ts @@ -0,0 +1,196 @@ +import { + adminConsoleApplicationId, + adminTenantId, + AdminTenantRole, + defaultManagementApiAdminName, + defaultTenantId, + getTenantOrganizationId, + getTenantRole, + OrganizationInvitationStatus, + SignInMode, + TenantRole, + userOnboardingDataKey, + type User, + type UserOnboardingData, +} from '@logto/schemas'; +import { conditionalArray } from '@silverhand/essentials'; + +import { EnvSet } from '#src/env-set/index.js'; +import { type WithLogContext } from '#src/middleware/koa-audit-log.js'; +import type TenantContext from '#src/tenants/TenantContext.js'; +import { getTenantId } from '#src/utils/tenant.js'; + +import { type InteractionProfile } from '../types.js'; + +type OrganizationProvisionPayload = + | { + userId: string; + email: string; + } + | { + userId: string; + ssoConnectorId: string; + }; + +export class ProvisionLibrary { + constructor( + private readonly tenantContext: TenantContext, + private readonly ctx: WithLogContext + ) {} + + /** + * This method is used to get the provision context for a new user registration. + * It will return the provision context based on the current tenant and the request context. + */ + async getUserProvisionContext(profile: InteractionProfile): Promise<{ + /** Admin user provisioning flag */ + isCreatingFirstAdminUser: boolean; + /** Initial user roles for admin tenant users */ + initialUserRoles: string[]; + /** Skip onboarding flow if the new user has pending Cloud invitations */ + customData?: { [userOnboardingDataKey]: UserOnboardingData }; + }> { + const { + provider, + queries: { + users: { hasActiveUsers }, + organizations: organizationsQueries, + }, + } = this.tenantContext; + + const { req, res, URL } = this.ctx; + + const [interactionDetails, [currentTenantId]] = await Promise.all([ + provider.interactionDetails(req, res), + getTenantId(URL), + ]); + + const { + params: { client_id }, + } = interactionDetails; + + const isAdminTenant = currentTenantId === adminTenantId; + const isAdminConsoleApp = String(client_id) === adminConsoleApplicationId; + + const { isCloud, isIntegrationTest } = EnvSet.values; + + /** + * Only allow creating the first admin user when + * + * - it's in OSS or integration tests + * - it's in the admin tenant + * - the client_id is the admin console application + * - there are no active users in the tenant + */ + const isCreatingFirstAdminUser = + (!isCloud || isIntegrationTest) && + isAdminTenant && + isAdminConsoleApp && + !(await hasActiveUsers()); + + const invitations = + isCloud && profile.primaryEmail + ? await organizationsQueries.invitations.findEntities({ + invitee: profile.primaryEmail, + }) + : []; + + const hasPendingInvitations = invitations.some( + (invitation) => invitation.status === OrganizationInvitationStatus.Pending + ); + + const initialUserRoles = this.getInitialUserRoles( + isAdminTenant, + isCreatingFirstAdminUser, + isCloud + ); + + // Skip onboarding flow if the new user has pending Cloud invitations + const customData = hasPendingInvitations + ? { + [userOnboardingDataKey]: { + isOnboardingDone: true, + } satisfies UserOnboardingData, + } + : undefined; + + return { + isCreatingFirstAdminUser, + initialUserRoles, + customData, + }; + } + + /** + * First admin user provision + * + * - For OSS, update the default sign-in experience to "sign-in only" once the first admin has been created. + * - Add the user to the default organization and assign the admin role. + */ + async adminUserProvision({ id }: User) { + const { isCloud } = EnvSet.values; + const { + queries: { signInExperiences, organizations }, + } = this.tenantContext; + + // In OSS, we need to limit sign-in experience to "sign-in only" once + // the first admin has been create since we don't want other unexpected registrations + await signInExperiences.updateDefaultSignInExperience({ + signInMode: isCloud ? SignInMode.SignInAndRegister : SignInMode.SignIn, + }); + + const organizationId = getTenantOrganizationId(defaultTenantId); + await organizations.relations.users.insert({ organizationId, userId: id }); + await organizations.relations.usersRoles.insert({ + organizationId, + userId: id, + organizationRoleId: getTenantRole(TenantRole.Admin).id, + }); + } + + /** + * Provision the organization for a new user + * + * - If the user has an enterprise SSO identity, provision the organization based on the SSO connector + * - Otherwise, provision the organization based on the primary email + */ + async newUserJtiOrganizationProvision( + userId: string, + { primaryEmail, enterpriseSsoIdentity }: InteractionProfile + ) { + if (enterpriseSsoIdentity) { + return this.jitOrganizationProvision({ + userId, + ssoConnectorId: enterpriseSsoIdentity.ssoConnectorId, + }); + } + if (primaryEmail) { + return this.jitOrganizationProvision({ + userId, + email: primaryEmail, + }); + } + } + + private async jitOrganizationProvision(payload: OrganizationProvisionPayload) { + const { + libraries: { users: usersLibraries }, + } = this.tenantContext; + + const provisionedOrganizations = await usersLibraries.provisionOrganizations(payload); + + // TODO: trigger hooks event + + return provisionedOrganizations; + } + + private readonly getInitialUserRoles = ( + isInAdminTenant: boolean, + isCreatingFirstAdminUser: boolean, + isCloud: boolean + ) => + conditionalArray( + isInAdminTenant && AdminTenantRole.User, + isCreatingFirstAdminUser && !isCloud && defaultManagementApiAdminName // OSS uses the legacy Management API user role + ); +} diff --git a/packages/integration-tests/src/tests/api/experience-api/register-interaction/organization-jti.test.ts b/packages/integration-tests/src/tests/api/experience-api/register-interaction/organization-jti.test.ts new file mode 100644 index 000000000000..5cc2208411cd --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/register-interaction/organization-jti.test.ts @@ -0,0 +1,175 @@ +import { ConnectorType } from '@logto/connector-kit'; +import { SignInIdentifier } from '@logto/schemas'; + +import { deleteUser, getUserOrganizations } from '#src/api/admin-user.js'; +import { updateSignInExperience } from '#src/api/sign-in-experience.js'; +import { SsoConnectorApi } from '#src/api/sso-connector.js'; +import { + clearConnectorsByTypes, + setEmailConnector, + setSmsConnector, +} from '#src/helpers/connector.js'; +import { + registerNewUserWithVerificationCode, + signInWithEnterpriseSso, + signInWithVerificationCode, +} from '#src/helpers/experience/index.js'; +import { OrganizationApiTest } from '#src/helpers/organization.js'; +import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { devFeatureTest, generateEmail, randomString } from '#src/utils.js'; + +devFeatureTest.describe('organization just-in-time provisioning', () => { + const organizationApi = new OrganizationApiTest(); + const ssoConnectorApi = new SsoConnectorApi(); + + beforeAll(async () => { + await clearConnectorsByTypes([ConnectorType.Email]); + await Promise.all([setEmailConnector(), setSmsConnector()]); + + await enableAllVerificationCodeSignInMethods({ + identifiers: [SignInIdentifier.Email], + password: false, + verify: true, + }); + + await updateSignInExperience({ + singleSignOnEnabled: true, + }); + }); + + afterEach(async () => { + await Promise.all([organizationApi.cleanUp(), ssoConnectorApi.cleanUp()]); + }); + + const prepare = async (emailDomain = `foo-${randomString()}.com`, ssoConnectorId?: string) => { + const organizations = await Promise.all([ + organizationApi.create({ name: 'foo' }), + organizationApi.create({ name: 'bar' }), + organizationApi.create({ name: 'baz' }), + ]); + const roles = await Promise.all([ + organizationApi.roleApi.create({ name: randomString() }), + organizationApi.roleApi.create({ name: randomString() }), + ]); + await Promise.all( + organizations.map(async (organization) => { + if (emailDomain) { + await organizationApi.jit.addEmailDomain(organization.id, emailDomain); + } + + if (ssoConnectorId) { + await organizationApi.jit.ssoConnectors.add(organization.id, [ssoConnectorId]); + } + }) + ); + await Promise.all([ + organizationApi.jit.roles.add(organizations[0].id, [roles[0].id, roles[1].id]), + organizationApi.jit.roles.add(organizations[1].id, [roles[0].id]), + ]); + return { + organizations, + roles, + emailDomain, + expectOrganizations: () => + expect.arrayContaining([ + expect.objectContaining({ + id: organizations[0].id, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + organizationRoles: expect.arrayContaining([ + expect.objectContaining({ id: roles[0].id }), + expect.objectContaining({ id: roles[1].id }), + ]), + }), + expect.objectContaining({ + id: organizations[1].id, + organizationRoles: [expect.objectContaining({ id: roles[0].id })], + }), + expect.objectContaining({ + id: organizations[2].id, + organizationRoles: [], + }), + ]), + }; + }; + + it('should automatically provision a user with matched email', async () => { + const { emailDomain, expectOrganizations } = await prepare(); + const email = randomString() + '@' + emailDomain; + + const userId = await registerNewUserWithVerificationCode({ + type: SignInIdentifier.Email, + value: email, + }); + + const userOrganizations = await getUserOrganizations(userId); + expect(userOrganizations).toEqual(expectOrganizations()); + + await deleteUser(userId); + }); + + it('should not provision a user with the matched email from a SSO identity', async () => { + const organization = await organizationApi.create({ name: 'sso_foo' }); + const domain = 'sso_example.com'; + await organizationApi.jit.addEmailDomain(organization.id, domain); + const connector = await ssoConnectorApi.createMockOidcConnector([domain]); + + const userId = await signInWithEnterpriseSso( + connector.id, + { + sub: randomString(), + email: generateEmail(domain), + email_verified: true, + }, + true + ); + + const userOrganizations = await getUserOrganizations(userId); + expect(userOrganizations).toEqual([]); + await deleteUser(userId); + }); + + it('should not provision an existing user with the matched email when sign-in', async () => { + const emailDomain = `foo-${randomString()}.com`; + const email = randomString() + '@' + emailDomain; + const userId = await registerNewUserWithVerificationCode({ + type: SignInIdentifier.Email, + value: email, + }); + + await prepare(emailDomain); + + await signInWithVerificationCode({ + type: SignInIdentifier.Email, + value: email, + }); + + const userOrganizations = await getUserOrganizations(userId); + expect(userOrganizations).toEqual([]); + await deleteUser(userId); + }); + + it('should provision a user with the matched sso connector', async () => { + const organization = await organizationApi.create({ name: 'sso_foo' }); + const domain = 'sso_example.com'; + const connector = await ssoConnectorApi.createMockOidcConnector([domain]); + await organizationApi.jit.ssoConnectors.add(organization.id, [connector.id]); + + const userId = await signInWithEnterpriseSso( + connector.id, + { + sub: randomString(), + email: generateEmail(domain), + email_verified: true, + }, + true + ); + + const userOrganizations = await getUserOrganizations(userId); + + expect(userOrganizations).toEqual( + expect.arrayContaining([expect.objectContaining({ id: organization.id })]) + ); + + await deleteUser(userId); + }); +}); diff --git a/packages/integration-tests/src/tests/api/interaction/organization-jit.test.ts b/packages/integration-tests/src/tests/api/interaction/organization-jit.test.ts index d315d08039f7..e78fd7ef327b 100644 --- a/packages/integration-tests/src/tests/api/interaction/organization-jit.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/organization-jit.test.ts @@ -180,7 +180,7 @@ describe('organization just-in-time provisioning', () => { await deleteUser(userId); }); - it('should not automatically provision an existing user when the user is an existing user', async () => { + it('should not automatically provision an existing user', async () => { const emailDomain = `foo-${randomString()}.com`; const email = randomString() + '@' + emailDomain; const { client } = await registerWithEmail(email); From 6fca3fe3c4deb63171f1c697a5d5d530ef9992af Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 17 Jul 2024 15:16:54 +0800 Subject: [PATCH 030/135] feat(connector): enable custom headers for SMTP connector (#6256) --- .changeset/slow-boxes-greet.md | 5 +++++ .../connectors/connector-smtp/src/constant.ts | 9 ++++++++ .../connector-smtp/src/index.test.ts | 22 +++++++++++++++++++ .../connectors/connector-smtp/src/index.ts | 11 ++++++++-- .../connectors/connector-smtp/src/mock.ts | 1 + .../connectors/connector-smtp/src/types.ts | 1 + 6 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 .changeset/slow-boxes-greet.md diff --git a/.changeset/slow-boxes-greet.md b/.changeset/slow-boxes-greet.md new file mode 100644 index 000000000000..3114f181f659 --- /dev/null +++ b/.changeset/slow-boxes-greet.md @@ -0,0 +1,5 @@ +--- +"@logto/connector-smtp": minor +--- + +enable static custom headers for SMTP connector diff --git a/packages/connectors/connector-smtp/src/constant.ts b/packages/connectors/connector-smtp/src/constant.ts index d2078b048c0e..28d9a42761f6 100644 --- a/packages/connectors/connector-smtp/src/constant.ts +++ b/packages/connectors/connector-smtp/src/constant.ts @@ -198,5 +198,14 @@ export const defaultMetadata: ConnectorMetadata = { type: ConnectorConfigFormItemType.Switch, required: false, }, + { + key: 'customHeaders', + label: 'Custom Headers', + type: ConnectorConfigFormItemType.Json, + required: false, + defaultValue: {}, + description: + 'Custom headers to be added to original email headers when sending messages. Both keys and values should be string-typed.', + }, ], }; diff --git a/packages/connectors/connector-smtp/src/index.test.ts b/packages/connectors/connector-smtp/src/index.test.ts index a28137dd32dd..b57ad02e1332 100644 --- a/packages/connectors/connector-smtp/src/index.test.ts +++ b/packages/connectors/connector-smtp/src/index.test.ts @@ -79,6 +79,28 @@ describe('SMTP connector', () => { to: 'baz', }); }); + + it('should send mail with customer headers', async () => { + const connector = await createConnector({ + getConfig: vi.fn().mockResolvedValue({ + ...mockedConfig, + customHeaders: { 'X-Test': 'test', 'X-Test-Another': ['test1', 'test2', 'test3'] }, + }), + }); + await connector.sendMessage({ + to: 'baz', + type: TemplateType.OrganizationInvitation, + payload: { code: '345678', link: 'https://example.com' }, + }); + + expect(sendMail).toHaveBeenCalledWith({ + from: '', + subject: 'Organization invitation', + text: 'This is for organization invitation. Your link is https://example.com.', + to: 'baz', + headers: { 'X-Test': 'test', 'X-Test-Another': ['test1', 'test2', 'test3'] }, + }); + }); }); describe('Test config guard', () => { diff --git a/packages/connectors/connector-smtp/src/index.ts b/packages/connectors/connector-smtp/src/index.ts index 8aed0e05dd65..f447f420e65c 100644 --- a/packages/connectors/connector-smtp/src/index.ts +++ b/packages/connectors/connector-smtp/src/index.ts @@ -1,4 +1,4 @@ -import { assert } from '@silverhand/essentials'; +import { assert, conditional } from '@silverhand/essentials'; import type { GetConnectorConfig, @@ -14,6 +14,7 @@ import { replaceSendMessageHandlebars, } from '@logto/connector-kit'; import nodemailer from 'nodemailer'; +import type Mail from 'nodemailer/lib/mailer'; import type SMTPTransport from 'nodemailer/lib/smtp-transport'; import { defaultMetadata } from './constant.js'; @@ -44,11 +45,17 @@ const sendMessage = template.contentType ); - const mailOptions = { + const mailOptions: Mail.Options = { to, from: config.fromEmail, replyTo: config.replyTo, subject: replaceSendMessageHandlebars(template.subject, payload), + ...conditional( + config.customHeaders && + Object.entries(config.customHeaders).length > 0 && { + headers: config.customHeaders, + } + ), ...contentsObject, }; diff --git a/packages/connectors/connector-smtp/src/mock.ts b/packages/connectors/connector-smtp/src/mock.ts index 87f2017d3b5f..bef4a38b1b1d 100644 --- a/packages/connectors/connector-smtp/src/mock.ts +++ b/packages/connectors/connector-smtp/src/mock.ts @@ -35,6 +35,7 @@ export const mockedConfig = { usageType: 'OrganizationInvitation', }, ], + customHeaders: {}, }; export const mockedOauth2AuthWithToken = { diff --git a/packages/connectors/connector-smtp/src/types.ts b/packages/connectors/connector-smtp/src/types.ts index 263fe802807f..cf18622105f5 100644 --- a/packages/connectors/connector-smtp/src/types.ts +++ b/packages/connectors/connector-smtp/src/types.ts @@ -125,6 +125,7 @@ export const smtpConfigGuard = z.object({ servername: z.string().optional(), ignoreTLS: z.boolean().optional(), requireTLS: z.boolean().optional(), + customHeaders: z.record(z.string().or(z.string().array())).optional(), }); export type SmtpConfig = z.infer; From 3aa7e57b3d481c4bc52386922476671a1f8875d9 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 17 Jul 2024 15:17:37 +0800 Subject: [PATCH 031/135] fix(console): fix Google connector `scope` field can not be unset bug (#6254) --- .changeset/cool-otters-unite.md | 5 +++++ .../src/pages/ConnectorDetails/ConnectorContent/index.tsx | 2 +- packages/console/src/utils/connector-form.ts | 5 ----- 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 .changeset/cool-otters-unite.md diff --git a/.changeset/cool-otters-unite.md b/.changeset/cool-otters-unite.md new file mode 100644 index 000000000000..b82659b6d7af --- /dev/null +++ b/.changeset/cool-otters-unite.md @@ -0,0 +1,5 @@ +--- +"@logto/console": patch +--- + +fix Google connector `scope` field can not be reset bug diff --git a/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx b/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx index 5d56a1849bce..48541240f5ba 100644 --- a/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx @@ -68,7 +68,7 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop const { syncProfile, name, logo, logoDark, target, rawConfig } = data; // Apply the raw config first to avoid losing data updated from other forms that are not // included in the form items. - const config = { ...rawConfig, ...configParser(data, formItems) }; + const config = removeFalsyValues({ ...rawConfig, ...configParser(data, formItems) }); const payload = isSocialConnector ? { diff --git a/packages/console/src/utils/connector-form.ts b/packages/console/src/utils/connector-form.ts index 237b0fa41a7b..762704cef949 100644 --- a/packages/console/src/utils/connector-form.ts +++ b/packages/console/src/utils/connector-form.ts @@ -27,11 +27,6 @@ export const parseFormConfig = ( return Object.fromEntries( Object.entries(config) .map(([key, value]) => { - // Filter out empty input - if (value === '') { - return null; - } - const formItem = formItems.find((item) => item.key === key); if (!formItem) { From f73b698381d1aeb16729d6c40cb04eae746bfb81 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 17 Jul 2024 15:44:37 +0800 Subject: [PATCH 032/135] style(experience): improve input filed style (#6260) --- .../InputField/NotchedBorder/index.module.scss | 16 +++++++++------- .../InputFields/InputField/index.module.scss | 1 - 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/experience/src/components/InputFields/InputField/NotchedBorder/index.module.scss b/packages/experience/src/components/InputFields/InputField/NotchedBorder/index.module.scss index 8e725c6b933e..371d9458c6c5 100644 --- a/packages/experience/src/components/InputFields/InputField/NotchedBorder/index.module.scss +++ b/packages/experience/src/components/InputFields/InputField/NotchedBorder/index.module.scss @@ -2,14 +2,14 @@ .container { position: absolute; - inset: 0; + inset: -0.4px -2px 0; pointer-events: none; .border, .outline { text-align: left; position: absolute; - inset: -8px 0 0; + inset: -10px 0 -0.5px; border: _.border(var(--color-line-border)); border-radius: var(--radius); pointer-events: none; @@ -18,6 +18,8 @@ legend { display: inline-block; visibility: hidden; + // To keep the label in the middle of the empty space + margin-left: 1px; padding: 0; // fix to the label font height to keep space for the input container height: 20px; @@ -36,7 +38,7 @@ .outline { display: none; - inset: -9px -2px -2px; + inset: -11.5px -2px -2.5px; border-radius: 10px; border: 3px outset var(--color-overlay-brand-focused); } @@ -47,8 +49,8 @@ font: var(--font-body-1); color: var(--color-type-secondary); pointer-events: none; - top: 50%; - transform: translateY(-46%); + top: 48%; + transform: translateY(-50%); transition: 0.2s ease-out; transition-property: position, font, top; background-color: transparent; @@ -65,8 +67,8 @@ } .label { - top: 0; - left: _.unit(3.25); + top: -1px; + left: _.unit(3.5); font: var(--font-body-3); color: var(--color-type-secondary); padding: 0 _.unit(1); diff --git a/packages/experience/src/components/InputFields/InputField/index.module.scss b/packages/experience/src/components/InputFields/InputField/index.module.scss index 3ccf92a02ae7..933740f08eaf 100644 --- a/packages/experience/src/components/InputFields/InputField/index.module.scss +++ b/packages/experience/src/components/InputFields/InputField/index.module.scss @@ -26,7 +26,6 @@ color: var(--color-type-primary); outline: none; border-radius: var(--radius); - margin: -1px 1px 0; &::placeholder { color: var(--color-type-secondary); From f8f14c0ba7cf13058ba25d160a7908a33d203e16 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 17 Jul 2024 17:33:47 +0800 Subject: [PATCH 033/135] feat(core): set up proxy to host custom ui assets if available (#6214) * feat(core): set up proxy to host custom ui assets if available * refactor: use object param for koa spa proxy middleware * refactor: make queries param mandatory --- .../koa-serve-custom-ui-assets.test.ts | 91 +++++++++++++++++++ .../middleware/koa-serve-custom-ui-assets.ts | 40 ++++++++ .../core/src/middleware/koa-spa-proxy.test.ts | 34 ++++++- packages/core/src/middleware/koa-spa-proxy.ts | 27 +++++- packages/core/src/tenants/Tenant.ts | 18 +++- 5 files changed, 199 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/middleware/koa-serve-custom-ui-assets.test.ts create mode 100644 packages/core/src/middleware/koa-serve-custom-ui-assets.ts diff --git a/packages/core/src/middleware/koa-serve-custom-ui-assets.test.ts b/packages/core/src/middleware/koa-serve-custom-ui-assets.test.ts new file mode 100644 index 000000000000..41621a06c376 --- /dev/null +++ b/packages/core/src/middleware/koa-serve-custom-ui-assets.test.ts @@ -0,0 +1,91 @@ +import { Readable } from 'node:stream'; + +import { StorageProvider } from '@logto/schemas'; +import { createMockUtils, pickDefault } from '@logto/shared/esm'; + +import RequestError from '#src/errors/RequestError/index.js'; +import SystemContext from '#src/tenants/SystemContext.js'; +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; + +const { jest } = import.meta; +const { mockEsmWithActual } = createMockUtils(jest); + +const experienceBlobsProviderConfig = { + provider: StorageProvider.AzureStorage, + connectionString: 'connectionString', + container: 'container', +} satisfies { + provider: StorageProvider.AzureStorage; + connectionString: string; + container: string; +}; + +// eslint-disable-next-line @silverhand/fp/no-mutation +SystemContext.shared.experienceBlobsProviderConfig = experienceBlobsProviderConfig; + +const mockedIsFileExisted = jest.fn(async (filename: string) => true); +const mockedDownloadFile = jest.fn(); + +await mockEsmWithActual('#src/utils/storage/azure-storage.js', () => ({ + buildAzureStorage: jest.fn(() => ({ + uploadFile: jest.fn(async () => 'https://fake.url'), + downloadFile: mockedDownloadFile, + isFileExisted: mockedIsFileExisted, + })), +})); + +await mockEsmWithActual('#src/utils/tenant.js', () => ({ + getTenantId: jest.fn().mockResolvedValue(['default']), +})); + +const koaServeCustomUiAssets = await pickDefault(import('./koa-serve-custom-ui-assets.js')); + +describe('koaServeCustomUiAssets middleware', () => { + const next = jest.fn(); + + it('should serve the file directly if the request path contains a dot', async () => { + const mockBodyStream = Readable.from('javascript content'); + mockedDownloadFile.mockImplementation(async (objectKey: string) => { + if (objectKey.endsWith('/scripts.js')) { + return { + contentType: 'text/javascript', + readableStreamBody: mockBodyStream, + }; + } + throw new Error('File not found'); + }); + const ctx = createMockContext({ url: '/scripts.js' }); + + await koaServeCustomUiAssets('custom-ui-asset-id')(ctx, next); + + expect(ctx.type).toEqual('text/javascript'); + expect(ctx.body).toEqual(mockBodyStream); + }); + + it('should serve the index.html', async () => { + const mockBodyStream = Readable.from(''); + mockedDownloadFile.mockImplementation(async (objectKey: string) => { + if (objectKey.endsWith('/index.html')) { + return { + contentType: 'text/html', + readableStreamBody: mockBodyStream, + }; + } + throw new Error('File not found'); + }); + const ctx = createMockContext({ url: '/sign-in' }); + await koaServeCustomUiAssets('custom-ui-asset-id')(ctx, next); + + expect(ctx.type).toEqual('text/html'); + expect(ctx.body).toEqual(mockBodyStream); + }); + + it('should return 404 if the file does not exist', async () => { + mockedIsFileExisted.mockResolvedValue(false); + const ctx = createMockContext({ url: '/fake.txt' }); + + await expect(koaServeCustomUiAssets('custom-ui-asset-id')(ctx, next)).rejects.toMatchError( + new RequestError({ code: 'entity.not_found', status: 404 }) + ); + }); +}); diff --git a/packages/core/src/middleware/koa-serve-custom-ui-assets.ts b/packages/core/src/middleware/koa-serve-custom-ui-assets.ts new file mode 100644 index 000000000000..0232fedf97ec --- /dev/null +++ b/packages/core/src/middleware/koa-serve-custom-ui-assets.ts @@ -0,0 +1,40 @@ +import type { MiddlewareType } from 'koa'; + +import SystemContext from '#src/tenants/SystemContext.js'; +import assertThat from '#src/utils/assert-that.js'; +import { buildAzureStorage } from '#src/utils/storage/azure-storage.js'; +import { getTenantId } from '#src/utils/tenant.js'; + +/** + * Middleware that serves custom UI assets user uploaded previously through sign-in experience settings. + * If the request path contains a dot, consider it as a file and will try to serve the file directly. + * Otherwise, redirect the request to the `index.html` page. + */ +export default function koaServeCustomUiAssets(customUiAssetId: string) { + const { experienceBlobsProviderConfig } = SystemContext.shared; + assertThat(experienceBlobsProviderConfig?.provider === 'AzureStorage', 'storage.not_configured'); + + const serve: MiddlewareType = async (ctx, next) => { + const [tenantId] = await getTenantId(ctx.URL); + assertThat(tenantId, 'session.not_found', 404); + + const { container, connectionString } = experienceBlobsProviderConfig; + const { downloadFile, isFileExisted } = buildAzureStorage(connectionString, container); + + const contextPath = `${tenantId}/${customUiAssetId}`; + const requestPath = ctx.request.path; + const isFileRequest = requestPath.includes('.'); + + const fileObjectKey = `${contextPath}${isFileRequest ? requestPath : '/index.html'}`; + const isExisted = await isFileExisted(fileObjectKey); + assertThat(isExisted, 'entity.not_found', 404); + + const downloadResponse = await downloadFile(fileObjectKey); + ctx.type = downloadResponse.contentType ?? 'application/octet-stream'; + ctx.body = downloadResponse.readableStreamBody; + + return next(); + }; + + return serve; +} diff --git a/packages/core/src/middleware/koa-spa-proxy.test.ts b/packages/core/src/middleware/koa-spa-proxy.test.ts index 3216ea0b8c3c..23f003c419c9 100644 --- a/packages/core/src/middleware/koa-spa-proxy.test.ts +++ b/packages/core/src/middleware/koa-spa-proxy.test.ts @@ -10,6 +10,7 @@ const { mockEsmDefault } = createMockUtils(jest); const mockProxyMiddleware = jest.fn(); const mockStaticMiddleware = jest.fn(); +const mockCustomUiAssetsMiddleware = jest.fn(); const mountedApps = Object.values(UserApps); mockEsmDefault('node:fs/promises', () => ({ @@ -18,6 +19,17 @@ mockEsmDefault('node:fs/promises', () => ({ mockEsmDefault('koa-proxies', () => jest.fn(() => mockProxyMiddleware)); mockEsmDefault('#src/middleware/koa-serve-static.js', () => jest.fn(() => mockStaticMiddleware)); +mockEsmDefault('#src/middleware/koa-serve-custom-ui-assets.js', () => + jest.fn(() => mockCustomUiAssetsMiddleware) +); + +const mockFindDefaultSignInExperience = jest.fn().mockResolvedValue({ customUiAssets: null }); +const { MockQueries } = await import('#src/test-utils/tenant.js'); +const queries = new MockQueries({ + signInExperiences: { + findDefaultSignInExperience: mockFindDefaultSignInExperience, + }, +}); const koaSpaProxy = await pickDefault(import('./koa-spa-proxy.js')); @@ -42,7 +54,7 @@ describe('koaSpaProxy middleware', () => { url: `/${app}/foo`, }); - await koaSpaProxy(mountedApps)(ctx, next); + await koaSpaProxy({ mountedApps, queries })(ctx, next); expect(mockProxyMiddleware).not.toBeCalled(); }); @@ -50,7 +62,7 @@ describe('koaSpaProxy middleware', () => { it('dev env should call dev proxy for SPA paths', async () => { const ctx = createContextWithRouteParameters(); - await koaSpaProxy(mountedApps)(ctx, next); + await koaSpaProxy({ mountedApps, queries })(ctx, next); expect(mockProxyMiddleware).toBeCalled(); }); @@ -64,7 +76,7 @@ describe('koaSpaProxy middleware', () => { url: '/foo', }); - await koaSpaProxy(mountedApps)(ctx, next); + await koaSpaProxy({ mountedApps, queries })(ctx, next); expect(mockStaticMiddleware).toBeCalled(); expect(ctx.request.path).toEqual('/'); @@ -81,8 +93,22 @@ describe('koaSpaProxy middleware', () => { url: '/sign-in', }); - await koaSpaProxy(mountedApps)(ctx, next); + await koaSpaProxy({ mountedApps, queries })(ctx, next); expect(mockStaticMiddleware).toBeCalled(); stub.restore(); }); + + it('should serve custom UI assets if user uploaded them', async () => { + const customUiAssets = { id: 'custom-ui-assets', createdAt: Date.now() }; + mockFindDefaultSignInExperience.mockResolvedValue({ customUiAssets }); + + const ctx = createContextWithRouteParameters({ + url: '/sign-in', + }); + + await koaSpaProxy({ mountedApps, queries })(ctx, next); + expect(mockCustomUiAssetsMiddleware).toBeCalled(); + expect(mockStaticMiddleware).not.toBeCalled(); + expect(mockProxyMiddleware).not.toBeCalled(); + }); }); diff --git a/packages/core/src/middleware/koa-spa-proxy.ts b/packages/core/src/middleware/koa-spa-proxy.ts index ede71a7ef4a2..6ec020135132 100644 --- a/packages/core/src/middleware/koa-spa-proxy.ts +++ b/packages/core/src/middleware/koa-spa-proxy.ts @@ -7,13 +7,25 @@ import type { IRouterParamContext } from 'koa-router'; import { EnvSet } from '#src/env-set/index.js'; import serveStatic from '#src/middleware/koa-serve-static.js'; +import type Queries from '#src/tenants/Queries.js'; -export default function koaSpaProxy( - mountedApps: string[], +import serveCustomUiAssets from './koa-serve-custom-ui-assets.js'; + +type Properties = { + readonly mountedApps: string[]; + readonly queries: Queries; + readonly packagePath?: string; + readonly port?: number; + readonly prefix?: string; +}; + +export default function koaSpaProxy({ + mountedApps, packagePath = 'experience', port = 5001, - prefix = '' -): MiddlewareType { + prefix = '', + queries, +}: Properties): MiddlewareType { type Middleware = MiddlewareType; const distributionPath = path.join('node_modules/@logto', packagePath, 'dist'); @@ -43,6 +55,13 @@ export default function koaSpaProxy Date: Thu, 18 Jul 2024 11:04:12 +0800 Subject: [PATCH 034/135] style(experience): remove autofill style from input component (#6261) --- .../InputFields/InputField/index.module.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/experience/src/components/InputFields/InputField/index.module.scss b/packages/experience/src/components/InputFields/InputField/index.module.scss index 933740f08eaf..967acc82ebcf 100644 --- a/packages/experience/src/components/InputFields/InputField/index.module.scss +++ b/packages/experience/src/components/InputFields/InputField/index.module.scss @@ -32,6 +32,18 @@ transition: opacity 0.2s ease-out; opacity: 0%; } + + &:-webkit-autofill, + &:-webkit-autofill:hover, + &:-webkit-autofill:focus { + -webkit-text-fill-color: var(--color-type-primary); + /* + * Create an extremely long (about 11.57 days) transition for background-color + * This is a "hack" to prevent the browser from applying its default autofill background color + */ + transition: background-color 999999s ease-in-out 0s; + background-color: transparent; + } } .suffix { From 9772392e6ae19679aa7fef2165d3a971f2df04a5 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 18 Jul 2024 12:20:35 +0800 Subject: [PATCH 035/135] fix(console): fix image upload in onboarding process (#6266) --- .../src/onboarding/pages/SignInExperience/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/console/src/onboarding/pages/SignInExperience/index.tsx b/packages/console/src/onboarding/pages/SignInExperience/index.tsx index 366e7127c14d..e0579ef891e3 100644 --- a/packages/console/src/onboarding/pages/SignInExperience/index.tsx +++ b/packages/console/src/onboarding/pages/SignInExperience/index.tsx @@ -175,7 +175,12 @@ function SignInExperience() { name="logo" control={control} render={({ field: { onChange, value, name } }) => ( - + )} /> ) : ( From ca73d3142f8697961f35d250038c934ec13d7095 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Thu, 18 Jul 2024 14:04:56 +0800 Subject: [PATCH 036/135] fix(console): fix grant data card height (#6264) --- .../MainContent/SettingsSection/InstructionTab/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx index 58455222e768..7fc7bedfe6c2 100644 --- a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx +++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx @@ -91,7 +91,7 @@ function InstructionTab({ isActive }: Props) { language="typescript" className={styles.sampleCode} value={jwtCustomizerGrantContextTypeDefinition} - height="400px" + height="180px" theme="logto-dark" options={typeDefinitionCodeEditorOptions} /> From 6963192ac5cf2285826799a34a20c9efaa1f343a Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 18 Jul 2024 15:53:48 +0800 Subject: [PATCH 037/135] fix(console): fix passwordless connector tester send failed bug (#6268) --- .../src/hooks/use-connector-form-config-parser.tsx | 8 ++++++-- .../src/pages/ConnectorDetails/ConnectorContent/index.tsx | 6 +++++- packages/console/src/utils/connector-form.ts | 8 +++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/console/src/hooks/use-connector-form-config-parser.tsx b/packages/console/src/hooks/use-connector-form-config-parser.tsx index 38ef40dcc0fb..d6e20f501ea1 100644 --- a/packages/console/src/hooks/use-connector-form-config-parser.tsx +++ b/packages/console/src/hooks/use-connector-form-config-parser.tsx @@ -31,9 +31,13 @@ const useJsonStringConfigParser = () => { export const useConnectorFormConfigParser = () => { const parseJsonConfig = useJsonStringConfigParser(); - return (data: ConnectorFormType, formItems: ConnectorResponse['formItems']) => { + return ( + data: ConnectorFormType, + formItems: ConnectorResponse['formItems'], + skipFalsyValuesRemoval = false + ) => { return formItems - ? parseFormConfig(data.formConfig, formItems) + ? parseFormConfig(data.formConfig, formItems, skipFalsyValuesRemoval) : parseJsonConfig(data.jsonConfig); }; }; diff --git a/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx b/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx index 48541240f5ba..da1c0dd9d325 100644 --- a/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/ConnectorContent/index.tsx @@ -68,7 +68,11 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop const { syncProfile, name, logo, logoDark, target, rawConfig } = data; // Apply the raw config first to avoid losing data updated from other forms that are not // included in the form items. - const config = removeFalsyValues({ ...rawConfig, ...configParser(data, formItems) }); + // Explicitly SKIP falsy values removal logic (the last argument of `configParser()` method) for social connectors. + const config = removeFalsyValues({ + ...rawConfig, + ...configParser(data, formItems, isSocialConnector), + }); const payload = isSocialConnector ? { diff --git a/packages/console/src/utils/connector-form.ts b/packages/console/src/utils/connector-form.ts index 762704cef949..f0a7f423c50a 100644 --- a/packages/console/src/utils/connector-form.ts +++ b/packages/console/src/utils/connector-form.ts @@ -22,11 +22,17 @@ const initFormData = (formItems: ConnectorConfigFormItem[], config?: Record, - formItems: ConnectorConfigFormItem[] + formItems: ConnectorConfigFormItem[], + skipFalsyValuesRemoval = false ) => { return Object.fromEntries( Object.entries(config) .map(([key, value]) => { + // Filter out empty input + if (!skipFalsyValuesRemoval && value === '') { + return null; + } + const formItem = formItems.find((item) => item.key === key); if (!formItem) { From 17d7be39924f1e464ffa04d52b031a3545700481 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 18 Jul 2024 15:56:09 +0800 Subject: [PATCH 038/135] feat(console): implement custom ui assets upload component (#6217) --- .../src/assets/images/blur-preview.svg | 102 ++++++++++++++++++ .../CustomUiAssetsUploader/index.module.scss | 62 +++++++++++ .../CustomUiAssetsUploader/index.tsx | 89 +++++++++++++++ .../console/src/components/FileIcon/index.tsx | 20 ++++ .../src/components/ImageInputs/index.tsx | 4 +- .../SignInExperiencePreview/index.module.scss | 24 +++++ .../SignInExperiencePreview/index.tsx | 74 +++++++++---- packages/console/src/consts/user-assets.ts | 17 --- .../Uploader/FileUploader/index.tsx | 17 +-- .../Uploader/ImageUploaderField/index.tsx | 4 +- .../Experience/LogosUploader/index.tsx | 8 +- .../index.module.scss | 0 .../{CustomCssForm => CustomUiForm}/index.tsx | 48 +++++++-- .../PageContent/Branding/index.tsx | 4 +- .../SignInExperience/PageContent/index.tsx | 1 + .../PageContent/utils/parser.ts | 5 +- .../components/Preview/index.tsx | 3 + .../src/pages/SignInExperience/types.ts | 10 +- packages/console/src/utils/uploader.ts | 19 +++- .../admin-console/sign-in-exp/index.ts | 11 +- packages/schemas/src/types/user-assets.ts | 18 ++++ 21 files changed, 471 insertions(+), 69 deletions(-) create mode 100644 packages/console/src/assets/images/blur-preview.svg create mode 100644 packages/console/src/components/CustomUiAssetsUploader/index.module.scss create mode 100644 packages/console/src/components/CustomUiAssetsUploader/index.tsx create mode 100644 packages/console/src/components/FileIcon/index.tsx delete mode 100644 packages/console/src/consts/user-assets.ts rename packages/console/src/pages/SignInExperience/PageContent/Branding/{CustomCssForm => CustomUiForm}/index.module.scss (100%) rename packages/console/src/pages/SignInExperience/PageContent/Branding/{CustomCssForm => CustomUiForm}/index.tsx (51%) diff --git a/packages/console/src/assets/images/blur-preview.svg b/packages/console/src/assets/images/blur-preview.svg new file mode 100644 index 000000000000..9d07235496a8 --- /dev/null +++ b/packages/console/src/assets/images/blur-preview.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/console/src/components/CustomUiAssetsUploader/index.module.scss b/packages/console/src/components/CustomUiAssetsUploader/index.module.scss new file mode 100644 index 000000000000..34b5b4ae7b7b --- /dev/null +++ b/packages/console/src/components/CustomUiAssetsUploader/index.module.scss @@ -0,0 +1,62 @@ +@use '@/scss/underscore' as _; + +.placeholder { + display: flex; + align-items: center; + padding: _.unit(5) _.unit(5) _.unit(5) _.unit(4); + border: 1px solid var(--color-border); + border-radius: 12px; + gap: _.unit(4); + position: relative; + overflow: hidden; + + .main { + display: flex; + flex-direction: column; + gap: _.unit(0.5); + flex: 1; + + .name { + font: var(--font-label-2); + color: var(--color-text-primary); + } + + .secondaryInfo { + display: flex; + align-items: center; + gap: _.unit(3); + } + + .info { + font: var(--font-body-3); + color: var(--color-text-secondary); + } + + .error { + font: var(--font-body-3); + color: var(--color-error); + } + } + + .icon { + width: 40px; + height: 40px; + } + + // display a fake progress bar on the bottom with animations + .progressBar { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 4px; + background-color: var(--color-primary); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.3s; + } + + &.hasError { + border-color: var(--color-error); + } +} diff --git a/packages/console/src/components/CustomUiAssetsUploader/index.tsx b/packages/console/src/components/CustomUiAssetsUploader/index.tsx new file mode 100644 index 000000000000..79b240d817a8 --- /dev/null +++ b/packages/console/src/components/CustomUiAssetsUploader/index.tsx @@ -0,0 +1,89 @@ +import { type CustomUiAssets, maxUploadFileSize, type AllowedUploadMimeType } from '@logto/schemas'; +import { format } from 'date-fns/fp'; +import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import DeleteIcon from '@/assets/icons/delete.svg'; +import IconButton from '@/ds-components/IconButton'; +import FileUploader from '@/ds-components/Uploader/FileUploader'; +import { formatBytes } from '@/utils/uploader'; + +import FileIcon from '../FileIcon'; + +import * as styles from './index.module.scss'; + +type Props = { + readonly value?: CustomUiAssets; + readonly onChange: (value: CustomUiAssets) => void; +}; + +const allowedMimeTypes: AllowedUploadMimeType[] = ['application/zip']; + +function CustomUiAssetsUploader({ value, onChange }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const [file, setFile] = useState(); + const [error, setError] = useState(); + const showUploader = !value?.id && !file && !error; + + const onComplete = useCallback( + (id: string) => { + setFile(undefined); + onChange({ id, createdAt: Date.now() }); + }, + [onChange] + ); + + const onErrorChange = useCallback( + (errorMessage?: string, files?: File[]) => { + if (errorMessage) { + setError(errorMessage); + } + if (files?.length) { + setFile(files[0]); + } + }, + [setError, setFile] + ); + + if (showUploader) { + return ( + + allowedMimeTypes={allowedMimeTypes} + maxSize={maxUploadFileSize} + uploadUrl="api/sign-in-exp/default/custom-ui-assets" + onCompleted={({ customUiAssetId }) => { + onComplete(customUiAssetId); + }} + onUploadErrorChange={onErrorChange} + /> + ); + } + + return ( +
+ +
+
{file?.name ?? t('sign_in_exp.custom_ui.title')}
+
+ {!!value?.createdAt && ( + {format('yyyy/MM/dd HH:mm')(value.createdAt)} + )} + {file && {formatBytes(file.size)}} + {error && {error}} +
+
+ { + setFile(undefined); + setError(undefined); + onChange({ id: '', createdAt: 0 }); + }} + > + + + {file &&
} +
+ ); +} + +export default CustomUiAssetsUploader; diff --git a/packages/console/src/components/FileIcon/index.tsx b/packages/console/src/components/FileIcon/index.tsx new file mode 100644 index 000000000000..bab200d1d0fd --- /dev/null +++ b/packages/console/src/components/FileIcon/index.tsx @@ -0,0 +1,20 @@ +import { Theme } from '@logto/schemas'; +import { type ReactNode } from 'react'; + +import FileIconDark from '@/assets/icons/file-icon-dark.svg'; +import FileIconLight from '@/assets/icons/file-icon.svg'; +import useTheme from '@/hooks/use-theme'; + +const themeToRoleIcon = Object.freeze({ + [Theme.Light]: , + [Theme.Dark]: , +} satisfies Record); + +/** Render a role icon according to the current theme. */ +const FileIcon = () => { + const theme = useTheme(); + + return themeToRoleIcon[theme]; +}; + +export default FileIcon; diff --git a/packages/console/src/components/ImageInputs/index.tsx b/packages/console/src/components/ImageInputs/index.tsx index a9f8d6f5e908..dcabc41d39a6 100644 --- a/packages/console/src/components/ImageInputs/index.tsx +++ b/packages/console/src/components/ImageInputs/index.tsx @@ -137,7 +137,9 @@ function ImageInputs({ actionDescription={t(`sign_in_exp.branding.with_${field.theme}`, { value: t(`sign_in_exp.branding_uploads.${field.type}.title`), })} - onCompleted={onChange} + onCompleted={({ url }) => { + onChange(url); + }} // Noop fallback should not be necessary, but for TypeScript to be happy onUploadErrorChange={uploadErrorChangeHandlers[field.name] ?? noop} onDelete={() => { diff --git a/packages/console/src/components/SignInExperiencePreview/index.module.scss b/packages/console/src/components/SignInExperiencePreview/index.module.scss index 7f9457496653..4948109aad7c 100644 --- a/packages/console/src/components/SignInExperiencePreview/index.module.scss +++ b/packages/console/src/components/SignInExperiencePreview/index.module.scss @@ -85,4 +85,28 @@ } } } + + &.disabled { + background: url('raw:../../assets/images/blur-preview.svg') 0 0 / 100% auto no-repeat; + } + + .placeholder { + width: 100%; + height: calc(_screenSize.$web-height + _.unit(20)); + padding: _.unit(10); + backdrop-filter: blur(25px); + display: flex; + flex-direction: column; + color: var(--color-static-white); + + .title { + font: var(--font-label-2); + } + + .description { + margin-top: _.unit(1.5); + font: var(--font-body-2); + white-space: pre-wrap; + } + } } diff --git a/packages/console/src/components/SignInExperiencePreview/index.tsx b/packages/console/src/components/SignInExperiencePreview/index.tsx index ca0a61ac8a11..ef6ff41b2891 100644 --- a/packages/console/src/components/SignInExperiencePreview/index.tsx +++ b/packages/console/src/components/SignInExperiencePreview/index.tsx @@ -4,7 +4,15 @@ import type { ConnectorMetadata, SignInExperience, ConnectorResponse } from '@lo import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; import { format } from 'date-fns'; -import { useContext, useRef, useMemo, useCallback, useEffect, useState } from 'react'; +import { + useContext, + useRef, + useMemo, + useCallback, + useEffect, + useState, + type ReactNode, +} from 'react'; import { useTranslation } from 'react-i18next'; import useSWR from 'swr'; @@ -28,6 +36,16 @@ type Props = { * the `AppDataContext` will be used. */ readonly endpoint?: URL; + /** + * Whether the preview is disabled. If `true`, the preview will be disabled and a placeholder will + * be shown instead. Defaults to `false`. + */ + // eslint-disable-next-line react/boolean-prop-naming + readonly disabled?: boolean; + /** + * The placeholder to show when the preview is disabled. + */ + readonly disabledPlaceholder?: ReactNode; }; function SignInExperiencePreview({ @@ -36,6 +54,8 @@ function SignInExperiencePreview({ language = 'en', signInExperience, endpoint: endpointInput, + disabled = false, + disabledPlaceholder, }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); @@ -97,6 +117,10 @@ function SignInExperiencePreview({ }, []); useEffect(() => { + if (disabled) { + setIframeLoaded(false); + return; + } const iframe = previewRef.current; iframe?.addEventListener('load', iframeOnLoadEventHandler); @@ -104,7 +128,7 @@ function SignInExperiencePreview({ return () => { iframe?.removeEventListener('load', iframeOnLoadEventHandler); }; - }, [iframeLoaded, iframeOnLoadEventHandler]); + }, [iframeLoaded, disabled, iframeOnLoadEventHandler]); useEffect(() => { if (!iframeLoaded) { @@ -122,7 +146,8 @@ function SignInExperiencePreview({
-
-
- {platform !== PreviewPlatform.DesktopWeb && ( -
-
{format(Date.now(), 'HH:mm')}
- -
- )} -