0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(core): guard password by policy

This commit is contained in:
Gao Sun 2023-09-03 02:11:22 +08:00
parent ed7d842517
commit b8a7b900e1
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
44 changed files with 429 additions and 204 deletions

View file

@ -60,7 +60,7 @@
"@microsoft/applicationinsights-clickanalytics-js": "^3.0.2", "@microsoft/applicationinsights-clickanalytics-js": "^3.0.2",
"@microsoft/applicationinsights-react-js": "^17.0.0", "@microsoft/applicationinsights-react-js": "^17.0.0",
"@microsoft/applicationinsights-web": "^3.0.2", "@microsoft/applicationinsights-web": "^3.0.2",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"applicationinsights": "^2.7.0" "applicationinsights": "^2.7.0"
}, },
"peerDependencies": { "peerDependencies": {

View file

@ -50,7 +50,7 @@
"@logto/phrases-ui": "workspace:^1.2.0", "@logto/phrases-ui": "workspace:^1.2.0",
"@logto/schemas": "workspace:1.8.0", "@logto/schemas": "workspace:1.8.0",
"@logto/shared": "workspace:^2.0.0", "@logto/shared": "workspace:^2.0.0",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"chalk": "^5.0.0", "chalk": "^5.0.0",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",

View file

@ -23,7 +23,7 @@
"prepublishOnly": "pnpm build" "prepublishOnly": "pnpm build"
}, },
"dependencies": { "dependencies": {
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"got": "^13.0.0", "got": "^13.0.0",
"snakecase-keys": "^5.4.4", "snakecase-keys": "^5.4.4",
"zod": "^3.20.2" "zod": "^3.20.2"

View file

@ -44,7 +44,7 @@
"@parcel/transformer-svg-react": "2.9.3", "@parcel/transformer-svg-react": "2.9.3",
"@silverhand/eslint-config": "4.0.1", "@silverhand/eslint-config": "4.0.1",
"@silverhand/eslint-config-react": "4.0.1", "@silverhand/eslint-config-react": "4.0.1",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"@silverhand/ts-config": "4.0.0", "@silverhand/ts-config": "4.0.0",
"@silverhand/ts-config-react": "4.0.0", "@silverhand/ts-config-react": "4.0.0",
"@swc/core": "^1.3.52", "@swc/core": "^1.3.52",

View file

@ -41,7 +41,7 @@
"@logto/schemas": "workspace:^1.8.0", "@logto/schemas": "workspace:^1.8.0",
"@logto/shared": "workspace:^2.0.0", "@logto/shared": "workspace:^2.0.0",
"@logto/ui": "workspace:*", "@logto/ui": "workspace:*",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"@withtyped/client": "^0.7.22", "@withtyped/client": "^0.7.22",
"chalk": "^5.0.0", "chalk": "^5.0.0",
"clean-deep": "^3.4.0", "clean-deep": "^3.4.0",

View file

@ -91,4 +91,5 @@ export const mockSignInExperience: SignInExperience = {
signInMode: SignInMode.SignInAndRegister, signInMode: SignInMode.SignInAndRegister,
customCss: null, customCss: null,
customContent: {}, customContent: {},
passwordPolicy: {},
}; };

View file

@ -32,12 +32,13 @@ describe('sign-in-experience query', () => {
signUp: JSON.stringify(mockSignInExperience.signUp), signUp: JSON.stringify(mockSignInExperience.signUp),
socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets), socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets),
customContent: JSON.stringify(mockSignInExperience.customContent), customContent: JSON.stringify(mockSignInExperience.customContent),
passwordPolicy: JSON.stringify(mockSignInExperience.passwordPolicy),
}; };
it('findDefaultSignInExperience', async () => { it('findDefaultSignInExperience', async () => {
/* eslint-disable sql/no-unsafe-query */ /* eslint-disable sql/no-unsafe-query */
const expectSql = ` const expectSql = `
select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css", "custom_content" select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css", "custom_content", "password_policy"
from "sign_in_experiences" from "sign_in_experiences"
where "id"=$1 where "id"=$1
`; `;

View file

@ -76,6 +76,10 @@ const { sendVerificationCodeToIdentifier } = await mockEsmWithActual(
}) })
); );
const { validatePassword } = await mockEsmWithActual('./utils/validate-password.js', () => ({
validatePassword: jest.fn(),
}));
const { createLog, prependAllLogEntries } = createMockLogContext(); const { createLog, prependAllLogEntries } = createMockLogContext();
await mockEsmWithActual( await mockEsmWithActual(
@ -151,6 +155,7 @@ describe('interaction routes', () => {
expect(verifyIdentifierSettings).toBeCalled(); expect(verifyIdentifierSettings).toBeCalled();
expect(verifyProfileSettings).toBeCalled(); expect(verifyProfileSettings).toBeCalled();
expect(verifyIdentifierPayload).toBeCalled(); expect(verifyIdentifierPayload).toBeCalled();
expect(validatePassword).toBeCalled();
expect(storeInteractionResult).toBeCalled(); expect(storeInteractionResult).toBeCalled();
expect(response.status).toEqual(204); expect(response.status).toEqual(204);
}); });

View file

@ -37,6 +37,7 @@ import {
verifyProfileSettings, verifyProfileSettings,
} from './utils/sign-in-experience-validation.js'; } from './utils/sign-in-experience-validation.js';
import { createSocialAuthorizationUrl } from './utils/social-verification.js'; import { createSocialAuthorizationUrl } from './utils/social-verification.js';
import { validatePassword } from './utils/validate-password.js';
import { sendVerificationCodeToIdentifier } from './utils/verification-code-validation.js'; import { sendVerificationCodeToIdentifier } from './utils/verification-code-validation.js';
import { import {
verifyIdentifierPayload, verifyIdentifierPayload,
@ -71,6 +72,10 @@ export default function interactionRoutes<T extends AnonymousRouter>(
status: [204, 400, 401, 403, 422], status: [204, 400, 401, 403, 422],
}), }),
koaInteractionSie(queries), koaInteractionSie(queries),
async ({ guard: { body }, passwordPolicyChecker }, next) => {
await validatePassword(body.profile?.password, passwordPolicyChecker);
return next();
},
async (ctx, next) => { async (ctx, next) => {
const { event, identifier, profile } = ctx.guard.body; const { event, identifier, profile } = ctx.guard.body;
const { signInExperience, createLog } = ctx; const { signInExperience, createLog } = ctx;
@ -205,6 +210,10 @@ export default function interactionRoutes<T extends AnonymousRouter>(
status: [204, 400, 404], status: [204, 400, 404],
}), }),
koaInteractionSie(queries), koaInteractionSie(queries),
async ({ guard: { body }, passwordPolicyChecker }, next) => {
await validatePassword(body.password, passwordPolicyChecker);
return next();
},
async (ctx, next) => { async (ctx, next) => {
const profilePayload = ctx.guard.body; const profilePayload = ctx.guard.body;
const { signInExperience, interactionDetails, createLog } = ctx; const { signInExperience, interactionDetails, createLog } = ctx;
@ -243,6 +252,10 @@ export default function interactionRoutes<T extends AnonymousRouter>(
status: [204, 400, 404], status: [204, 400, 404],
}), }),
koaInteractionSie(queries), koaInteractionSie(queries),
async ({ guard: { body }, passwordPolicyChecker }, next) => {
await validatePassword(body.password, passwordPolicyChecker);
return next();
},
async (ctx, next) => { async (ctx, next) => {
const profilePayload = ctx.guard.body; const profilePayload = ctx.guard.body;
const { signInExperience, interactionDetails, createLog } = ctx; const { signInExperience, interactionDetails, createLog } = ctx;
@ -373,6 +386,8 @@ export default function interactionRoutes<T extends AnonymousRouter>(
// Check interaction exists // Check interaction exists
const { event } = getInteractionStorage(interactionDetails.result); const { event } = getInteractionStorage(interactionDetails.result);
// This file needs refactor
// eslint-disable-next-line max-lines
await sendVerificationCodeToIdentifier( await sendVerificationCodeToIdentifier(
{ event, ...guard.body }, { event, ...guard.body },
interactionDetails.jti, interactionDetails.jti,

View file

@ -1,12 +1,23 @@
import crypto from 'node:crypto';
import { PasswordPolicyChecker } from '@logto/core-kit';
import type { SignInExperience } from '@logto/schemas'; import type { SignInExperience } from '@logto/schemas';
import type { MiddlewareType } from 'koa'; import type { MiddlewareType } from 'koa';
import { type IRouterParamContext } from 'koa-router'; import { type IRouterParamContext } from 'koa-router';
import type Queries from '#src/tenants/Queries.js'; import type Queries from '#src/tenants/Queries.js';
/**
* Extend the context with the default sign-in experience and the corresponding
* password policy checker.
*/
export type WithInteractionSieContext<ContextT extends IRouterParamContext = IRouterParamContext> = export type WithInteractionSieContext<ContextT extends IRouterParamContext = IRouterParamContext> =
ContextT & { signInExperience: SignInExperience }; ContextT & { signInExperience: SignInExperience; passwordPolicyChecker: PasswordPolicyChecker };
/**
* Create a middleware that injects the default sign-in experience and the
* corresponding password policy checker into the context.
*/
export default function koaInteractionSie<StateT, ContextT extends IRouterParamContext, ResponseT>({ export default function koaInteractionSie<StateT, ContextT extends IRouterParamContext, ResponseT>({
signInExperiences: { findDefaultSignInExperience }, signInExperiences: { findDefaultSignInExperience },
}: Queries): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> { }: Queries): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> {
@ -14,6 +25,10 @@ export default function koaInteractionSie<StateT, ContextT extends IRouterParamC
const signInExperience = await findDefaultSignInExperience(); const signInExperience = await findDefaultSignInExperience();
ctx.signInExperience = signInExperience; ctx.signInExperience = signInExperience;
ctx.passwordPolicyChecker = new PasswordPolicyChecker(
signInExperience.passwordPolicy,
crypto.subtle
);
return next(); return next();
}; };

View file

@ -0,0 +1,22 @@
import { type PasswordPolicyChecker } from '@logto/core-kit';
import { type Optional } from '@silverhand/essentials';
import RequestError from '#src/errors/RequestError/index.js';
/**
* Validate password against the given password policy if the password is not undefined,
* throw a {@link RequestError} if the password is invalid; otherwise, do nothing.
*/
export const validatePassword = async (
password: Optional<string>,
checker: PasswordPolicyChecker
) => {
if (password === undefined) {
return;
}
const issues = await checker.check(password, {});
if (issues.length > 0) {
throw new RequestError('password.password_rejected', issues);
}
};

View file

@ -1,3 +1,6 @@
import crypto from 'node:crypto';
import { PasswordPolicyChecker } from '@logto/core-kit';
import { InteractionEvent, MissingProfile, SignInIdentifier } from '@logto/schemas'; import { InteractionEvent, MissingProfile, SignInIdentifier } from '@logto/schemas';
import { createMockUtils, pickDefault } from '@logto/shared/esm'; import { createMockUtils, pickDefault } from '@logto/shared/esm';
import type Provider from 'oidc-provider'; import type Provider from 'oidc-provider';
@ -32,6 +35,10 @@ describe('validateMandatoryUserProfile', () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
interactionDetails: {} as Awaited<ReturnType<Provider['interactionDetails']>>, interactionDetails: {} as Awaited<ReturnType<Provider['interactionDetails']>>,
signInExperience: mockSignInExperience, signInExperience: mockSignInExperience,
passwordPolicyChecker: new PasswordPolicyChecker(
mockSignInExperience.passwordPolicy,
crypto.subtle
),
}; };
const interaction: IdentifierVerifiedInteractionResult = { const interaction: IdentifierVerifiedInteractionResult = {
@ -274,7 +281,7 @@ describe('validateMandatoryUserProfile', () => {
}); });
}); });
describe('email or Phone required', () => { describe('email or phone required', () => {
const ctx = { const ctx = {
...baseCtx, ...baseCtx,
signInExperience: { signInExperience: {

View file

@ -20,6 +20,7 @@ import {
ZodString, ZodString,
ZodUnion, ZodUnion,
ZodUnknown, ZodUnknown,
ZodDefault,
} from 'zod'; } from 'zod';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
@ -266,5 +267,13 @@ export const zodTypeToSwagger = (
} }
} }
if (config instanceof ZodDefault) {
return {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
default: config._def.defaultValue(),
...zodTypeToSwagger(config._def.innerType),
};
}
throw new RequestError('swagger.invalid_zod_type', config); throw new RequestError('swagger.invalid_zod_type', config);
}; };

View file

@ -28,7 +28,7 @@
"@logto/schemas": "workspace:^1.6.0", "@logto/schemas": "workspace:^1.6.0",
"@logto/shared": "workspace:^2.0.0", "@logto/shared": "workspace:^2.0.0",
"@silverhand/eslint-config": "4.0.1", "@silverhand/eslint-config": "4.0.1",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"@silverhand/ts-config": "4.0.0", "@silverhand/ts-config": "4.0.0",
"@types/expect-puppeteer": "^5.0.3", "@types/expect-puppeteer": "^5.0.3",
"@types/jest": "^29.4.0", "@types/jest": "^29.4.0",

View file

@ -33,8 +33,9 @@
"url": "https://github.com/logto-io/logto/issues" "url": "https://github.com/logto-io/logto/issues"
}, },
"dependencies": { "dependencies": {
"@logto/core-kit": "workspace:^2.0.1",
"@logto/language-kit": "workspace:^1.0.0", "@logto/language-kit": "workspace:^1.0.0",
"@silverhand/essentials": "^2.5.0" "@silverhand/essentials": "^2.8.4"
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^3.20.2" "zod": "^3.20.2"

View file

@ -34,7 +34,7 @@
}, },
"dependencies": { "dependencies": {
"@logto/language-kit": "workspace:^1.0.0", "@logto/language-kit": "workspace:^1.0.0",
"@silverhand/essentials": "^2.5.0" "@silverhand/essentials": "^2.8.4"
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^3.20.2" "zod": "^3.20.2"

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'Die Verschlüsselungsmethode {{name}} wird nicht unterstützt.', unsupported_encryption_method: 'Die Verschlüsselungsmethode {{name}} wird nicht unterstützt.',
pepper_not_found: 'Password pepper not found. Please check your core envs.', pepper_not_found: 'Password pepper not found. Please check your core envs.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'The encryption method {{name}} is not supported.', unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
pepper_not_found: 'Password pepper not found. Please check your core envs.', pepper_not_found: 'Password pepper not found. Please check your core envs.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.',
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'El método de encriptación {{name}} no es compatible.', unsupported_encryption_method: 'El método de encriptación {{name}} no es compatible.',
pepper_not_found: 'No se encontró el password pepper. Por favor revisa tus variables de entorno.', pepper_not_found: 'No se encontró el password pepper. Por favor revisa tus variables de entorno.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -2,6 +2,7 @@ const password = {
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.", unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",
pepper_not_found: pepper_not_found:
'Mot de passe pepper non trouvé. Veuillez vérifier votre environnement de base.', 'Mot de passe pepper non trouvé. Veuillez vérifier votre environnement de base.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'Il metodo di crittografia {{name}} non è supportato.', unsupported_encryption_method: 'Il metodo di crittografia {{name}} non è supportato.',
pepper_not_found: 'Pepper password non trovato. Per favore controlla le tue env di core.', pepper_not_found: 'Pepper password non trovato. Per favore controlla le tue env di core.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: '暗号化方式 {{name}} はサポートされていません。', unsupported_encryption_method: '暗号化方式 {{name}} はサポートされていません。',
pepper_not_found: 'パスワードペッパーが見つかりません。コアの環境を確認してください。', pepper_not_found: 'パスワードペッパーが見つかりません。コアの環境を確認してください。',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.', unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',
pepper_not_found: '비밀번호 Pepper를 찾을 수 없어요. Core 환경설정을 확인해 주세요.', pepper_not_found: '비밀번호 Pepper를 찾을 수 없어요. Core 환경설정을 확인해 주세요.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'Metoda szyfrowania {{name}} nie jest obsługiwana.', unsupported_encryption_method: 'Metoda szyfrowania {{name}} nie jest obsługiwana.',
pepper_not_found: 'Nie znaleziono wartości pepper dla hasła. Sprawdź swoje zmienne środowiskowe.', pepper_not_found: 'Nie znaleziono wartości pepper dla hasła. Sprawdź swoje zmienne środowiskowe.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'O método de criptografia {{name}} não é suportado.', unsupported_encryption_method: 'O método de criptografia {{name}} não é suportado.',
pepper_not_found: 'Password pepper não encontrada. Por favor, verifique seus envs principais.', pepper_not_found: 'Password pepper não encontrada. Por favor, verifique seus envs principais.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.', unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',
pepper_not_found: 'pepper da Password não encontrada. Por favor, verifique os envs.', pepper_not_found: 'pepper da Password não encontrada. Por favor, verifique os envs.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: 'Метод шифрования {{name}} не поддерживается.', unsupported_encryption_method: 'Метод шифрования {{name}} не поддерживается.',
pepper_not_found: 'Не найден пепер пароля. Пожалуйста, проверьте ваши основные envs.', pepper_not_found: 'Не найден пепер пароля. Пожалуйста, проверьте ваши основные envs.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.', unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',
pepper_not_found: 'Şifre pepperı bulunamadı. Lütfen core envs.i kontrol edin.', pepper_not_found: 'Şifre pepperı bulunamadı. Lütfen core envs.i kontrol edin.',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: '不支持的加密方法 {{name}}', unsupported_encryption_method: '不支持的加密方法 {{name}}',
pepper_not_found: '密码 pepper 未找到。请检查 core 的环境变量。', pepper_not_found: '密码 pepper 未找到。请检查 core 的环境变量。',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: '不支持的加密方法 {{name}}', unsupported_encryption_method: '不支持的加密方法 {{name}}',
pepper_not_found: '密碼 pepper 未找到。請檢查 core 的環境變量。', pepper_not_found: '密碼 pepper 未找到。請檢查 core 的環境變量。',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -1,6 +1,7 @@
const password = { const password = {
unsupported_encryption_method: '不支持的加密方法 {{name}}', unsupported_encryption_method: '不支持的加密方法 {{name}}',
pepper_not_found: '密碼 pepper 未找到。請檢查 core 的環境變數。', pepper_not_found: '密碼 pepper 未找到。請檢查 core 的環境變數。',
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
}; };
export default Object.freeze(password); export default Object.freeze(password);

View file

@ -0,0 +1,21 @@
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
/** The alteration script for adding `password_policy` column to the sign-in experience table. */
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
alter table sign_in_experiences
add column password_policy jsonb not null default '{}';
`);
},
down: async (pool) => {
await pool.query(sql`
alter table sign_in_experiences
drop column password_policy;
`);
},
};
export default alteration;

View file

@ -41,7 +41,7 @@
}, },
"devDependencies": { "devDependencies": {
"@silverhand/eslint-config": "4.0.1", "@silverhand/eslint-config": "4.0.1",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"@silverhand/ts-config": "4.0.0", "@silverhand/ts-config": "4.0.0",
"@types/inquirer": "^9.0.0", "@types/inquirer": "^9.0.0",
"@types/jest": "^29.4.0", "@types/jest": "^29.4.0",

View file

@ -1,5 +1,6 @@
import { hexColorRegEx } from '@logto/core-kit'; import { type PasswordPolicy, hexColorRegEx, passwordPolicyGuard } from '@logto/core-kit';
import { languageTagGuard } from '@logto/language-kit'; import { languageTagGuard } from '@logto/language-kit';
import { type DeepPartial } from '@silverhand/essentials';
import type { Json } from '@withtyped/server'; import type { Json } from '@withtyped/server';
import { z } from 'zod'; import { z } from 'zod';
@ -204,6 +205,10 @@ export const logContextPayloadGuard = z
}) })
.catchall(z.unknown()); .catchall(z.unknown());
export type PartialPasswordPolicy = DeepPartial<PasswordPolicy>;
export const partialPasswordPolicyGuard = passwordPolicyGuard.deepPartial();
/** /**
* The basic log context type. It's more about a type hint instead of forcing the log shape. * The basic log context type. It's more about a type hint instead of forcing the log shape.
* *

View file

@ -49,6 +49,7 @@ export const createDefaultSignInExperience = (
signInMode: SignInMode.SignInAndRegister, signInMode: SignInMode.SignInAndRegister,
customCss: null, customCss: null,
customContent: {}, customContent: {},
passwordPolicy: {},
}); });
/** @deprecated Use `createDefaultSignInExperience()` instead. */ /** @deprecated Use `createDefaultSignInExperience()` instead. */

View file

@ -15,5 +15,6 @@ create table sign_in_experiences (
sign_in_mode sign_in_mode not null default 'SignInAndRegister', sign_in_mode sign_in_mode not null default 'SignInAndRegister',
custom_css text, custom_css text,
custom_content jsonb /* @use CustomContent */ not null default '{}'::jsonb, custom_content jsonb /* @use CustomContent */ not null default '{}'::jsonb,
password_policy jsonb /* @use PartialPasswordPolicy */ not null default '{}'::jsonb,
primary key (tenant_id, id) primary key (tenant_id, id)
); );

View file

@ -60,7 +60,7 @@
}, },
"prettier": "@silverhand/eslint-config/.prettierrc", "prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": { "dependencies": {
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"chalk": "^5.0.0", "chalk": "^5.0.0",
"find-up": "^6.3.0", "find-up": "^6.3.0",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",

View file

@ -37,7 +37,7 @@
}, },
"dependencies": { "dependencies": {
"@logto/language-kit": "workspace:^1.0.0", "@logto/language-kit": "workspace:^1.0.0",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"@withtyped/client": "^0.7.22" "@withtyped/client": "^0.7.22"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -53,7 +53,7 @@
"devDependencies": { "devDependencies": {
"@jest/types": "^29.0.3", "@jest/types": "^29.0.3",
"@silverhand/eslint-config": "4.0.1", "@silverhand/eslint-config": "4.0.1",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"@silverhand/ts-config": "4.0.0", "@silverhand/ts-config": "4.0.0",
"@silverhand/ts-config-react": "4.0.0", "@silverhand/ts-config-react": "4.0.0",
"@types/color": "^3.0.3", "@types/color": "^3.0.3",

View file

@ -25,7 +25,7 @@ describe('PasswordPolicyChecker', () => {
it('should reject malformed policy', () => { it('should reject malformed policy', () => {
expect(() => { expect(() => {
// @ts-expect-error // @ts-expect-error
return new PasswordPolicyChecker({ length: { min: 1, max: 2 } }); return new PasswordPolicyChecker({ length: { min: 1, max: '2' } });
}).toThrowError(ZodError); }).toThrowError(ZodError);
}); });
}); });
@ -39,35 +39,37 @@ describe('PasswordPolicyChecker -> check()', () => {
rejects: { rejects: {
pwned: true, pwned: true,
repetitionAndSequence: true, repetitionAndSequence: true,
words: [{ type: 'custom', value: 'test' }], personalInfo: true,
words: ['test', 'aaaa'],
}, },
}); });
it('should accept valid password', async () => { it('should accept valid password', async () => {
expect(await checker.check('aL1!aL1!')).toEqual([]); expect(await checker.check('aL1!aL1!', {})).toEqual([]);
}); });
it('should reject with all failed checks', async () => { it('should reject with all failed checks', async () => {
expect(await checker.check('aaa')).toEqual([ expect(await checker.check('aaa', {})).toEqual([
{ code: 'password_rejected.too_short' }, { code: 'password_rejected.too_short', interpolation: { min: 7 } },
{ code: 'password_rejected.character_types', interpolation: { min: 2 } }, { code: 'password_rejected.character_types', interpolation: { min: 2 } },
{ code: 'password_rejected.repetition' }, { code: 'password_rejected.repetition' },
]); ]);
expect(await checker.check('123456')).toEqual([ expect(await checker.check('123456', { phoneNumber: '12345' })).toEqual([
{ code: 'password_rejected.too_short' }, { code: 'password_rejected.too_short', interpolation: { min: 7 } },
{ code: 'password_rejected.character_types', interpolation: { min: 2 } }, { code: 'password_rejected.character_types', interpolation: { min: 2 } },
{ code: 'password_rejected.pwned' },
{ code: 'password_rejected.sequence' }, { code: 'password_rejected.sequence' },
{ code: 'password_rejected.pwned' },
{ code: 'password_rejected.personal_info' },
]); ]);
expect(await checker.check('aaaaaatest😀')).toEqual([ expect(await checker.check('aaaaaatest😀', {})).toEqual([
{ code: 'password_rejected.too_long' }, { code: 'password_rejected.too_long', interpolation: { max: 8 } },
{ code: 'password_rejected.unsupported_characters' }, { code: 'password_rejected.unsupported_characters' },
{ code: 'password_rejected.repetition' }, { code: 'password_rejected.repetition' },
{ {
code: 'password_rejected.restricted_words', code: 'password_rejected.restricted_words',
interpolation: { type: 'custom', value: 'test' }, interpolation: { words: 'test\naaaa', count: 2 },
}, },
]); ]);
}); });
@ -77,12 +79,12 @@ describe('PasswordPolicyChecker -> checkCharTypes()', () => {
const checker1 = new PasswordPolicyChecker({ const checker1 = new PasswordPolicyChecker({
length: { min: 1, max: 256 }, length: { min: 1, max: 256 },
characterTypes: { min: 2 }, characterTypes: { min: 2 },
rejects: { pwned: false, repetitionAndSequence: false, words: [] }, rejects: { pwned: false, repetitionAndSequence: false, personalInfo: false, words: [] },
}); });
const checker2 = new PasswordPolicyChecker({ const checker2 = new PasswordPolicyChecker({
length: { min: 1, max: 256 }, length: { min: 1, max: 256 },
characterTypes: { min: 4 }, characterTypes: { min: 4 },
rejects: { pwned: false, repetitionAndSequence: false, words: [] }, rejects: { pwned: false, repetitionAndSequence: false, personalInfo: false, words: [] },
}); });
it('should reject unsupported characters', () => { it('should reject unsupported characters', () => {
@ -106,7 +108,7 @@ describe('PasswordPolicyChecker -> hasBeenPwned()', () => {
const checker = new PasswordPolicyChecker({ const checker = new PasswordPolicyChecker({
length: { min: 1, max: 256 }, length: { min: 1, max: 256 },
characterTypes: { min: 2 }, characterTypes: { min: 2 },
rejects: { pwned: true, repetitionAndSequence: false, words: [] }, rejects: { pwned: true, repetitionAndSequence: false, personalInfo: false, words: [] },
}); });
mockPwnResponse(); mockPwnResponse();
@ -139,6 +141,36 @@ describe('PasswordPolicyChecker -> hasRepetition()', () => {
}); });
}); });
describe('PasswordPolicyChecker -> hasPersonalInfo()', () => {
const checker = new PasswordPolicyChecker({
rejects: { pwned: false, repetitionAndSequence: false, personalInfo: true, words: [] },
});
it('should reject password with name', () => {
expect(checker.hasPersonalInfo('test', { name: 'test' })).toBe(true);
expect(checker.hasPersonalInfo('test', { name: 'test2' })).toBe(false);
expect(checker.hasPersonalInfo('FOO', { name: 'Foo bar' })).toBe(true);
expect(checker.hasPersonalInfo('Foo', { name: 'bar fOo' })).toBe(true);
});
it('should reject password with username', () => {
expect(checker.hasPersonalInfo('123.456!test', { username: 'teST' })).toBe(true);
expect(checker.hasPersonalInfo('test', { username: 'test2' })).toBe(false);
});
it('should reject password with email', () => {
expect(checker.hasPersonalInfo('teST', { email: 'test@foo.com' })).toBe(true);
expect(checker.hasPersonalInfo('TEST', { email: 'test1@foo.com' })).toBe(false);
expect(checker.hasPersonalInfo('FOO', { email: 'test@foo.com' })).toBe(false);
expect(checker.hasPersonalInfo('Foo', { email: 'fOO@foo.com' })).toBe(true);
});
it('should reject password with phone number', () => {
expect(checker.hasPersonalInfo('teST1234567890.', { phoneNumber: '123456789' })).toBe(true);
expect(checker.hasPersonalInfo('TEST12345678', { phoneNumber: '123456789' })).toBe(false);
});
});
describe('PasswordPolicyChecker -> hasWords()', () => { describe('PasswordPolicyChecker -> hasWords()', () => {
const checker = new PasswordPolicyChecker({ const checker = new PasswordPolicyChecker({
length: { min: 1, max: 256 }, length: { min: 1, max: 256 },
@ -146,25 +178,15 @@ describe('PasswordPolicyChecker -> hasWords()', () => {
rejects: { rejects: {
pwned: false, pwned: false,
repetitionAndSequence: false, repetitionAndSequence: false,
words: [ personalInfo: false,
{ type: 'custom', value: 'test' }, words: ['test', 'teSt2', 'TesT3'],
{ type: 'custom', value: 'teSt2' },
{ type: 'personal', value: 'TesT3' },
],
}, },
}); });
it('should reject password with blacklisted words (case insensitive)', () => { it('should reject password with blacklisted words (case insensitive)', () => {
expect(checker.hasWords('test')).toEqual([{ type: 'custom', value: 'test' }]); expect(checker.hasWords('test')).toEqual(['test']);
expect(checker.hasWords('tEst2')).toEqual([ expect(checker.hasWords('tEst2')).toEqual(['test', 'test2']);
{ type: 'custom', value: 'test' }, expect(checker.hasWords('tEST TEst2 teSt3')).toEqual(['test', 'test2', 'test3']);
{ type: 'custom', value: 'test2' },
]);
expect(checker.hasWords('tEST TEst2 teSt3')).toEqual([
{ type: 'custom', value: 'test' },
{ type: 'custom', value: 'test2' },
{ type: 'personal', value: 'test3' },
]);
}); });
it('should accept password without blacklisted words', () => { it('should accept password without blacklisted words', () => {
@ -177,7 +199,7 @@ describe('PasswordPolicyChecker -> hasSequentialChars()', () => {
const checker = new PasswordPolicyChecker({ const checker = new PasswordPolicyChecker({
length: { min: 1, max: 256 }, length: { min: 1, max: 256 },
characterTypes: { min: 2 }, characterTypes: { min: 2 },
rejects: { pwned: false, repetitionAndSequence: true, words: [] }, rejects: { pwned: false, repetitionAndSequence: true, personalInfo: false, words: [] },
}); });
it('should reject password with too many sequential characters', () => { it('should reject password with too many sequential characters', () => {

View file

@ -1,13 +1,8 @@
import { type DeepPartial } from '@silverhand/essentials';
import { z } from 'zod'; import { z } from 'zod';
/** A word that used for password policy. */
type Word = {
type: 'custom' | 'personal';
value: string;
};
/** Password policy configuration type. */ /** Password policy configuration type. */
type PasswordPolicy = { export type PasswordPolicy = {
/** Policy about password length. */ /** Policy about password length. */
length: { length: {
/** Minimum password length. */ /** Minimum password length. */
@ -33,29 +28,38 @@ type PasswordPolicy = {
pwned: boolean; pwned: boolean;
/** Whether to reject passwords that like '123456' or 'aaaaaa'. */ /** Whether to reject passwords that like '123456' or 'aaaaaa'. */
repetitionAndSequence: boolean; repetitionAndSequence: boolean;
/** Whether to reject passwords that include personal information. */
personalInfo: boolean;
/** Whether to reject passwords that include specific words. */ /** Whether to reject passwords that include specific words. */
words: Word[]; words: string[];
}; };
}; };
/** Password policy configuration guard. */ /** Password policy configuration guard. */
const passwordPolicyGuard: z.ZodType<PasswordPolicy> = z.object({ export const passwordPolicyGuard = z.object({
length: z.object({ length: z
min: z.number().int().min(1), .object({
max: z.number().int().min(1), min: z.number().int().min(1).default(8),
}), max: z.number().int().min(1).default(256),
characterTypes: z.object({ })
min: z.number().int().min(1).max(4), .default({}),
}), characterTypes: z
rejects: z.object({ .object({
pwned: z.boolean(), min: z.number().int().min(1).max(4).optional().default(2),
repetitionAndSequence: z.boolean(), })
words: z.array(z.object({ type: z.enum(['custom', 'personal']), value: z.string() })), .default({}),
}), rejects: z
}); .object({
pwned: z.boolean().default(true),
repetitionAndSequence: z.boolean().default(true),
personalInfo: z.boolean().default(true),
words: z.string().array().default([]),
})
.default({}),
}) satisfies z.ZodType<PasswordPolicy, z.ZodTypeDef, DeepPartial<PasswordPolicy>>;
/** The code of why a password is rejected. */ /** The code of why a password is rejected. */
type PasswordRejectionCode = export type PasswordRejectionCode =
| 'too_short' | 'too_short'
| 'too_long' | 'too_long'
| 'character_types' | 'character_types'
@ -63,16 +67,25 @@ type PasswordRejectionCode =
| 'pwned' | 'pwned'
| 'repetition' | 'repetition'
| 'sequence' | 'sequence'
| 'personal_info'
| 'restricted_words'; | 'restricted_words';
/** A password issue that does not meet the policy. */ /** A password issue that does not meet the policy. */
type PasswordIssue = { export type PasswordIssue = {
/** Issue code. */ /** Issue code. */
code: `password_rejected.${PasswordRejectionCode}`; code: `password_rejected.${PasswordRejectionCode}`;
/** Interpolation data for the issue message. */ /** Interpolation data for the issue message. */
interpolation?: Record<string, unknown>; interpolation?: Record<string, unknown>;
}; };
/** Personal information to check. */
export type PersonalInfo = Partial<{
name: string;
username: string;
email: string;
phoneNumber: string;
}>;
/** /**
* The class for checking if a password meets the policy. The policy is defined as * The class for checking if a password meets the policy. The policy is defined as
* {@link PasswordPolicy}. * {@link PasswordPolicy}.
@ -98,28 +111,73 @@ type PasswordIssue = {
export class PasswordPolicyChecker { export class PasswordPolicyChecker {
static symbols = Object.freeze('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' as const); static symbols = Object.freeze('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' as const);
constructor(public readonly policy: PasswordPolicy) { public readonly policy: PasswordPolicy;
// Validate policy.
passwordPolicyGuard.parse(policy); constructor(
policy: DeepPartial<PasswordPolicy>,
/** The Web Crypto API to use. By default, the global `crypto.subtle` will be used. */
protected readonly subtle: SubtleCrypto = crypto.subtle
) {
this.policy = passwordPolicyGuard.parse(policy);
} }
/** /**
* Check if a password meets the policy. * Check if a password meets all the policy requirements.
* *
* @param password - Password to check. * @param password - Password to check.
* @param personalInfo - Personal information to check. Required if the policy
* requires to reject passwords that include personal information.
* @returns An array of issues. If the password meets the policy, an empty array will be returned. * @returns An array of issues. If the password meets the policy, an empty array will be returned.
* @throws TypeError - If the policy requires to reject passwords that include personal information
* but the personal information is not provided.
*/ */
/* eslint-disable @silverhand/fp/no-mutating-methods */ /* eslint-disable @silverhand/fp/no-mutating-methods */
async check(password: string): Promise<PasswordIssue[]> {
async check(password: string, personalInfo?: PersonalInfo): Promise<PasswordIssue[]> {
const issues: PasswordIssue[] = this.fastCheck(password);
if (this.policy.rejects.pwned && (await this.hasBeenPwned(password))) {
issues.push({
code: 'password_rejected.pwned',
});
}
if (this.policy.rejects.personalInfo) {
if (!personalInfo) {
throw new TypeError('Personal information is required to check personal information.');
}
if (this.hasPersonalInfo(password, personalInfo)) {
issues.push({
code: 'password_rejected.personal_info',
});
}
}
return issues;
}
/**
* Perform a fast check to see if the password passes the basic requirements.
* No pwned password and personal information check will be performed.
*
* This method is used for frontend validation.
*
* @param password - Password to check.
* @returns Whether the password passes the basic requirements.
*/
fastCheck(password: string) {
const issues: PasswordIssue[] = []; const issues: PasswordIssue[] = [];
if (password.length < this.policy.length.min) { if (password.length < this.policy.length.min) {
issues.push({ issues.push({
code: 'password_rejected.too_short', code: 'password_rejected.too_short',
interpolation: { min: this.policy.length.min },
}); });
} else if (password.length > this.policy.length.max) { } else if (password.length > this.policy.length.max) {
issues.push({ issues.push({
code: 'password_rejected.too_long', code: 'password_rejected.too_long',
interpolation: { max: this.policy.length.max },
}); });
} }
@ -135,12 +193,6 @@ export class PasswordPolicyChecker {
}); });
} }
if (this.policy.rejects.pwned && (await this.hasBeenPwned(password))) {
issues.push({
code: 'password_rejected.pwned',
});
}
if (this.policy.rejects.repetitionAndSequence) { if (this.policy.rejects.repetitionAndSequence) {
if (this.hasRepetition(password)) { if (this.hasRepetition(password)) {
issues.push({ issues.push({
@ -155,12 +207,14 @@ export class PasswordPolicyChecker {
} }
} }
issues.push( const words = this.hasWords(password);
...this.hasWords(password).map<PasswordIssue>(({ type, value }) => ({
if (words.length > 0) {
issues.push({
code: 'password_rejected.restricted_words', code: 'password_rejected.restricted_words',
interpolation: { type, value }, interpolation: { words: words.join('\n'), count: words.length },
})) });
); }
return issues; return issues;
} }
@ -199,13 +253,13 @@ export class PasswordPolicyChecker {
* @returns Whether the password has been pwned. * @returns Whether the password has been pwned.
*/ */
async hasBeenPwned(password: string): Promise<boolean> { async hasBeenPwned(password: string): Promise<boolean> {
const hash = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(password)); const hash = await this.subtle.digest('SHA-1', new TextEncoder().encode(password));
const hashHex = Array.from(new Uint8Array(hash)) const hashHex = Array.from(new Uint8Array(hash))
.map((binary) => binary.toString(16).padStart(2, '0')) .map((binary) => binary.toString(16).padStart(2, '0'))
.join(''); .join('');
const hashPrefix = hashHex.slice(0, 5); const hashPrefix = hashHex.slice(0, 5);
const hashSuffix = hashHex.slice(5); const hashSuffix = hashHex.slice(5);
const response = await fetch(`https://api.haveibeenpwned.com/range/${hashPrefix}`); const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`);
const text = await response.text(); const text = await response.text();
const hashes = text.split('\n'); const hashes = text.split('\n');
const found = hashes.some((hex) => hex.toLowerCase().startsWith(hashSuffix)); const found = hashes.some((hex) => hex.toLowerCase().startsWith(hashSuffix));
@ -228,20 +282,53 @@ export class PasswordPolicyChecker {
return Boolean(matchedRepetition); return Boolean(matchedRepetition);
} }
/**
* Check if the given password contains personal information.
*
* @param password - Password to check.
* @param personalInfo - Personal information to check.
* @returns Whether the password contains personal information.
*/
hasPersonalInfo(password: string, personalInfo: PersonalInfo): boolean {
const lowercasedPassword = password.toLowerCase();
const { name, username, email, phoneNumber } = personalInfo;
if (
name
?.toLowerCase()
.split(' ')
.some((word) => lowercasedPassword.includes(word))
) {
return true;
}
if (username && lowercasedPassword.includes(username.toLowerCase())) {
return true;
}
const emailPrefix = email?.split('@')[0];
if (emailPrefix && lowercasedPassword.includes(emailPrefix.toLowerCase())) {
return true;
}
if (phoneNumber && lowercasedPassword.includes(phoneNumber)) {
return true;
}
return false;
}
/** /**
* Check if the given password contains specific words. * Check if the given password contains specific words.
* *
* @param password - Password to check. * @param password - Password to check.
* @returns An array of matched words. * @returns An array of matched words.
*/ */
hasWords(password: string): Word[] { hasWords(password: string): string[] {
const words = this.policy.rejects.words.map(({ value, ...rest }) => ({ const words = this.policy.rejects.words.map((word) => word.toLowerCase());
...rest,
value: value.toLowerCase(),
}));
const lowercasedPassword = password.toLowerCase(); const lowercasedPassword = password.toLowerCase();
return words.filter(({ value }) => lowercasedPassword.includes(value)); return words.filter((word) => lowercasedPassword.includes(word));
} }
/** /**

View file

@ -37,7 +37,7 @@
"@react-spring/web": "^9.6.1", "@react-spring/web": "^9.6.1",
"@silverhand/eslint-config": "4.0.1", "@silverhand/eslint-config": "4.0.1",
"@silverhand/eslint-config-react": "4.0.1", "@silverhand/eslint-config-react": "4.0.1",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.8.4",
"@silverhand/ts-config": "4.0.0", "@silverhand/ts-config": "4.0.0",
"@silverhand/ts-config-react": "4.0.0", "@silverhand/ts-config-react": "4.0.0",
"@swc/core": "^1.3.52", "@swc/core": "^1.3.52",

View file

@ -204,6 +204,7 @@ export const mockSignInExperience: SignInExperience = {
signInMode: SignInMode.SignInAndRegister, signInMode: SignInMode.SignInAndRegister,
customCss: null, customCss: null,
customContent: {}, customContent: {},
passwordPolicy: {},
}; };
export const mockSignInExperienceSettings: SignInExperienceResponse = { export const mockSignInExperienceSettings: SignInExperienceResponse = {
@ -228,6 +229,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = {
}, },
customCss: null, customCss: null,
customContent: {}, customContent: {},
passwordPolicy: {},
}; };
const usernameSettings = { const usernameSettings = {

View file

@ -49,8 +49,8 @@ importers:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.2(tslib@2.4.1)(typescript@5.0.2) version: 3.0.2(tslib@2.4.1)(typescript@5.0.2)
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
applicationinsights: applicationinsights:
specifier: ^2.7.0 specifier: ^2.7.0
version: 2.7.0 version: 2.7.0
@ -125,8 +125,8 @@ importers:
specifier: workspace:^2.0.0 specifier: workspace:^2.0.0
version: link:../shared version: link:../shared
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
chalk: chalk:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.1.2 version: 5.1.2
@ -240,8 +240,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
dayjs: dayjs:
specifier: ^1.10.5 specifier: ^1.10.5
version: 1.11.6 version: 1.11.6
@ -328,8 +328,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
dayjs: dayjs:
specifier: ^1.10.5 specifier: ^1.10.5
version: 1.11.6 version: 1.11.6
@ -416,8 +416,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -495,8 +495,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -577,8 +577,8 @@ importers:
specifier: workspace:^2.0.0 specifier: workspace:^2.0.0
version: link:../../shared version: link:../../shared
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -665,8 +665,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -747,8 +747,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -826,8 +826,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -905,8 +905,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -984,8 +984,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1063,8 +1063,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1145,8 +1145,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1224,8 +1224,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1303,8 +1303,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1382,8 +1382,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1461,8 +1461,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1540,8 +1540,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.7.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1619,8 +1619,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1698,8 +1698,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1777,8 +1777,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1856,8 +1856,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -1935,8 +1935,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2014,8 +2014,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2099,8 +2099,8 @@ importers:
specifier: workspace:^2.0.0 specifier: workspace:^2.0.0
version: link:../../shared version: link:../../shared
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2184,8 +2184,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
fast-xml-parser: fast-xml-parser:
specifier: ^4.2.5 specifier: ^4.2.5
version: 4.2.5 version: 4.2.5
@ -2269,8 +2269,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2348,8 +2348,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2427,8 +2427,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2512,8 +2512,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2591,8 +2591,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2670,8 +2670,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2749,8 +2749,8 @@ importers:
specifier: workspace:^1.1.0 specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit version: link:../../toolkit/connector-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -2888,8 +2888,8 @@ importers:
specifier: 4.0.1 specifier: 4.0.1
version: 4.0.1(eslint@8.44.0)(postcss@8.4.6)(prettier@3.0.0)(stylelint@15.0.0)(typescript@5.0.2) version: 4.0.1(eslint@8.44.0)(postcss@8.4.6)(prettier@3.0.0)(stylelint@15.0.0)(typescript@5.0.2)
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
'@silverhand/ts-config': '@silverhand/ts-config':
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0(typescript@5.0.2) version: 4.0.0(typescript@5.0.2)
@ -3164,8 +3164,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../ui version: link:../ui
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
'@withtyped/client': '@withtyped/client':
specifier: ^0.7.22 specifier: ^0.7.22
version: 0.7.22(zod@3.20.2) version: 0.7.22(zod@3.20.2)
@ -3495,8 +3495,8 @@ importers:
specifier: 4.0.1 specifier: 4.0.1
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2) version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
'@silverhand/ts-config': '@silverhand/ts-config':
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0(typescript@5.0.2) version: 4.0.0(typescript@5.0.2)
@ -3555,8 +3555,8 @@ importers:
specifier: workspace:^1.0.0 specifier: workspace:^1.0.0
version: link:../toolkit/language-kit version: link:../toolkit/language-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
zod: zod:
specifier: ^3.20.2 specifier: ^3.20.2
version: 3.20.2 version: 3.20.2
@ -3582,12 +3582,15 @@ importers:
packages/phrases-ui: packages/phrases-ui:
dependencies: dependencies:
'@logto/core-kit':
specifier: workspace:^2.0.1
version: link:../toolkit/core-kit
'@logto/language-kit': '@logto/language-kit':
specifier: workspace:^1.0.0 specifier: workspace:^1.0.0
version: link:../toolkit/language-kit version: link:../toolkit/language-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
zod: zod:
specifier: ^3.20.2 specifier: ^3.20.2
version: 3.20.2 version: 3.20.2
@ -3645,8 +3648,8 @@ importers:
specifier: 4.0.1 specifier: 4.0.1
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2) version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
'@silverhand/ts-config': '@silverhand/ts-config':
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0(typescript@5.0.2) version: 4.0.0(typescript@5.0.2)
@ -3699,8 +3702,8 @@ importers:
packages/shared: packages/shared:
dependencies: dependencies:
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
chalk: chalk:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.1.2 version: 5.1.2
@ -3751,8 +3754,8 @@ importers:
specifier: workspace:^1.0.0 specifier: workspace:^1.0.0
version: link:../language-kit version: link:../language-kit
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
'@withtyped/client': '@withtyped/client':
specifier: ^0.7.22 specifier: ^0.7.22
version: 0.7.22(zod@3.20.2) version: 0.7.22(zod@3.20.2)
@ -3821,8 +3824,8 @@ importers:
specifier: 4.0.1 specifier: 4.0.1
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2) version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
'@silverhand/ts-config': '@silverhand/ts-config':
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0(typescript@5.0.2) version: 4.0.0(typescript@5.0.2)
@ -3960,8 +3963,8 @@ importers:
specifier: 4.0.1 specifier: 4.0.1
version: 4.0.1(eslint@8.44.0)(postcss@8.4.6)(prettier@3.0.0)(stylelint@15.0.0)(typescript@5.0.2) version: 4.0.1(eslint@8.44.0)(postcss@8.4.6)(prettier@3.0.0)(stylelint@15.0.0)(typescript@5.0.2)
'@silverhand/essentials': '@silverhand/essentials':
specifier: ^2.5.0 specifier: ^2.8.4
version: 2.5.0 version: 2.8.4
'@silverhand/ts-config': '@silverhand/ts-config':
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0(typescript@5.0.2) version: 4.0.0(typescript@5.0.2)
@ -7297,7 +7300,7 @@ packages:
resolution: {integrity: sha512-yDWSZMI2Qo/xoYU92tnwSP/gnSvq8+CLK5DqD/4brO42QJa7xjt7eA+HSyuMmSUrKffY2nP3riU81gs+nR8DkA==} resolution: {integrity: sha512-yDWSZMI2Qo/xoYU92tnwSP/gnSvq8+CLK5DqD/4brO42QJa7xjt7eA+HSyuMmSUrKffY2nP3riU81gs+nR8DkA==}
engines: {node: ^18.12.0} engines: {node: ^18.12.0}
dependencies: dependencies:
'@silverhand/essentials': 2.8.2 '@silverhand/essentials': 2.8.4
tiny-cookie: 2.4.1 tiny-cookie: 2.4.1
dev: false dev: false
@ -7305,7 +7308,7 @@ packages:
resolution: {integrity: sha512-4XsXlCC0uZHcfazV09/4YKo4koqvSzQlkPUAToTp/WHpb6h2XDOJh5/hi55LXL4zp0PCcgpErKRxFCtgXCc6WQ==} resolution: {integrity: sha512-4XsXlCC0uZHcfazV09/4YKo4koqvSzQlkPUAToTp/WHpb6h2XDOJh5/hi55LXL4zp0PCcgpErKRxFCtgXCc6WQ==}
dependencies: dependencies:
'@logto/client': 2.2.0 '@logto/client': 2.2.0
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.8.4
js-base64: 3.7.5 js-base64: 3.7.5
dev: true dev: true
@ -7313,7 +7316,7 @@ packages:
resolution: {integrity: sha512-vw8xDW8k38/58Q1r592z/9JdsmUh4+LMmoVm/Nu7LbWKlT32eD3H9hZDkFK9XEHpriifhI0hP7asGWEmhrEUuQ==} resolution: {integrity: sha512-vw8xDW8k38/58Q1r592z/9JdsmUh4+LMmoVm/Nu7LbWKlT32eD3H9hZDkFK9XEHpriifhI0hP7asGWEmhrEUuQ==}
dependencies: dependencies:
'@logto/js': 2.1.1 '@logto/js': 2.1.1
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.8.4
camelcase-keys: 7.0.2 camelcase-keys: 7.0.2
jose: 4.14.4 jose: 4.14.4
dev: true dev: true
@ -7322,7 +7325,7 @@ packages:
resolution: {integrity: sha512-7I2ELo5UWIJsFCYK/gX465l0+QhXTdyYWkgb2CcdPu5KbaPBNpASedm+fEV2NREYe2svbNODFhog6UMA/xGQnQ==} resolution: {integrity: sha512-7I2ELo5UWIJsFCYK/gX465l0+QhXTdyYWkgb2CcdPu5KbaPBNpASedm+fEV2NREYe2svbNODFhog6UMA/xGQnQ==}
dependencies: dependencies:
'@logto/js': 2.1.1 '@logto/js': 2.1.1
'@silverhand/essentials': 2.8.2 '@silverhand/essentials': 2.8.4
camelcase-keys: 7.0.2 camelcase-keys: 7.0.2
jose: 4.14.4 jose: 4.14.4
dev: true dev: true
@ -7331,7 +7334,7 @@ packages:
resolution: {integrity: sha512-zxy9zr5swOxbzYJNYtKXofj2tSIS9565d+1pT6RSbmx3Hn+JG6uzsb75PZXW9vlmmm7p1sGZeTQ+xVzKNFPsMg==} resolution: {integrity: sha512-zxy9zr5swOxbzYJNYtKXofj2tSIS9565d+1pT6RSbmx3Hn+JG6uzsb75PZXW9vlmmm7p1sGZeTQ+xVzKNFPsMg==}
engines: {node: ^18.12.0} engines: {node: ^18.12.0}
dependencies: dependencies:
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.8.4
'@withtyped/server': 0.12.8(zod@3.20.2) '@withtyped/server': 0.12.8(zod@3.20.2)
transitivePeerDependencies: transitivePeerDependencies:
- zod - zod
@ -7341,7 +7344,7 @@ packages:
resolution: {integrity: sha512-dIrEUW7gi477HQpNsq/HT1gdvPK2ZmVuV73u2rH9LXGEIFIVGqmmIaaK3IcOPG110jKCBhTzF0+hKsW9Y3Pjmw==} resolution: {integrity: sha512-dIrEUW7gi477HQpNsq/HT1gdvPK2ZmVuV73u2rH9LXGEIFIVGqmmIaaK3IcOPG110jKCBhTzF0+hKsW9Y3Pjmw==}
engines: {node: ^18.12.0} engines: {node: ^18.12.0}
dependencies: dependencies:
'@silverhand/essentials': 2.8.2 '@silverhand/essentials': 2.8.4
'@withtyped/server': 0.12.9(zod@3.20.2) '@withtyped/server': 0.12.9(zod@3.20.2)
transitivePeerDependencies: transitivePeerDependencies:
- zod - zod
@ -7350,7 +7353,7 @@ packages:
/@logto/js@2.1.1: /@logto/js@2.1.1:
resolution: {integrity: sha512-PHikheavVK+l4ivgtzi14p184hEPgXjqQEAom1Gme1MZoopx+WlwxvHSEQBsmyvVqRtI0oiojhoU5tgYi1FKJw==} resolution: {integrity: sha512-PHikheavVK+l4ivgtzi14p184hEPgXjqQEAom1Gme1MZoopx+WlwxvHSEQBsmyvVqRtI0oiojhoU5tgYi1FKJw==}
dependencies: dependencies:
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.8.4
camelcase-keys: 7.0.2 camelcase-keys: 7.0.2
jose: 4.14.2 jose: 4.14.2
dev: true dev: true
@ -7359,7 +7362,7 @@ packages:
resolution: {integrity: sha512-joSzzAqaRKeEquRenoFrIXXkNxkJci5zSkk4afywz1P8tTcTysnV4eXaBmwXNpmDfQdtHBwRdSACZPLgeF8JiQ==} resolution: {integrity: sha512-joSzzAqaRKeEquRenoFrIXXkNxkJci5zSkk4afywz1P8tTcTysnV4eXaBmwXNpmDfQdtHBwRdSACZPLgeF8JiQ==}
dependencies: dependencies:
'@logto/client': 2.1.0 '@logto/client': 2.1.0
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.8.4
js-base64: 3.7.5 js-base64: 3.7.5
node-fetch: 2.6.7 node-fetch: 2.6.7
transitivePeerDependencies: transitivePeerDependencies:
@ -7372,7 +7375,7 @@ packages:
react: '>=16.8.0 || ^18.0.0' react: '>=16.8.0 || ^18.0.0'
dependencies: dependencies:
'@logto/browser': 2.1.0 '@logto/browser': 2.1.0
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.8.4
react: 18.2.0 react: 18.2.0
dev: true dev: true
@ -8955,16 +8958,8 @@ packages:
lodash: 4.17.21 lodash: 4.17.21
dev: true dev: true
/@silverhand/essentials@2.5.0: /@silverhand/essentials@2.8.4:
resolution: {integrity: sha512-8GgVFAmbo6S0EgsjYXH4aH8a69O7SzEtPFPDpVZmJuGEt8e3ODVx0F2V4rXyC3/SzFbcb2md2gRbA+Z6aTad6g==} resolution: {integrity: sha512-VaI00QyD2trA7n7/wHNcGNGRXoSr8dUGs/hQCu4Rju4Edl3vso7CeCXdfGU2aNDuT2uMs75of6Ph8gqVJhWlYQ==}
engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^7}
/@silverhand/essentials@2.7.0:
resolution: {integrity: sha512-F5Qo5ZNnERUURK/9F1ZIi4FBDM22aeD59Zv0VtkgIhUL9tYK9svA2Jz88NNdYBwqCPrh8ExZlpFNi+pNmXKNlQ==}
engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0}
/@silverhand/essentials@2.8.2:
resolution: {integrity: sha512-mrXzAQ6ZGyLoKQpfEOr/LmbQL7FIurVsBaymnsQyKNV56bFYCv5M+2irsAKROtmLMgSunAU/10XiDIaEYW9tbA==}
engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0} engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0}
/@silverhand/ts-config-react@4.0.0(typescript@5.0.2): /@silverhand/ts-config-react@4.0.0(typescript@5.0.2):
@ -10076,7 +10071,7 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.19.1 zod: ^3.19.1
dependencies: dependencies:
'@silverhand/essentials': 2.8.2 '@silverhand/essentials': 2.8.4
'@withtyped/shared': 0.2.2 '@withtyped/shared': 0.2.2
zod: 3.20.2 zod: 3.20.2
dev: true dev: true
@ -10086,7 +10081,7 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.19.1 zod: ^3.19.1
dependencies: dependencies:
'@silverhand/essentials': 2.8.2 '@silverhand/essentials': 2.8.4
'@withtyped/shared': 0.2.2 '@withtyped/shared': 0.2.2
zod: 3.20.2 zod: 3.20.2