feat(onboarding): add initial onboarding module with domain and persistence layers

- Created package.json for onboarding module with dependencies and scripts.
- Implemented InitialOnboarding aggregate with validation and state management.
- Added Sequelize model for initial onboarding with appropriate attributes and validations.
- Developed mappers for converting between domain and persistence models.
- Included tests for aggregate behavior and Sequelize model validation.
- Configured TypeScript settings for the onboarding module.
This commit is contained in:
David Arranz 2026-09-07 13:46:40 +02:00
parent 1065311deb
commit 2f23d75b6f
14 changed files with 899 additions and 0 deletions

View File

@ -0,0 +1,61 @@
{
"name": "@erp/onboarding",
"version": "0.10.8",
"private": true,
"type": "module",
"sideEffects": false,
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"check": "biome check .",
"lint": "biome lint .",
"clean": "rimraf .turbo node_modules dist"
},
"exports": {
".": "./src/common/index.ts",
"./common": "./src/common/index.ts",
"./api": "./src/api/index.ts",
"./api/domain": "./src/api/domain/index.ts",
"./client": "./src/web/manifest.ts",
"./globals.css": "./src/web/globals.css",
"./components": "./src/web/components/index.ts"
},
"peerDependencies": {
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"dependencies": {
"@erp/auth": "workspace:*",
"@erp/core": "workspace:*",
"@erp/identity": "workspace:*",
"@hookform/resolvers": "^5.2.2",
"@repo/i18next": "workspace:*",
"@repo/rdx-audit": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
"@repo/rdx-ui": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@tanstack/react-query": "^5.98.0",
"@tanstack/react-table": "^8.21.3",
"express": "^4.22.1",
"lucide-react": "^1.8.0",
"react-hook-form": "^7.72.1",
"react-i18next": "^17.0.2",
"react-router-dom": "^7.14.0",
"sequelize": "^6.37.8",
"use-debounce": "^10.1.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jest": "^30.0.0",
"@types/node": "^25.9.1",
"@types/react": "^19.2.14",
"@types/react-router-dom": "^5.3.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
}
}

View File

@ -0,0 +1 @@
export * from "./initial-onboarding";

View File

@ -0,0 +1 @@
export * from "./initial-onboarding.aggregate";

View File

@ -0,0 +1,72 @@
import { test } from "node:test";
import { UniqueID, UtcDateTime } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import { InitialOnboarding } from "./initial-onboarding.aggregate";
const check = (value: unknown) => {
if (!value) throw new Error("Assertion failed");
};
const date = () => UtcDateTime.now();
const props = (
status: "pending" | "completed",
companyId = Maybe.none<UniqueID>(),
completedAt = Maybe.none<UtcDateTime>()
) => ({
accountId: UniqueID.generateNewID(),
status,
companyId,
createdAt: date(),
updatedAt: date(),
completedAt,
});
test("creates pending", () => {
const accountId = UniqueID.generateNewID();
const r = InitialOnboarding.createPending(accountId);
check(r.isSuccess);
check(r.data.status === "pending");
check(r.data.companyId.isNone());
check(r.data.completedAt.isNone());
check(r.data.accountId.equals(accountId));
});
test("rehydrates valid states", () => {
check(InitialOnboarding.rehydrate(props("pending"), UniqueID.generateNewID()).isSuccess);
check(
InitialOnboarding.rehydrate(
props("completed", Maybe.some(UniqueID.generateNewID()), Maybe.some(date())),
UniqueID.generateNewID()
).isSuccess
);
});
test("rejects invalid state combinations", () => {
check(
InitialOnboarding.rehydrate(
props("pending", Maybe.some(UniqueID.generateNewID())),
UniqueID.generateNewID()
).isFailure
);
check(
InitialOnboarding.rehydrate(
props("pending", Maybe.none<UniqueID>(), Maybe.some(date())),
UniqueID.generateNewID()
).isFailure
);
check(InitialOnboarding.rehydrate(props("completed"), UniqueID.generateNewID()).isFailure);
check(
InitialOnboarding.rehydrate(
props("completed", Maybe.some(UniqueID.generateNewID())),
UniqueID.generateNewID()
).isFailure
);
});
test("completes once", () => {
const o = InitialOnboarding.createPending(UniqueID.generateNewID()).data;
const id = UniqueID.generateNewID();
check(o.complete({ companyId: id, completedAt: date(), updatedAt: date() }).isSuccess);
check(o.status === "completed");
check(o.companyId.unwrap().equals(id));
check(o.completedAt.isSome());
check(o.complete({ companyId: id, completedAt: date(), updatedAt: date() }).isFailure);
});

View File

@ -0,0 +1,79 @@
import { AggregateRoot, UniqueID, type UtcDateTime } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
export type InitialOnboardingStatus = "pending" | "completed";
export interface InitialOnboardingProps {
accountId: UniqueID;
status: InitialOnboardingStatus;
companyId: Maybe<UniqueID>;
completedAt: Maybe<UtcDateTime>;
}
export class InitialOnboarding extends AggregateRoot<InitialOnboardingProps> {
private static validate(props: InitialOnboardingProps): Result<void, Error> {
const valid =
props.status === "pending"
? props.companyId.isNone() && props.completedAt.isNone()
: props.companyId.isSome() && props.completedAt.isSome();
return valid
? Result.ok(undefined)
: Result.fail(new Error("Invalid initial onboarding state."));
}
static createPending(accountId: UniqueID): Result<InitialOnboarding, Error> {
return InitialOnboarding.rehydrate(
{
accountId,
status: "pending",
companyId: Maybe.none(),
completedAt: Maybe.none(),
},
UniqueID.generateNewID()
);
}
static rehydrate(props: InitialOnboardingProps, id: UniqueID): Result<InitialOnboarding, Error> {
const valid = InitialOnboarding.validate(props);
return valid.isFailure ? Result.fail(valid.error) : Result.ok(new InitialOnboarding(props, id));
}
complete(params: {
companyId: UniqueID;
completedAt: UtcDateTime;
updatedAt: UtcDateTime;
}): Result<void, Error> {
if (this.props.status === "completed")
return Result.fail(new Error("Initial onboarding already completed."));
const props = {
...this.props,
status: "completed" as const,
companyId: Maybe.some(params.companyId),
completedAt: Maybe.some(params.completedAt),
updatedAt: params.updatedAt,
};
const valid = InitialOnboarding.validate(props);
if (valid.isFailure) {
return Result.fail(valid.error);
}
this.props = props;
return Result.ok(undefined);
}
get accountId() {
return this.props.accountId;
}
get status() {
return this.props.status;
}
get companyId() {
return this.props.companyId;
}
get completedAt() {
return this.props.completedAt;
}
}

View File

@ -0,0 +1,6 @@
import initialOnboardingModelInit from "./models/sequelize-initial-onboarding.model";
export * from "./mappers";
export * from "./models";
export const models = [initialOnboardingModelInit];

View File

@ -0,0 +1 @@
export * from "./sequelize-initial-onboarding-domain.mapper";

View File

@ -0,0 +1,142 @@
import assert from "node:assert/strict";
import test from "node:test";
import { UniqueID, UtcDateTime } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import type { InferAttributes } from "sequelize";
import { Sequelize } from "sequelize";
import type { InitialOnboardingStatus } from "../../../../domain";
import { InitialOnboarding } from "../../../../domain";
import initialOnboardingModelInit, {
InitialOnboardingModel,
} from "../models/sequelize-initial-onboarding.model";
import { SequelizeInitialOnboardingDomainMapper } from "./sequelize-initial-onboarding-domain.mapper";
const database = new Sequelize("mysql://root:pass@localhost:3306/erp_test", { logging: false });
initialOnboardingModelInit(database);
const mapper = new SequelizeInitialOnboardingDomainMapper();
const id = "11111111-1111-4111-8111-111111111111";
const accountId = "22222222-2222-4222-8222-222222222222";
const companyId = "33333333-3333-4333-8333-333333333333";
const completedAt = "2026-01-02T10:30:00.789Z";
function aggregate(status: InitialOnboardingStatus) {
const result = InitialOnboarding.rehydrate(
{
accountId: UniqueID.create(accountId).data,
status,
companyId:
status === "completed" ? Maybe.some(UniqueID.create(companyId).data) : Maybe.none(),
completedAt:
status === "completed"
? Maybe.some(UtcDateTime.createFromISO(completedAt).data)
: Maybe.none(),
},
UniqueID.create(id).data
);
assert.equal(result.isSuccess, true);
return result.data;
}
function row(patch: Partial<InferAttributes<InitialOnboardingModel>> = {}) {
return InitialOnboardingModel.build(
{
id,
account_id: accountId,
status: "pending",
company_id: null,
completed_at: null,
...patch,
},
{ raw: true }
);
}
for (const status of ["pending", "completed"] as const) {
test(`toPersistence ${status}`, () => {
const result = mapper.mapToPersistence(aggregate(status));
assert.equal(result.isSuccess, true);
assert.deepEqual(result.data, {
id,
account_id: accountId,
status,
company_id: status === "completed" ? companyId : null,
completed_at: status === "completed" ? completedAt : null,
});
});
test(`toDomain ${status}`, () => {
const result = mapper.mapToDomain(
row({
status,
company_id: status === "completed" ? companyId : null,
completed_at: status === "completed" ? completedAt : null,
})
);
assert.equal(result.isSuccess, true);
const value = result.data;
assert.ok(value instanceof InitialOnboarding);
assert.ok(value.id instanceof UniqueID);
assert.equal(value.id.toPrimitive(), id);
assert.equal(value.accountId.toPrimitive(), accountId);
assert.equal(value.status, status);
assert.equal(value.companyId.isSome(), status === "completed");
assert.equal(value.completedAt.isSome(), status === "completed");
if (status === "completed") {
assert.ok(value.companyId.unwrap() instanceof UniqueID);
assert.ok(value.completedAt.unwrap() instanceof UtcDateTime);
assert.equal(value.companyId.unwrap().toPrimitive(), companyId);
assert.equal(value.completedAt.unwrap().toISOString(), completedAt);
}
});
}
const invalidStates: [string, Partial<InferAttributes<InitialOnboardingModel>>][] = [
["pending + companyId", { company_id: companyId }],
["pending + completedAt", { completed_at: completedAt }],
["completed sin companyId", { status: "completed", completed_at: completedAt }],
["completed sin completedAt", { status: "completed", company_id: companyId }],
];
for (const [name, patch] of invalidStates) {
test(name, () => {
const result = mapper.mapToDomain(row(patch));
assert.equal(result.isFailure, true);
assert.equal(result.error.message, "Invalid initial onboarding persistence state.");
});
}
test("toDomain accepts Sequelize Date timestamps and preserves UTC milliseconds", () => {
const result = mapper.mapToDomain(
row({
status: "completed",
company_id: companyId,
completed_at: new Date(completedAt),
})
);
assert.equal(result.isSuccess, true);
assert.equal(result.data.completedAt.unwrap().toISOString(), completedAt);
});
test("toDomain rejects an unknown status explicitly", () => {
const result = mapper.mapToDomain(
row({ status: "unknown", company_id: companyId, completed_at: completedAt })
);
assert.equal(result.isFailure, true);
assert.equal(result.error.message, "Invalid initial onboarding persistence status.");
});
test("toDomain propagates invalid identifiers and timestamps as failures", () => {
for (const patch of [
{ id: "invalid" },
{ account_id: "invalid" },
{ status: "completed", company_id: "invalid", completed_at: completedAt },
{ created_at: "invalid" },
{ updated_at: "invalid" },
{ status: "completed", company_id: companyId, completed_at: "invalid" },
]) {
assert.equal(mapper.mapToDomain(row(patch)).isFailure, true);
}
});

View File

@ -0,0 +1,62 @@
import { UniqueID, UtcDateTime, maybeFromNullableResult, maybeToNullable } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { InitialOnboarding } from "../../../../domain";
import type { InitialOnboardingCreationAttributes, InitialOnboardingModel } from "../models";
function toUtcDateTime(value: Date | string): Result<UtcDateTime, Error> {
try {
return UtcDateTime.createFromISO(value instanceof Date ? value.toISOString() : value);
} catch (error: unknown) {
return Result.fail(error instanceof Error ? error : new Error(String(error)));
}
}
export class SequelizeInitialOnboardingDomainMapper {
public mapToDomain(source: InitialOnboardingModel): Result<InitialOnboarding, Error> {
if (source.status !== "pending" && source.status !== "completed") {
return Result.fail(new Error("Invalid initial onboarding persistence status."));
}
if (
(source.status === "pending" &&
(source.company_id !== null || source.completed_at !== null)) ||
(source.status === "completed" &&
(source.company_id === null || source.completed_at === null))
) {
return Result.fail(new Error("Invalid initial onboarding persistence state."));
}
const id = UniqueID.create(source.id);
if (id.isFailure) return Result.fail(id.error);
const accountId = UniqueID.create(source.account_id);
if (accountId.isFailure) return Result.fail(accountId.error);
const companyId = maybeFromNullableResult(source.company_id, (value) => UniqueID.create(value));
if (companyId.isFailure) return Result.fail(companyId.error);
const completedAt = maybeFromNullableResult(source.completed_at, (value) =>
toUtcDateTime(value)
);
if (completedAt.isFailure) return Result.fail(completedAt.error);
return InitialOnboarding.rehydrate(
{
accountId: accountId.data,
status: source.status,
companyId: companyId.data,
completedAt: completedAt.data,
},
id.data
);
}
public mapToPersistence(
source: InitialOnboarding
): Result<InitialOnboardingCreationAttributes, Error> {
return Result.ok({
id: source.id.toPrimitive(),
account_id: source.accountId.toPrimitive(),
status: source.status,
company_id: maybeToNullable(source.companyId, (value) => value.toPrimitive()),
completed_at: maybeToNullable(source.completedAt, (value) => value.toISOString()),
});
}
}

View File

@ -0,0 +1 @@
export * from "./sequelize-initial-onboarding.model";

View File

@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import test from "node:test";
import { DataTypes, Sequelize } from "sequelize";
import initialOnboardingModelInit, {
InitialOnboardingModel,
} from "./sequelize-initial-onboarding.model";
const database = new Sequelize("mysql://root:pass@localhost:3306/erp_test", { logging: false });
initialOnboardingModelInit(database);
test("model declares table, nullability, UUIDs, timestamps and unique account without cross-module FKs", () => {
assert.equal(InitialOnboardingModel.getTableName(), "initial_onboardings");
assert.equal(InitialOnboardingModel.options.timestamps, false);
const attributes = InitialOnboardingModel.getAttributes();
assert.deepEqual(Object.keys(attributes).sort(), [
"account_id",
"company_id",
"completed_at",
"created_at",
"id",
"status",
"updated_at",
]);
for (const key of ["id", "account_id", "status", "created_at", "updated_at"] as const) {
assert.equal(attributes[key].allowNull, false);
}
for (const key of ["company_id", "completed_at"] as const) {
assert.equal(attributes[key].allowNull, true);
}
for (const key of ["id", "account_id", "company_id"] as const) {
assert.ok(attributes[key].type instanceof DataTypes.UUID);
assert.equal(attributes[key].references, undefined);
}
for (const key of ["created_at", "updated_at", "completed_at"] as const) {
assert.ok(attributes[key].type instanceof DataTypes.DATE);
}
assert.ok(attributes.status.type instanceof DataTypes.STRING);
assert.equal(attributes.id.primaryKey, true);
const unique = InitialOnboardingModel.options.indexes?.find(
(index) => index.name === "uq_initial_onboardings_account_id"
);
assert.equal(unique?.unique, true);
assert.deepEqual(unique?.fields, ["account_id"]);
});
test("model validation accepts only pending and completed", async () => {
for (const status of ["pending", "completed", "processing", "failed", "cancelled", "draft"]) {
const model = InitialOnboardingModel.build({
id: "11111111-1111-4111-8111-111111111111",
account_id: "22222222-2222-4222-8222-222222222222",
status,
});
if (status === "pending" || status === "completed") {
await model.validate({ fields: ["status"] });
} else {
await assert.rejects(
model.validate({ fields: ["status"] }),
/Validation isIn on status failed/
);
}
}
});

View File

@ -0,0 +1,49 @@
import type { InferAttributes, InferCreationAttributes, Sequelize } from "sequelize";
import { DataTypes, Model } from "sequelize";
export type InitialOnboardingCreationAttributes = InferCreationAttributes<InitialOnboardingModel>;
export class InitialOnboardingModel extends Model<
InferAttributes<InitialOnboardingModel>,
InferCreationAttributes<InitialOnboardingModel>
> {
declare id: string;
declare account_id: string;
declare status: string;
declare company_id: string | null;
declare completed_at: Date | string | null;
}
export default (database: Sequelize) => {
InitialOnboardingModel.init(
{
id: { type: DataTypes.UUID, primaryKey: true, allowNull: false },
// Referencias lógicas entre módulos, sin dependencias de sus modelos ORM.
account_id: { type: DataTypes.UUID, allowNull: false },
status: {
type: DataTypes.STRING,
allowNull: false,
validate: { isIn: [["pending", "completed"]] },
},
company_id: { type: DataTypes.UUID, allowNull: true, defaultValue: null },
completed_at: { type: DataTypes.DATE, allowNull: true, defaultValue: null },
},
{
sequelize: database,
modelName: "InitialOnboardingModel",
tableName: "initial_onboardings",
charset: "utf8mb4",
collate: "utf8mb4_unicode_ci",
underscored: true,
timestamps: true,
indexes: [
{
name: "uq_initial_onboardings_account_id",
fields: ["account_id"],
unique: true,
},
],
}
);
return InitialOnboardingModel;
};

View File

@ -0,0 +1,28 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@erp/onboarding/*": ["./src/*"]
},
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"types": ["jest", "node"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -1213,6 +1213,103 @@ importers:
specifier: ^6.0.2
version: 6.0.2
modules/onboarding:
dependencies:
'@erp/auth':
specifier: workspace:*
version: link:../auth
'@erp/core':
specifier: workspace:*
version: link:../core
'@erp/identity':
specifier: workspace:*
version: link:../identity
'@hookform/resolvers':
specifier: ^5.2.2
version: 5.4.0(react-hook-form@7.76.1(react@19.2.6))
'@repo/i18next':
specifier: workspace:*
version: link:../../packages/i18n
'@repo/rdx-audit':
specifier: workspace:*
version: link:../../packages/rdx-audit
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria
'@repo/rdx-ddd':
specifier: workspace:*
version: link:../../packages/rdx-ddd
'@repo/rdx-logger':
specifier: workspace:*
version: link:../../packages/rdx-logger
'@repo/rdx-ui':
specifier: workspace:*
version: link:../../packages/rdx-ui
'@repo/rdx-utils':
specifier: workspace:*
version: link:../../packages/rdx-utils
'@repo/shadcn-ui':
specifier: workspace:*
version: link:../../packages/shadcn-ui
'@tanstack/react-query':
specifier: ^5.98.0
version: 5.100.14(react@19.2.6)
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
express:
specifier: ^4.22.1
version: 4.22.2(supports-color@7.2.0)
lucide-react:
specifier: ^1.8.0
version: 1.16.0(react@19.2.6)
react-hook-form:
specifier: ^7.72.1
version: 7.76.1(react@19.2.6)
react-i18next:
specifier: ^17.0.2
version: 17.0.8(i18next@26.4.0(typescript@6.0.2))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.2)
react-router-dom:
specifier: ^7.14.0
version: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
sequelize:
specifier: ^6.37.8
version: 6.37.8(mysql2@3.22.3(@types/node@25.9.1))(pg-hstore@2.3.4)(supports-color@7.2.0)
use-debounce:
specifier: ^10.1.1
version: 10.1.1(react@19.2.6)
zod:
specifier: ^4.3.6
version: 4.4.3
devDependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.25
'@types/jest':
specifier: ^30.0.0
version: 30.0.0
'@types/node':
specifier: ^25.9.1
version: 25.9.1
'@types/react':
specifier: ^19.2.14
version: 19.2.15
'@types/react-router-dom':
specifier: ^5.3.3
version: 5.3.3
react:
specifier: ^19.2.5
version: 19.2.6
react-dom:
specifier: ^19.2.5
version: 19.2.6(react@19.2.6)
rimraf:
specifier: ^6.1.3
version: 6.1.3
typescript:
specifier: ^6.0.2
version: 6.0.2
modules/supplier-invoices:
dependencies:
'@erp/auth':
@ -2773,6 +2870,30 @@ packages:
'@types/node':
optional: true
'@jest/diff-sequences@30.5.0':
resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/expect-utils@30.5.1':
resolution: {integrity: sha512-WcRWhHQdTMRDpyWKZ/6MINBmovI7zeD+bL8wFjCncRV3NQOwKy1X45IfyblfHR4k/XciIlNEdFL9QjFO+HNKOg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/get-type@30.5.0':
resolution: {integrity: sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/pattern@30.5.0':
resolution: {integrity: sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/schemas@30.5.0':
resolution: {integrity: sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/types@30.5.1':
resolution: {integrity: sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@ -4145,6 +4266,9 @@ packages:
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@sinclair/typebox@0.34.52':
resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==}
'@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
@ -4419,6 +4543,18 @@ packages:
'@types/inquirer@9.0.9':
resolution: {integrity: sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==}
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
'@types/istanbul-lib-report@3.0.3':
resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}
'@types/istanbul-reports@3.0.4':
resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
'@types/jest@30.0.0':
resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -4505,6 +4641,9 @@ packages:
'@types/set-cookie-parser@2.4.10':
resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==}
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
'@types/statuses@2.0.6':
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
@ -4523,6 +4662,12 @@ packages:
'@types/validator@13.15.10':
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript-eslint/eslint-plugin@8.60.0':
resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -4662,6 +4807,10 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@ -4833,6 +4982,10 @@ packages:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
ci-info@4.4.0:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@ -5330,6 +5483,10 @@ packages:
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@2.0.0:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@ -5411,6 +5568,10 @@ packages:
resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==}
engines: {node: '>=0.10.0'}
expect@30.5.1:
resolution: {integrity: sha512-m8YrYgvKe9+9gEnWEuKz+qCGfHqkrff7PPfyDnOFkjsfYRKqiYyOxDFMFieCGAuhtk/VNm63tvNgKZVzVy+Hvg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
express-list-routes@1.3.2:
resolution: {integrity: sha512-bwpS3aagDdzmtnIlYb9BDGoaIES3RQaOVNugfBUESM6kqkv+Y1bvztDju8wARzy8FDzuO6/HXT6OCGCiNd9f+g==}
@ -6026,6 +6187,30 @@ packages:
engines: {node: '>=10'}
hasBin: true
jest-diff@30.5.1:
resolution: {integrity: sha512-e3cNNMpv8Kh20MjjphTXs+3Vz7DQyLM1nft7KJhnh46atFhjVJRa+0Hq0beywuwsACtMQUBihQkFl8zxb7gt1Q==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jest-matcher-utils@30.5.1:
resolution: {integrity: sha512-aroZVqwOz/wC2y6pC+obgFWKV9viaQWQTTSB6W5H55+egtUczIfXR/rxExTv92xD/ADYwpqaYfVWx1aqwKg7FA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jest-message-util@30.5.1:
resolution: {integrity: sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jest-mock@30.5.1:
resolution: {integrity: sha512-9fVjc3leUpGID2/by/LU4Dvdcp7PFh9LlxS3QRWK3ABm+KtvEVsG/AEGeLY3gKOZsjkBxyfwGltoAVlW7dygHg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jest-regex-util@30.5.0:
resolution: {integrity: sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jest-util@30.5.1:
resolution: {integrity: sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
@ -6853,6 +7038,10 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
pretty-format@30.5.1:
resolution: {integrity: sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
pretty-ms@9.3.0:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'}
@ -6955,6 +7144,12 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-is@19.2.8:
resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==}
react-qr-code@2.0.21:
resolution: {integrity: sha512-xaywjo0eaF4S3LOz6ns5eoPbM2E+q9HYl4VATYpxK4bBniOhQ9noY2RJ9G4SnZFhUwzx63FUT6KdHzfKgUwyuQ==}
peerDependencies:
@ -7325,6 +7520,10 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
@ -7357,6 +7556,10 @@ packages:
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
@ -8838,6 +9041,33 @@ snapshots:
optionalDependencies:
'@types/node': 25.9.1
'@jest/diff-sequences@30.5.0': {}
'@jest/expect-utils@30.5.1':
dependencies:
'@jest/get-type': 30.5.0
'@jest/get-type@30.5.0': {}
'@jest/pattern@30.5.0':
dependencies:
'@types/node': 25.9.1
jest-regex-util: 30.5.0
'@jest/schemas@30.5.0':
dependencies:
'@sinclair/typebox': 0.34.52
'@jest/types@30.5.1':
dependencies:
'@jest/pattern': 30.5.0
'@jest/schemas': 30.5.0
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 25.9.1
'@types/yargs': 17.0.35
chalk: 4.1.2
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -10114,6 +10344,8 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
'@sinclair/typebox@0.34.52': {}
'@sindresorhus/merge-streams@4.0.0': {}
'@so-ric/colorspace@1.1.6':
@ -10362,6 +10594,21 @@ snapshots:
'@types/through': 0.0.33
rxjs: 7.8.2
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/istanbul-lib-report@3.0.3':
dependencies:
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports@3.0.4':
dependencies:
'@types/istanbul-lib-report': 3.0.3
'@types/jest@30.0.0':
dependencies:
expect: 30.5.1
pretty-format: 30.5.1
'@types/json-schema@7.0.15': {}
'@types/jsonwebtoken@9.0.10':
@ -10467,6 +10714,8 @@ snapshots:
dependencies:
'@types/node': 25.9.1
'@types/stack-utils@2.0.3': {}
'@types/statuses@2.0.6': {}
'@types/through@0.0.33':
@ -10481,6 +10730,12 @@ snapshots:
'@types/validator@13.15.10': {}
'@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.35':
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.2))(eslint@10.4.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.2)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@ -10639,6 +10894,8 @@ snapshots:
dependencies:
color-convert: 2.0.1
ansi-styles@5.2.0: {}
any-promise@1.3.0: {}
arg@4.1.3: {}
@ -10828,6 +11085,8 @@ snapshots:
dependencies:
readdirp: 5.0.0
ci-info@4.4.0: {}
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@ -11294,6 +11553,8 @@ snapshots:
escape-html@1.0.3: {}
escape-string-regexp@2.0.0: {}
escape-string-regexp@4.0.0: {}
eslint-scope@9.1.2:
@ -11407,6 +11668,15 @@ snapshots:
dependencies:
homedir-polyfill: 1.0.3
expect@30.5.1:
dependencies:
'@jest/expect-utils': 30.5.1
'@jest/get-type': 30.5.0
jest-matcher-utils: 30.5.1
jest-message-util: 30.5.1
jest-mock: 30.5.1
jest-util: 30.5.1
express-list-routes@1.3.2: {}
express-rate-limit@8.5.2(express@5.2.1(supports-color@7.2.0)):
@ -12047,6 +12317,51 @@ snapshots:
filelist: 1.0.6
picocolors: 1.1.1
jest-diff@30.5.1:
dependencies:
'@jest/diff-sequences': 30.5.0
'@jest/get-type': 30.5.0
chalk: 4.1.2
pretty-format: 30.5.1
jest-matcher-utils@30.5.1:
dependencies:
'@jest/get-type': 30.5.0
chalk: 4.1.2
jest-diff: 30.5.1
pretty-format: 30.5.1
jest-message-util@30.5.1:
dependencies:
'@babel/code-frame': 7.29.7
'@jest/types': 30.5.1
'@types/stack-utils': 2.0.3
chalk: 4.1.2
graceful-fs: 4.2.11
jest-util: 30.5.1
picomatch: 4.0.4
pretty-format: 30.5.1
slash: 3.0.0
stack-utils: 2.0.6
jest-mock@30.5.1:
dependencies:
'@jest/expect-utils': 30.5.1
'@jest/types': 30.5.1
'@types/node': 25.9.1
jest-util: 30.5.1
jest-regex-util@30.5.0: {}
jest-util@30.5.1:
dependencies:
'@jest/types': 30.5.1
'@types/node': 25.9.1
chalk: 4.1.2
ci-info: 4.4.0
graceful-fs: 4.2.11
picomatch: 4.0.4
jiti@2.7.0: {}
joi@18.2.1:
@ -12828,6 +13143,13 @@ snapshots:
prelude-ls@1.2.1: {}
pretty-format@30.5.1:
dependencies:
'@jest/react-is-18': react-is@18.3.1
'@jest/react-is-19': react-is@19.2.8
'@jest/schemas': 30.5.0
ansi-styles: 5.2.0
pretty-ms@9.3.0:
dependencies:
parse-ms: 4.0.0
@ -12987,6 +13309,10 @@ snapshots:
react-is@16.13.1: {}
react-is@18.3.1: {}
react-is@19.2.8: {}
react-qr-code@2.0.21(react@19.2.6):
dependencies:
prop-types: 15.8.1
@ -13459,6 +13785,8 @@ snapshots:
sisteransi@1.0.5: {}
slash@3.0.0: {}
sonner@2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
react: 19.2.6
@ -13481,6 +13809,10 @@ snapshots:
stack-trace@0.0.10: {}
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0
statuses@2.0.2: {}
stdin-discarder@0.2.2: {}