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:
parent
ed7d842517
commit
b8a7b900e1
44 changed files with 429 additions and 204 deletions
|
@ -60,7 +60,7 @@
|
|||
"@microsoft/applicationinsights-clickanalytics-js": "^3.0.2",
|
||||
"@microsoft/applicationinsights-react-js": "^17.0.0",
|
||||
"@microsoft/applicationinsights-web": "^3.0.2",
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"applicationinsights": "^2.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
"@logto/phrases-ui": "workspace:^1.2.0",
|
||||
"@logto/schemas": "workspace:1.8.0",
|
||||
"@logto/shared": "workspace:^2.0.0",
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"chalk": "^5.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"prepublishOnly": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"got": "^13.0.0",
|
||||
"snakecase-keys": "^5.4.4",
|
||||
"zod": "^3.20.2"
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"@parcel/transformer-svg-react": "2.9.3",
|
||||
"@silverhand/eslint-config": "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-react": "4.0.0",
|
||||
"@swc/core": "^1.3.52",
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"@logto/schemas": "workspace:^1.8.0",
|
||||
"@logto/shared": "workspace:^2.0.0",
|
||||
"@logto/ui": "workspace:*",
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"@withtyped/client": "^0.7.22",
|
||||
"chalk": "^5.0.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
|
|
@ -91,4 +91,5 @@ export const mockSignInExperience: SignInExperience = {
|
|||
signInMode: SignInMode.SignInAndRegister,
|
||||
customCss: null,
|
||||
customContent: {},
|
||||
passwordPolicy: {},
|
||||
};
|
||||
|
|
|
@ -32,12 +32,13 @@ describe('sign-in-experience query', () => {
|
|||
signUp: JSON.stringify(mockSignInExperience.signUp),
|
||||
socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets),
|
||||
customContent: JSON.stringify(mockSignInExperience.customContent),
|
||||
passwordPolicy: JSON.stringify(mockSignInExperience.passwordPolicy),
|
||||
};
|
||||
|
||||
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", "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"
|
||||
where "id"=$1
|
||||
`;
|
||||
|
|
|
@ -76,6 +76,10 @@ const { sendVerificationCodeToIdentifier } = await mockEsmWithActual(
|
|||
})
|
||||
);
|
||||
|
||||
const { validatePassword } = await mockEsmWithActual('./utils/validate-password.js', () => ({
|
||||
validatePassword: jest.fn(),
|
||||
}));
|
||||
|
||||
const { createLog, prependAllLogEntries } = createMockLogContext();
|
||||
|
||||
await mockEsmWithActual(
|
||||
|
@ -151,6 +155,7 @@ describe('interaction routes', () => {
|
|||
expect(verifyIdentifierSettings).toBeCalled();
|
||||
expect(verifyProfileSettings).toBeCalled();
|
||||
expect(verifyIdentifierPayload).toBeCalled();
|
||||
expect(validatePassword).toBeCalled();
|
||||
expect(storeInteractionResult).toBeCalled();
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
verifyProfileSettings,
|
||||
} from './utils/sign-in-experience-validation.js';
|
||||
import { createSocialAuthorizationUrl } from './utils/social-verification.js';
|
||||
import { validatePassword } from './utils/validate-password.js';
|
||||
import { sendVerificationCodeToIdentifier } from './utils/verification-code-validation.js';
|
||||
import {
|
||||
verifyIdentifierPayload,
|
||||
|
@ -71,6 +72,10 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
status: [204, 400, 401, 403, 422],
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
async ({ guard: { body }, passwordPolicyChecker }, next) => {
|
||||
await validatePassword(body.profile?.password, passwordPolicyChecker);
|
||||
return next();
|
||||
},
|
||||
async (ctx, next) => {
|
||||
const { event, identifier, profile } = ctx.guard.body;
|
||||
const { signInExperience, createLog } = ctx;
|
||||
|
@ -205,6 +210,10 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
status: [204, 400, 404],
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
async ({ guard: { body }, passwordPolicyChecker }, next) => {
|
||||
await validatePassword(body.password, passwordPolicyChecker);
|
||||
return next();
|
||||
},
|
||||
async (ctx, next) => {
|
||||
const profilePayload = ctx.guard.body;
|
||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||
|
@ -243,6 +252,10 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
status: [204, 400, 404],
|
||||
}),
|
||||
koaInteractionSie(queries),
|
||||
async ({ guard: { body }, passwordPolicyChecker }, next) => {
|
||||
await validatePassword(body.password, passwordPolicyChecker);
|
||||
return next();
|
||||
},
|
||||
async (ctx, next) => {
|
||||
const profilePayload = ctx.guard.body;
|
||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||
|
@ -373,6 +386,8 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
// Check interaction exists
|
||||
const { event } = getInteractionStorage(interactionDetails.result);
|
||||
|
||||
// This file needs refactor
|
||||
// eslint-disable-next-line max-lines
|
||||
await sendVerificationCodeToIdentifier(
|
||||
{ event, ...guard.body },
|
||||
interactionDetails.jti,
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
import { PasswordPolicyChecker } from '@logto/core-kit';
|
||||
import type { SignInExperience } from '@logto/schemas';
|
||||
import type { MiddlewareType } from 'koa';
|
||||
import { type IRouterParamContext } from 'koa-router';
|
||||
|
||||
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> =
|
||||
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>({
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
}: Queries): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> {
|
||||
|
@ -14,6 +25,10 @@ export default function koaInteractionSie<StateT, ContextT extends IRouterParamC
|
|||
const signInExperience = await findDefaultSignInExperience();
|
||||
|
||||
ctx.signInExperience = signInExperience;
|
||||
ctx.passwordPolicyChecker = new PasswordPolicyChecker(
|
||||
signInExperience.passwordPolicy,
|
||||
crypto.subtle
|
||||
);
|
||||
|
||||
return next();
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -1,3 +1,6 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
import { PasswordPolicyChecker } from '@logto/core-kit';
|
||||
import { InteractionEvent, MissingProfile, SignInIdentifier } from '@logto/schemas';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
import type Provider from 'oidc-provider';
|
||||
|
@ -32,6 +35,10 @@ describe('validateMandatoryUserProfile', () => {
|
|||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
interactionDetails: {} as Awaited<ReturnType<Provider['interactionDetails']>>,
|
||||
signInExperience: mockSignInExperience,
|
||||
passwordPolicyChecker: new PasswordPolicyChecker(
|
||||
mockSignInExperience.passwordPolicy,
|
||||
crypto.subtle
|
||||
),
|
||||
};
|
||||
|
||||
const interaction: IdentifierVerifiedInteractionResult = {
|
||||
|
@ -274,7 +281,7 @@ describe('validateMandatoryUserProfile', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('email or Phone required', () => {
|
||||
describe('email or phone required', () => {
|
||||
const ctx = {
|
||||
...baseCtx,
|
||||
signInExperience: {
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
ZodString,
|
||||
ZodUnion,
|
||||
ZodUnknown,
|
||||
ZodDefault,
|
||||
} from 'zod';
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"@logto/schemas": "workspace:^1.6.0",
|
||||
"@logto/shared": "workspace:^2.0.0",
|
||||
"@silverhand/eslint-config": "4.0.1",
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"@silverhand/ts-config": "4.0.0",
|
||||
"@types/expect-puppeteer": "^5.0.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -33,8 +33,9 @@
|
|||
"url": "https://github.com/logto-io/logto/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/core-kit": "workspace:^2.0.1",
|
||||
"@logto/language-kit": "workspace:^1.0.0",
|
||||
"@silverhand/essentials": "^2.5.0"
|
||||
"@silverhand/essentials": "^2.8.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.20.2"
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:^1.0.0",
|
||||
"@silverhand/essentials": "^2.5.0"
|
||||
"@silverhand/essentials": "^2.8.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.20.2"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: 'Die Verschlüsselungsmethode {{name}} wird nicht unterstützt.',
|
||||
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);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
||||
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);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
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.',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -2,6 +2,7 @@ const password = {
|
|||
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",
|
||||
pepper_not_found:
|
||||
'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);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
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.',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: '暗号化方式 {{name}} はサポートされていません。',
|
||||
pepper_not_found: 'パスワードペッパーが見つかりません。コアの環境を確認してください。',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',
|
||||
pepper_not_found: '비밀번호 Pepper를 찾을 수 없어요. Core 환경설정을 확인해 주세요.',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
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.',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
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.',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
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.',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: 'Метод шифрования {{name}} не поддерживается.',
|
||||
pepper_not_found: 'Не найден пепер пароля. Пожалуйста, проверьте ваши основные envs.',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',
|
||||
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);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
||||
pepper_not_found: '密码 pepper 未找到。请检查 core 的环境变量。',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
||||
pepper_not_found: '密碼 pepper 未找到。請檢查 core 的環境變量。',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const password = {
|
||||
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
||||
pepper_not_found: '密碼 pepper 未找到。請檢查 core 的環境變數。',
|
||||
password_rejected: 'Password rejected. Please check if your password meets the requirements.', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(password);
|
||||
|
|
|
@ -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;
|
|
@ -41,7 +41,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@silverhand/eslint-config": "4.0.1",
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"@silverhand/ts-config": "4.0.0",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -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 { type DeepPartial } from '@silverhand/essentials';
|
||||
import type { Json } from '@withtyped/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -204,6 +205,10 @@ export const logContextPayloadGuard = z
|
|||
})
|
||||
.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.
|
||||
*
|
||||
|
|
|
@ -49,6 +49,7 @@ export const createDefaultSignInExperience = (
|
|||
signInMode: SignInMode.SignInAndRegister,
|
||||
customCss: null,
|
||||
customContent: {},
|
||||
passwordPolicy: {},
|
||||
});
|
||||
|
||||
/** @deprecated Use `createDefaultSignInExperience()` instead. */
|
||||
|
|
|
@ -15,5 +15,6 @@ 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,
|
||||
password_policy jsonb /* @use PartialPasswordPolicy */ not null default '{}'::jsonb,
|
||||
primary key (tenant_id, id)
|
||||
);
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"dependencies": {
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"chalk": "^5.0.0",
|
||||
"find-up": "^6.3.0",
|
||||
"nanoid": "^4.0.0",
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:^1.0.0",
|
||||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"@withtyped/client": "^0.7.22"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"devDependencies": {
|
||||
"@jest/types": "^29.0.3",
|
||||
"@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-react": "4.0.0",
|
||||
"@types/color": "^3.0.3",
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('PasswordPolicyChecker', () => {
|
|||
it('should reject malformed policy', () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error
|
||||
return new PasswordPolicyChecker({ length: { min: 1, max: 2 } });
|
||||
return new PasswordPolicyChecker({ length: { min: 1, max: '2' } });
|
||||
}).toThrowError(ZodError);
|
||||
});
|
||||
});
|
||||
|
@ -39,35 +39,37 @@ describe('PasswordPolicyChecker -> check()', () => {
|
|||
rejects: {
|
||||
pwned: true,
|
||||
repetitionAndSequence: true,
|
||||
words: [{ type: 'custom', value: 'test' }],
|
||||
personalInfo: true,
|
||||
words: ['test', 'aaaa'],
|
||||
},
|
||||
});
|
||||
|
||||
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 () => {
|
||||
expect(await checker.check('aaa')).toEqual([
|
||||
{ code: 'password_rejected.too_short' },
|
||||
expect(await checker.check('aaa', {})).toEqual([
|
||||
{ code: 'password_rejected.too_short', interpolation: { min: 7 } },
|
||||
{ code: 'password_rejected.character_types', interpolation: { min: 2 } },
|
||||
{ code: 'password_rejected.repetition' },
|
||||
]);
|
||||
|
||||
expect(await checker.check('123456')).toEqual([
|
||||
{ code: 'password_rejected.too_short' },
|
||||
expect(await checker.check('123456', { phoneNumber: '12345' })).toEqual([
|
||||
{ code: 'password_rejected.too_short', interpolation: { min: 7 } },
|
||||
{ code: 'password_rejected.character_types', interpolation: { min: 2 } },
|
||||
{ code: 'password_rejected.pwned' },
|
||||
{ code: 'password_rejected.sequence' },
|
||||
{ code: 'password_rejected.pwned' },
|
||||
{ code: 'password_rejected.personal_info' },
|
||||
]);
|
||||
|
||||
expect(await checker.check('aaaaaatest😀')).toEqual([
|
||||
{ code: 'password_rejected.too_long' },
|
||||
expect(await checker.check('aaaaaatest😀', {})).toEqual([
|
||||
{ code: 'password_rejected.too_long', interpolation: { max: 8 } },
|
||||
{ code: 'password_rejected.unsupported_characters' },
|
||||
{ code: 'password_rejected.repetition' },
|
||||
{
|
||||
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({
|
||||
length: { min: 1, max: 256 },
|
||||
characterTypes: { min: 2 },
|
||||
rejects: { pwned: false, repetitionAndSequence: false, words: [] },
|
||||
rejects: { pwned: false, repetitionAndSequence: false, personalInfo: false, words: [] },
|
||||
});
|
||||
const checker2 = new PasswordPolicyChecker({
|
||||
length: { min: 1, max: 256 },
|
||||
characterTypes: { min: 4 },
|
||||
rejects: { pwned: false, repetitionAndSequence: false, words: [] },
|
||||
rejects: { pwned: false, repetitionAndSequence: false, personalInfo: false, words: [] },
|
||||
});
|
||||
|
||||
it('should reject unsupported characters', () => {
|
||||
|
@ -106,7 +108,7 @@ describe('PasswordPolicyChecker -> hasBeenPwned()', () => {
|
|||
const checker = new PasswordPolicyChecker({
|
||||
length: { min: 1, max: 256 },
|
||||
characterTypes: { min: 2 },
|
||||
rejects: { pwned: true, repetitionAndSequence: false, words: [] },
|
||||
rejects: { pwned: true, repetitionAndSequence: false, personalInfo: false, words: [] },
|
||||
});
|
||||
|
||||
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()', () => {
|
||||
const checker = new PasswordPolicyChecker({
|
||||
length: { min: 1, max: 256 },
|
||||
|
@ -146,25 +178,15 @@ describe('PasswordPolicyChecker -> hasWords()', () => {
|
|||
rejects: {
|
||||
pwned: false,
|
||||
repetitionAndSequence: false,
|
||||
words: [
|
||||
{ type: 'custom', value: 'test' },
|
||||
{ type: 'custom', value: 'teSt2' },
|
||||
{ type: 'personal', value: 'TesT3' },
|
||||
],
|
||||
personalInfo: false,
|
||||
words: ['test', 'teSt2', 'TesT3'],
|
||||
},
|
||||
});
|
||||
|
||||
it('should reject password with blacklisted words (case insensitive)', () => {
|
||||
expect(checker.hasWords('test')).toEqual([{ type: 'custom', value: 'test' }]);
|
||||
expect(checker.hasWords('tEst2')).toEqual([
|
||||
{ type: 'custom', value: 'test' },
|
||||
{ type: 'custom', value: 'test2' },
|
||||
]);
|
||||
expect(checker.hasWords('tEST TEst2 teSt3')).toEqual([
|
||||
{ type: 'custom', value: 'test' },
|
||||
{ type: 'custom', value: 'test2' },
|
||||
{ type: 'personal', value: 'test3' },
|
||||
]);
|
||||
expect(checker.hasWords('test')).toEqual(['test']);
|
||||
expect(checker.hasWords('tEst2')).toEqual(['test', 'test2']);
|
||||
expect(checker.hasWords('tEST TEst2 teSt3')).toEqual(['test', 'test2', 'test3']);
|
||||
});
|
||||
|
||||
it('should accept password without blacklisted words', () => {
|
||||
|
@ -177,7 +199,7 @@ describe('PasswordPolicyChecker -> hasSequentialChars()', () => {
|
|||
const checker = new PasswordPolicyChecker({
|
||||
length: { min: 1, max: 256 },
|
||||
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', () => {
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import { type DeepPartial } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
/** A word that used for password policy. */
|
||||
type Word = {
|
||||
type: 'custom' | 'personal';
|
||||
value: string;
|
||||
};
|
||||
|
||||
/** Password policy configuration type. */
|
||||
type PasswordPolicy = {
|
||||
export type PasswordPolicy = {
|
||||
/** Policy about password length. */
|
||||
length: {
|
||||
/** Minimum password length. */
|
||||
|
@ -33,29 +28,38 @@ type PasswordPolicy = {
|
|||
pwned: boolean;
|
||||
/** Whether to reject passwords that like '123456' or 'aaaaaa'. */
|
||||
repetitionAndSequence: boolean;
|
||||
/** Whether to reject passwords that include personal information. */
|
||||
personalInfo: boolean;
|
||||
/** Whether to reject passwords that include specific words. */
|
||||
words: Word[];
|
||||
words: string[];
|
||||
};
|
||||
};
|
||||
|
||||
/** Password policy configuration guard. */
|
||||
const passwordPolicyGuard: z.ZodType<PasswordPolicy> = z.object({
|
||||
length: z.object({
|
||||
min: z.number().int().min(1),
|
||||
max: z.number().int().min(1),
|
||||
}),
|
||||
characterTypes: z.object({
|
||||
min: z.number().int().min(1).max(4),
|
||||
}),
|
||||
rejects: z.object({
|
||||
pwned: z.boolean(),
|
||||
repetitionAndSequence: z.boolean(),
|
||||
words: z.array(z.object({ type: z.enum(['custom', 'personal']), value: z.string() })),
|
||||
}),
|
||||
});
|
||||
export const passwordPolicyGuard = z.object({
|
||||
length: z
|
||||
.object({
|
||||
min: z.number().int().min(1).default(8),
|
||||
max: z.number().int().min(1).default(256),
|
||||
})
|
||||
.default({}),
|
||||
characterTypes: z
|
||||
.object({
|
||||
min: z.number().int().min(1).max(4).optional().default(2),
|
||||
})
|
||||
.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. */
|
||||
type PasswordRejectionCode =
|
||||
export type PasswordRejectionCode =
|
||||
| 'too_short'
|
||||
| 'too_long'
|
||||
| 'character_types'
|
||||
|
@ -63,16 +67,25 @@ type PasswordRejectionCode =
|
|||
| 'pwned'
|
||||
| 'repetition'
|
||||
| 'sequence'
|
||||
| 'personal_info'
|
||||
| 'restricted_words';
|
||||
|
||||
/** A password issue that does not meet the policy. */
|
||||
type PasswordIssue = {
|
||||
export type PasswordIssue = {
|
||||
/** Issue code. */
|
||||
code: `password_rejected.${PasswordRejectionCode}`;
|
||||
/** Interpolation data for the issue message. */
|
||||
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
|
||||
* {@link PasswordPolicy}.
|
||||
|
@ -98,28 +111,73 @@ type PasswordIssue = {
|
|||
export class PasswordPolicyChecker {
|
||||
static symbols = Object.freeze('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' as const);
|
||||
|
||||
constructor(public readonly policy: PasswordPolicy) {
|
||||
// Validate policy.
|
||||
passwordPolicyGuard.parse(policy);
|
||||
public readonly policy: PasswordPolicy;
|
||||
|
||||
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 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.
|
||||
* @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 */
|
||||
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[] = [];
|
||||
|
||||
if (password.length < this.policy.length.min) {
|
||||
issues.push({
|
||||
code: 'password_rejected.too_short',
|
||||
interpolation: { min: this.policy.length.min },
|
||||
});
|
||||
} else if (password.length > this.policy.length.max) {
|
||||
issues.push({
|
||||
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.hasRepetition(password)) {
|
||||
issues.push({
|
||||
|
@ -155,12 +207,14 @@ export class PasswordPolicyChecker {
|
|||
}
|
||||
}
|
||||
|
||||
issues.push(
|
||||
...this.hasWords(password).map<PasswordIssue>(({ type, value }) => ({
|
||||
const words = this.hasWords(password);
|
||||
|
||||
if (words.length > 0) {
|
||||
issues.push({
|
||||
code: 'password_rejected.restricted_words',
|
||||
interpolation: { type, value },
|
||||
}))
|
||||
);
|
||||
interpolation: { words: words.join('\n'), count: words.length },
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
@ -199,13 +253,13 @@ export class PasswordPolicyChecker {
|
|||
* @returns Whether the password has been pwned.
|
||||
*/
|
||||
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))
|
||||
.map((binary) => binary.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
const hashPrefix = hashHex.slice(0, 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 hashes = text.split('\n');
|
||||
const found = hashes.some((hex) => hex.toLowerCase().startsWith(hashSuffix));
|
||||
|
@ -228,20 +282,53 @@ export class PasswordPolicyChecker {
|
|||
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.
|
||||
*
|
||||
* @param password - Password to check.
|
||||
* @returns An array of matched words.
|
||||
*/
|
||||
hasWords(password: string): Word[] {
|
||||
const words = this.policy.rejects.words.map(({ value, ...rest }) => ({
|
||||
...rest,
|
||||
value: value.toLowerCase(),
|
||||
}));
|
||||
hasWords(password: string): string[] {
|
||||
const words = this.policy.rejects.words.map((word) => word.toLowerCase());
|
||||
const lowercasedPassword = password.toLowerCase();
|
||||
|
||||
return words.filter(({ value }) => lowercasedPassword.includes(value));
|
||||
return words.filter((word) => lowercasedPassword.includes(word));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"@react-spring/web": "^9.6.1",
|
||||
"@silverhand/eslint-config": "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-react": "4.0.0",
|
||||
"@swc/core": "^1.3.52",
|
||||
|
|
|
@ -204,6 +204,7 @@ export const mockSignInExperience: SignInExperience = {
|
|||
signInMode: SignInMode.SignInAndRegister,
|
||||
customCss: null,
|
||||
customContent: {},
|
||||
passwordPolicy: {},
|
||||
};
|
||||
|
||||
export const mockSignInExperienceSettings: SignInExperienceResponse = {
|
||||
|
@ -228,6 +229,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = {
|
|||
},
|
||||
customCss: null,
|
||||
customContent: {},
|
||||
passwordPolicy: {},
|
||||
};
|
||||
|
||||
const usernameSettings = {
|
||||
|
|
213
pnpm-lock.yaml
213
pnpm-lock.yaml
|
@ -49,8 +49,8 @@ importers:
|
|||
specifier: ^3.0.2
|
||||
version: 3.0.2(tslib@2.4.1)(typescript@5.0.2)
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
applicationinsights:
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
|
@ -125,8 +125,8 @@ importers:
|
|||
specifier: workspace:^2.0.0
|
||||
version: link:../shared
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
chalk:
|
||||
specifier: ^5.0.0
|
||||
version: 5.1.2
|
||||
|
@ -240,8 +240,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
dayjs:
|
||||
specifier: ^1.10.5
|
||||
version: 1.11.6
|
||||
|
@ -328,8 +328,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
dayjs:
|
||||
specifier: ^1.10.5
|
||||
version: 1.11.6
|
||||
|
@ -416,8 +416,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -495,8 +495,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -577,8 +577,8 @@ importers:
|
|||
specifier: workspace:^2.0.0
|
||||
version: link:../../shared
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -665,8 +665,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -747,8 +747,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -826,8 +826,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -905,8 +905,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -984,8 +984,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1063,8 +1063,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1145,8 +1145,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1224,8 +1224,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1303,8 +1303,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1382,8 +1382,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1461,8 +1461,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1540,8 +1540,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.7.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1619,8 +1619,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1698,8 +1698,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1777,8 +1777,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1856,8 +1856,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -1935,8 +1935,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2014,8 +2014,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2099,8 +2099,8 @@ importers:
|
|||
specifier: workspace:^2.0.0
|
||||
version: link:../../shared
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2184,8 +2184,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
fast-xml-parser:
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
|
@ -2269,8 +2269,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2348,8 +2348,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2427,8 +2427,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2512,8 +2512,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2591,8 +2591,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2670,8 +2670,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2749,8 +2749,8 @@ importers:
|
|||
specifier: workspace:^1.1.0
|
||||
version: link:../../toolkit/connector-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -2888,8 +2888,8 @@ importers:
|
|||
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)
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@silverhand/ts-config':
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0(typescript@5.0.2)
|
||||
|
@ -3164,8 +3164,8 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../ui
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@withtyped/client':
|
||||
specifier: ^0.7.22
|
||||
version: 0.7.22(zod@3.20.2)
|
||||
|
@ -3495,8 +3495,8 @@ importers:
|
|||
specifier: 4.0.1
|
||||
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@silverhand/ts-config':
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0(typescript@5.0.2)
|
||||
|
@ -3555,8 +3555,8 @@ importers:
|
|||
specifier: workspace:^1.0.0
|
||||
version: link:../toolkit/language-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
zod:
|
||||
specifier: ^3.20.2
|
||||
version: 3.20.2
|
||||
|
@ -3582,12 +3582,15 @@ importers:
|
|||
|
||||
packages/phrases-ui:
|
||||
dependencies:
|
||||
'@logto/core-kit':
|
||||
specifier: workspace:^2.0.1
|
||||
version: link:../toolkit/core-kit
|
||||
'@logto/language-kit':
|
||||
specifier: workspace:^1.0.0
|
||||
version: link:../toolkit/language-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
zod:
|
||||
specifier: ^3.20.2
|
||||
version: 3.20.2
|
||||
|
@ -3645,8 +3648,8 @@ importers:
|
|||
specifier: 4.0.1
|
||||
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@silverhand/ts-config':
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0(typescript@5.0.2)
|
||||
|
@ -3699,8 +3702,8 @@ importers:
|
|||
packages/shared:
|
||||
dependencies:
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
chalk:
|
||||
specifier: ^5.0.0
|
||||
version: 5.1.2
|
||||
|
@ -3751,8 +3754,8 @@ importers:
|
|||
specifier: workspace:^1.0.0
|
||||
version: link:../language-kit
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@withtyped/client':
|
||||
specifier: ^0.7.22
|
||||
version: 0.7.22(zod@3.20.2)
|
||||
|
@ -3821,8 +3824,8 @@ importers:
|
|||
specifier: 4.0.1
|
||||
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@silverhand/ts-config':
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0(typescript@5.0.2)
|
||||
|
@ -3960,8 +3963,8 @@ importers:
|
|||
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)
|
||||
'@silverhand/essentials':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@silverhand/ts-config':
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0(typescript@5.0.2)
|
||||
|
@ -7297,7 +7300,7 @@ packages:
|
|||
resolution: {integrity: sha512-yDWSZMI2Qo/xoYU92tnwSP/gnSvq8+CLK5DqD/4brO42QJa7xjt7eA+HSyuMmSUrKffY2nP3riU81gs+nR8DkA==}
|
||||
engines: {node: ^18.12.0}
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.8.2
|
||||
'@silverhand/essentials': 2.8.4
|
||||
tiny-cookie: 2.4.1
|
||||
dev: false
|
||||
|
||||
|
@ -7305,7 +7308,7 @@ packages:
|
|||
resolution: {integrity: sha512-4XsXlCC0uZHcfazV09/4YKo4koqvSzQlkPUAToTp/WHpb6h2XDOJh5/hi55LXL4zp0PCcgpErKRxFCtgXCc6WQ==}
|
||||
dependencies:
|
||||
'@logto/client': 2.2.0
|
||||
'@silverhand/essentials': 2.7.0
|
||||
'@silverhand/essentials': 2.8.4
|
||||
js-base64: 3.7.5
|
||||
dev: true
|
||||
|
||||
|
@ -7313,7 +7316,7 @@ packages:
|
|||
resolution: {integrity: sha512-vw8xDW8k38/58Q1r592z/9JdsmUh4+LMmoVm/Nu7LbWKlT32eD3H9hZDkFK9XEHpriifhI0hP7asGWEmhrEUuQ==}
|
||||
dependencies:
|
||||
'@logto/js': 2.1.1
|
||||
'@silverhand/essentials': 2.7.0
|
||||
'@silverhand/essentials': 2.8.4
|
||||
camelcase-keys: 7.0.2
|
||||
jose: 4.14.4
|
||||
dev: true
|
||||
|
@ -7322,7 +7325,7 @@ packages:
|
|||
resolution: {integrity: sha512-7I2ELo5UWIJsFCYK/gX465l0+QhXTdyYWkgb2CcdPu5KbaPBNpASedm+fEV2NREYe2svbNODFhog6UMA/xGQnQ==}
|
||||
dependencies:
|
||||
'@logto/js': 2.1.1
|
||||
'@silverhand/essentials': 2.8.2
|
||||
'@silverhand/essentials': 2.8.4
|
||||
camelcase-keys: 7.0.2
|
||||
jose: 4.14.4
|
||||
dev: true
|
||||
|
@ -7331,7 +7334,7 @@ packages:
|
|||
resolution: {integrity: sha512-zxy9zr5swOxbzYJNYtKXofj2tSIS9565d+1pT6RSbmx3Hn+JG6uzsb75PZXW9vlmmm7p1sGZeTQ+xVzKNFPsMg==}
|
||||
engines: {node: ^18.12.0}
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.7.0
|
||||
'@silverhand/essentials': 2.8.4
|
||||
'@withtyped/server': 0.12.8(zod@3.20.2)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
@ -7341,7 +7344,7 @@ packages:
|
|||
resolution: {integrity: sha512-dIrEUW7gi477HQpNsq/HT1gdvPK2ZmVuV73u2rH9LXGEIFIVGqmmIaaK3IcOPG110jKCBhTzF0+hKsW9Y3Pjmw==}
|
||||
engines: {node: ^18.12.0}
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.8.2
|
||||
'@silverhand/essentials': 2.8.4
|
||||
'@withtyped/server': 0.12.9(zod@3.20.2)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
@ -7350,7 +7353,7 @@ packages:
|
|||
/@logto/js@2.1.1:
|
||||
resolution: {integrity: sha512-PHikheavVK+l4ivgtzi14p184hEPgXjqQEAom1Gme1MZoopx+WlwxvHSEQBsmyvVqRtI0oiojhoU5tgYi1FKJw==}
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.7.0
|
||||
'@silverhand/essentials': 2.8.4
|
||||
camelcase-keys: 7.0.2
|
||||
jose: 4.14.2
|
||||
dev: true
|
||||
|
@ -7359,7 +7362,7 @@ packages:
|
|||
resolution: {integrity: sha512-joSzzAqaRKeEquRenoFrIXXkNxkJci5zSkk4afywz1P8tTcTysnV4eXaBmwXNpmDfQdtHBwRdSACZPLgeF8JiQ==}
|
||||
dependencies:
|
||||
'@logto/client': 2.1.0
|
||||
'@silverhand/essentials': 2.7.0
|
||||
'@silverhand/essentials': 2.8.4
|
||||
js-base64: 3.7.5
|
||||
node-fetch: 2.6.7
|
||||
transitivePeerDependencies:
|
||||
|
@ -7372,7 +7375,7 @@ packages:
|
|||
react: '>=16.8.0 || ^18.0.0'
|
||||
dependencies:
|
||||
'@logto/browser': 2.1.0
|
||||
'@silverhand/essentials': 2.7.0
|
||||
'@silverhand/essentials': 2.8.4
|
||||
react: 18.2.0
|
||||
dev: true
|
||||
|
||||
|
@ -8955,16 +8958,8 @@ packages:
|
|||
lodash: 4.17.21
|
||||
dev: true
|
||||
|
||||
/@silverhand/essentials@2.5.0:
|
||||
resolution: {integrity: sha512-8GgVFAmbo6S0EgsjYXH4aH8a69O7SzEtPFPDpVZmJuGEt8e3ODVx0F2V4rXyC3/SzFbcb2md2gRbA+Z6aTad6g==}
|
||||
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==}
|
||||
/@silverhand/essentials@2.8.4:
|
||||
resolution: {integrity: sha512-VaI00QyD2trA7n7/wHNcGNGRXoSr8dUGs/hQCu4Rju4Edl3vso7CeCXdfGU2aNDuT2uMs75of6Ph8gqVJhWlYQ==}
|
||||
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):
|
||||
|
@ -10076,7 +10071,7 @@ packages:
|
|||
peerDependencies:
|
||||
zod: ^3.19.1
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.8.2
|
||||
'@silverhand/essentials': 2.8.4
|
||||
'@withtyped/shared': 0.2.2
|
||||
zod: 3.20.2
|
||||
dev: true
|
||||
|
@ -10086,7 +10081,7 @@ packages:
|
|||
peerDependencies:
|
||||
zod: ^3.19.1
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.8.2
|
||||
'@silverhand/essentials': 2.8.4
|
||||
'@withtyped/shared': 0.2.2
|
||||
zod: 3.20.2
|
||||
|
||||
|
|
Loading…
Reference in a new issue