@fiado/type-kit 3.50.0 → 3.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { CreatePoolRequest } from '../../../../src/cognitoBackofficeConnector/dtos/CreatePoolRequest';
4
+
5
+ describe('CreatePoolRequest', () => {
6
+ it('ya NO expone mfaConfig (pool MFA nativo OFF cerrado a nivel contrato — F-11 D2)', () => {
7
+ const instance = plainToInstance(
8
+ CreatePoolRequest,
9
+ {
10
+ region: 'us-east-1',
11
+ displayName: 'Tenant Demo',
12
+ mfaConfig: { requireMfa: true, mfaTypes: ['SOFTWARE_TOKEN_MFA'] },
13
+ passwordPolicy: {},
14
+ customAttributes: [],
15
+ appClientConfig: {},
16
+ },
17
+ { excludeExtraneousValues: true },
18
+ );
19
+
20
+ expect((instance as { mfaConfig?: unknown }).mfaConfig).toBeUndefined();
21
+ });
22
+
23
+ it('preserva los campos legítimos del request', () => {
24
+ const instance = plainToInstance(
25
+ CreatePoolRequest,
26
+ { region: 'us-east-1', displayName: 'Tenant Demo' },
27
+ { excludeExtraneousValues: true },
28
+ );
29
+
30
+ expect(instance.region).toBe('us-east-1');
31
+ expect(instance.displayName).toBe('Tenant Demo');
32
+ });
33
+ });
@@ -0,0 +1,68 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { CreateTenantRequest } from '../../../../src/platformRbac/dtos/CreateTenantRequest';
5
+
6
+ describe('CreateTenantRequest', () => {
7
+ const valid = {
8
+ tenantId: 'acme-mx',
9
+ displayName: 'Acme México',
10
+ emailDomain: 'acme.com',
11
+ tablePrefix: 'acme',
12
+ adminEmail: 'admin@acme.com',
13
+ adminName: 'Admin Acme',
14
+ region: 'us-east-1',
15
+ mfaRequired: true,
16
+ passwordMinLength: 12,
17
+ };
18
+
19
+ it('valida happy path (0 errores)', async () => {
20
+ const dto = plainToInstance(CreateTenantRequest, valid);
21
+ const errors = await validate(dto);
22
+ expect(errors).toEqual([]);
23
+ });
24
+
25
+ it('valida sin campos opcionales (mfaRequired / passwordMinLength)', async () => {
26
+ const { mfaRequired, passwordMinLength, ...sinOpcionales } = valid;
27
+ const dto = plainToInstance(CreateTenantRequest, sinOpcionales);
28
+ const errors = await validate(dto);
29
+ expect(errors).toEqual([]);
30
+ });
31
+
32
+ it('falla si adminEmail es inválido', async () => {
33
+ const dto = plainToInstance(CreateTenantRequest, { ...valid, adminEmail: 'no-es-email' });
34
+ const errors = await validate(dto);
35
+ expect(errors.some(e => e.property === 'adminEmail')).toBe(true);
36
+ });
37
+
38
+ it('falla si tenantId tiene mayúsculas', async () => {
39
+ const dto = plainToInstance(CreateTenantRequest, { ...valid, tenantId: 'Acme-MX' });
40
+ const errors = await validate(dto);
41
+ expect(errors.some(e => e.property === 'tenantId')).toBe(true);
42
+ });
43
+
44
+ it('falla si tenantId tiene caracteres inválidos (espacios / underscore)', async () => {
45
+ const dto = plainToInstance(CreateTenantRequest, { ...valid, tenantId: 'acme_mx 01' });
46
+ const errors = await validate(dto);
47
+ expect(errors.some(e => e.property === 'tenantId')).toBe(true);
48
+ });
49
+
50
+ it('falla si passwordMinLength es menor a 8', async () => {
51
+ const dto = plainToInstance(CreateTenantRequest, { ...valid, passwordMinLength: 6 });
52
+ const errors = await validate(dto);
53
+ expect(errors.some(e => e.property === 'passwordMinLength')).toBe(true);
54
+ });
55
+
56
+ it('NO expone temporaryPassword en el request (campo del response, no del body)', () => {
57
+ // El request NO debe llevar temporaryPassword (es solo del response, fallback out-of-band F-11).
58
+ // Con excludeExtraneousValues solo sobreviven las props @Expose() del DTO → el contrato lo excluye.
59
+ const dto = plainToInstance(
60
+ CreateTenantRequest,
61
+ { ...valid, temporaryPassword: 'Secreto123!' },
62
+ { excludeExtraneousValues: true },
63
+ );
64
+ expect((dto as unknown as Record<string, unknown>).temporaryPassword).toBeUndefined();
65
+ // sanity: una prop sí declarada en el contrato sí sobrevive
66
+ expect(dto.tenantId).toBe('acme-mx');
67
+ });
68
+ });
@@ -1,4 +1,3 @@
1
- import { MfaPoolConfig } from './MfaPoolConfig';
2
1
  import { PasswordPolicyConfig } from './PasswordPolicyConfig';
3
2
  import { CustomAttributeSpec } from './CustomAttributeSpec';
4
3
  import { AppClientConfig } from './AppClientConfig';
@@ -8,11 +7,15 @@ import { AppClientConfig } from './AppClientConfig';
8
7
  * (Flujo 2 v1.2, PASO 4 de la saga). El connector ejecuta CreateUserPool +
9
8
  * CreateUserPoolClient en secuencia. Si CreateUserPoolClient falla DESPUÉS de
10
9
  * CreateUserPool exitoso, el connector hace DeleteUserPool de cleanup.
10
+ *
11
+ * El request NO expone configuración de MFA nativo del pool (F-11 D2): el 2º
12
+ * factor del backoffice va por custom-auth (EMAIL OTP / TOTP), nunca por MFA
13
+ * nativo del pool. El connector fuerza `MfaConfiguration: 'OFF'` como invariante
14
+ * de código al crear el pool — no es negociable vía contrato.
11
15
  */
12
16
  export declare class CreatePoolRequest {
13
17
  region: string;
14
18
  displayName: string;
15
- mfaConfig: MfaPoolConfig;
16
19
  passwordPolicy: PasswordPolicyConfig;
17
20
  customAttributes: CustomAttributeSpec[];
18
21
  appClientConfig: AppClientConfig;
@@ -12,7 +12,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.CreatePoolRequest = void 0;
13
13
  const class_transformer_1 = require("class-transformer");
14
14
  const class_validator_1 = require("class-validator");
15
- const MfaPoolConfig_1 = require("./MfaPoolConfig");
16
15
  const PasswordPolicyConfig_1 = require("./PasswordPolicyConfig");
17
16
  const CustomAttributeSpec_1 = require("./CustomAttributeSpec");
18
17
  const AppClientConfig_1 = require("./AppClientConfig");
@@ -22,6 +21,11 @@ const AppClientConfig_1 = require("./AppClientConfig");
22
21
  * (Flujo 2 v1.2, PASO 4 de la saga). El connector ejecuta CreateUserPool +
23
22
  * CreateUserPoolClient en secuencia. Si CreateUserPoolClient falla DESPUÉS de
24
23
  * CreateUserPool exitoso, el connector hace DeleteUserPool de cleanup.
24
+ *
25
+ * El request NO expone configuración de MFA nativo del pool (F-11 D2): el 2º
26
+ * factor del backoffice va por custom-auth (EMAIL OTP / TOTP), nunca por MFA
27
+ * nativo del pool. El connector fuerza `MfaConfiguration: 'OFF'` como invariante
28
+ * de código al crear el pool — no es negociable vía contrato.
25
29
  */
26
30
  class CreatePoolRequest {
27
31
  }
@@ -36,12 +40,6 @@ __decorate([
36
40
  (0, class_validator_1.IsString)(),
37
41
  __metadata("design:type", String)
38
42
  ], CreatePoolRequest.prototype, "displayName", void 0);
39
- __decorate([
40
- (0, class_transformer_1.Expose)(),
41
- (0, class_validator_1.ValidateNested)(),
42
- (0, class_transformer_1.Type)(() => MfaPoolConfig_1.MfaPoolConfig),
43
- __metadata("design:type", MfaPoolConfig_1.MfaPoolConfig)
44
- ], CreatePoolRequest.prototype, "mfaConfig", void 0);
45
43
  __decorate([
46
44
  (0, class_transformer_1.Expose)(),
47
45
  (0, class_validator_1.ValidateNested)(),
@@ -10,7 +10,6 @@
10
10
  export * from './enums/CognitoChallengeType';
11
11
  export * from './enums/CognitoUserStatus';
12
12
  export * from './validators/NoTenantIdInCustomAttrs';
13
- export * from './validators/MfaTypesRequiresOne';
14
13
  export * from './dtos/CreateUserRequest';
15
14
  export * from './dtos/CreateUserResponse';
16
15
  export * from './dtos/UpdateUserAttributesRequest';
@@ -43,7 +42,6 @@ export * from './dtos/UpdateEmailRequest';
43
42
  export * from './dtos/VerifyEmailRequest';
44
43
  export * from './dtos/UpdateProfileRequest';
45
44
  export * from './dtos/HealthcheckResponse';
46
- export * from './dtos/MfaPoolConfig';
47
45
  export * from './dtos/PasswordPolicyConfig';
48
46
  export * from './dtos/CustomAttributeSpec';
49
47
  export * from './dtos/AppClientConfig';
@@ -26,7 +26,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  __exportStar(require("./enums/CognitoChallengeType"), exports);
27
27
  __exportStar(require("./enums/CognitoUserStatus"), exports);
28
28
  __exportStar(require("./validators/NoTenantIdInCustomAttrs"), exports);
29
- __exportStar(require("./validators/MfaTypesRequiresOne"), exports);
30
29
  __exportStar(require("./dtos/CreateUserRequest"), exports);
31
30
  __exportStar(require("./dtos/CreateUserResponse"), exports);
32
31
  __exportStar(require("./dtos/UpdateUserAttributesRequest"), exports);
@@ -59,7 +58,6 @@ __exportStar(require("./dtos/UpdateEmailRequest"), exports);
59
58
  __exportStar(require("./dtos/VerifyEmailRequest"), exports);
60
59
  __exportStar(require("./dtos/UpdateProfileRequest"), exports);
61
60
  __exportStar(require("./dtos/HealthcheckResponse"), exports);
62
- __exportStar(require("./dtos/MfaPoolConfig"), exports);
63
61
  __exportStar(require("./dtos/PasswordPolicyConfig"), exports);
64
62
  __exportStar(require("./dtos/CustomAttributeSpec"), exports);
65
63
  __exportStar(require("./dtos/AppClientConfig"), exports);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Input del POST backoffice de creación de tenant (F-11 — onboarding de tenant en SureKeep).
3
+ * Consumido por el controller `backofficeCreateTenant` del platform-rbac-business y, a futuro,
4
+ * por el frontend de staff. La `idempotencyKey` viaja en header HTTP (no en el body).
5
+ * El `temporaryPassword` NO viaja en el request: vive solo en el response como fallback
6
+ * out-of-band mientras el correo de invitación no entrega.
7
+ */
8
+ export declare class CreateTenantRequest {
9
+ tenantId: string;
10
+ displayName: string;
11
+ emailDomain: string;
12
+ tablePrefix: string;
13
+ adminEmail: string;
14
+ adminName: string;
15
+ region: string;
16
+ mfaRequired?: boolean;
17
+ passwordMinLength?: number;
18
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CreateTenantRequest = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ /**
16
+ * Input del POST backoffice de creación de tenant (F-11 — onboarding de tenant en SureKeep).
17
+ * Consumido por el controller `backofficeCreateTenant` del platform-rbac-business y, a futuro,
18
+ * por el frontend de staff. La `idempotencyKey` viaja en header HTTP (no en el body).
19
+ * El `temporaryPassword` NO viaja en el request: vive solo en el response como fallback
20
+ * out-of-band mientras el correo de invitación no entrega.
21
+ */
22
+ class CreateTenantRequest {
23
+ }
24
+ exports.CreateTenantRequest = CreateTenantRequest;
25
+ __decorate([
26
+ (0, class_transformer_1.Expose)(),
27
+ (0, class_validator_1.IsString)(),
28
+ (0, class_validator_1.Matches)(/^[a-z0-9-]+$/),
29
+ __metadata("design:type", String)
30
+ ], CreateTenantRequest.prototype, "tenantId", void 0);
31
+ __decorate([
32
+ (0, class_transformer_1.Expose)(),
33
+ (0, class_validator_1.IsString)(),
34
+ __metadata("design:type", String)
35
+ ], CreateTenantRequest.prototype, "displayName", void 0);
36
+ __decorate([
37
+ (0, class_transformer_1.Expose)(),
38
+ (0, class_validator_1.IsString)(),
39
+ __metadata("design:type", String)
40
+ ], CreateTenantRequest.prototype, "emailDomain", void 0);
41
+ __decorate([
42
+ (0, class_transformer_1.Expose)(),
43
+ (0, class_validator_1.IsString)(),
44
+ __metadata("design:type", String)
45
+ ], CreateTenantRequest.prototype, "tablePrefix", void 0);
46
+ __decorate([
47
+ (0, class_transformer_1.Expose)(),
48
+ (0, class_validator_1.IsEmail)(),
49
+ __metadata("design:type", String)
50
+ ], CreateTenantRequest.prototype, "adminEmail", void 0);
51
+ __decorate([
52
+ (0, class_transformer_1.Expose)(),
53
+ (0, class_validator_1.IsString)(),
54
+ __metadata("design:type", String)
55
+ ], CreateTenantRequest.prototype, "adminName", void 0);
56
+ __decorate([
57
+ (0, class_transformer_1.Expose)(),
58
+ (0, class_validator_1.IsString)(),
59
+ __metadata("design:type", String)
60
+ ], CreateTenantRequest.prototype, "region", void 0);
61
+ __decorate([
62
+ (0, class_transformer_1.Expose)(),
63
+ (0, class_validator_1.IsOptional)(),
64
+ (0, class_validator_1.IsBoolean)(),
65
+ __metadata("design:type", Boolean)
66
+ ], CreateTenantRequest.prototype, "mfaRequired", void 0);
67
+ __decorate([
68
+ (0, class_transformer_1.Expose)(),
69
+ (0, class_validator_1.IsOptional)(),
70
+ (0, class_validator_1.IsInt)(),
71
+ (0, class_validator_1.Min)(8),
72
+ __metadata("design:type", Number)
73
+ ], CreateTenantRequest.prototype, "passwordMinLength", void 0);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Output del POST backoffice de creación de tenant (F-11). Response plain sin validators
3
+ * (no validamos lo que mandamos al cliente — fiado-validation-and-dtos § 7).
4
+ */
5
+ export interface CreateTenantResponse {
6
+ tenantId: string;
7
+ userPoolId: string;
8
+ appClientId: string;
9
+ adminCognitoSub: string;
10
+ /** Fallback out-of-band mientras el correo de invitación no entrega (F-11). */
11
+ temporaryPassword: string;
12
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -21,3 +21,5 @@ export * from './mfa/MfaStatusResponse';
21
21
  export { AuthorizeDenyReason } from './enums/AuthorizeDenyReason';
22
22
  export * from './dtos/AuthorizeRequest';
23
23
  export * from './dtos/AuthorizeResponse';
24
+ export * from './dtos/CreateTenantRequest';
25
+ export type { CreateTenantResponse } from './dtos/CreateTenantResponse';
@@ -51,3 +51,6 @@ var AuthorizeDenyReason_1 = require("./enums/AuthorizeDenyReason");
51
51
  Object.defineProperty(exports, "AuthorizeDenyReason", { enumerable: true, get: function () { return AuthorizeDenyReason_1.AuthorizeDenyReason; } });
52
52
  __exportStar(require("./dtos/AuthorizeRequest"), exports);
53
53
  __exportStar(require("./dtos/AuthorizeResponse"), exports);
54
+ // F-11 — onboarding de tenant (POST backoffice createTenant). CreateTenantRequest lleva
55
+ // decoradores class-validator → export de valor; CreateTenantResponse es interface → type-only.
56
+ __exportStar(require("./dtos/CreateTenantRequest"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiado/type-kit",
3
- "version": "3.50.0",
3
+ "version": "3.51.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",
@@ -1,6 +1,5 @@
1
1
  import { Expose, Type } from 'class-transformer';
2
2
  import { IsArray, IsString, ValidateNested } from 'class-validator';
3
- import { MfaPoolConfig } from './MfaPoolConfig';
4
3
  import { PasswordPolicyConfig } from './PasswordPolicyConfig';
5
4
  import { CustomAttributeSpec } from './CustomAttributeSpec';
6
5
  import { AppClientConfig } from './AppClientConfig';
@@ -11,14 +10,16 @@ import { AppClientConfig } from './AppClientConfig';
11
10
  * (Flujo 2 v1.2, PASO 4 de la saga). El connector ejecuta CreateUserPool +
12
11
  * CreateUserPoolClient en secuencia. Si CreateUserPoolClient falla DESPUÉS de
13
12
  * CreateUserPool exitoso, el connector hace DeleteUserPool de cleanup.
13
+ *
14
+ * El request NO expone configuración de MFA nativo del pool (F-11 D2): el 2º
15
+ * factor del backoffice va por custom-auth (EMAIL OTP / TOTP), nunca por MFA
16
+ * nativo del pool. El connector fuerza `MfaConfiguration: 'OFF'` como invariante
17
+ * de código al crear el pool — no es negociable vía contrato.
14
18
  */
15
19
  export class CreatePoolRequest {
16
20
  @Expose() @IsString() region!: string;
17
21
  @Expose() @IsString() displayName!: string;
18
22
 
19
- @Expose() @ValidateNested() @Type(() => MfaPoolConfig)
20
- mfaConfig!: MfaPoolConfig;
21
-
22
23
  @Expose() @ValidateNested() @Type(() => PasswordPolicyConfig)
23
24
  passwordPolicy!: PasswordPolicyConfig;
24
25
 
@@ -10,7 +10,6 @@
10
10
  export * from './enums/CognitoChallengeType';
11
11
  export * from './enums/CognitoUserStatus';
12
12
  export * from './validators/NoTenantIdInCustomAttrs';
13
- export * from './validators/MfaTypesRequiresOne';
14
13
  export * from './dtos/CreateUserRequest';
15
14
  export * from './dtos/CreateUserResponse';
16
15
  export * from './dtos/UpdateUserAttributesRequest';
@@ -43,7 +42,6 @@ export * from './dtos/UpdateEmailRequest';
43
42
  export * from './dtos/VerifyEmailRequest';
44
43
  export * from './dtos/UpdateProfileRequest';
45
44
  export * from './dtos/HealthcheckResponse';
46
- export * from './dtos/MfaPoolConfig';
47
45
  export * from './dtos/PasswordPolicyConfig';
48
46
  export * from './dtos/CustomAttributeSpec';
49
47
  export * from './dtos/AppClientConfig';
@@ -0,0 +1,21 @@
1
+ import { Expose } from 'class-transformer';
2
+ import { IsBoolean, IsEmail, IsInt, IsOptional, IsString, Matches, Min } from 'class-validator';
3
+
4
+ /**
5
+ * Input del POST backoffice de creación de tenant (F-11 — onboarding de tenant en SureKeep).
6
+ * Consumido por el controller `backofficeCreateTenant` del platform-rbac-business y, a futuro,
7
+ * por el frontend de staff. La `idempotencyKey` viaja en header HTTP (no en el body).
8
+ * El `temporaryPassword` NO viaja en el request: vive solo en el response como fallback
9
+ * out-of-band mientras el correo de invitación no entrega.
10
+ */
11
+ export class CreateTenantRequest {
12
+ @Expose() @IsString() @Matches(/^[a-z0-9-]+$/) tenantId!: string;
13
+ @Expose() @IsString() displayName!: string;
14
+ @Expose() @IsString() emailDomain!: string;
15
+ @Expose() @IsString() tablePrefix!: string;
16
+ @Expose() @IsEmail() adminEmail!: string;
17
+ @Expose() @IsString() adminName!: string;
18
+ @Expose() @IsString() region!: string;
19
+ @Expose() @IsOptional() @IsBoolean() mfaRequired?: boolean;
20
+ @Expose() @IsOptional() @IsInt() @Min(8) passwordMinLength?: number;
21
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Output del POST backoffice de creación de tenant (F-11). Response plain sin validators
3
+ * (no validamos lo que mandamos al cliente — fiado-validation-and-dtos § 7).
4
+ */
5
+ export interface CreateTenantResponse {
6
+ tenantId: string;
7
+ userPoolId: string;
8
+ appClientId: string;
9
+ adminCognitoSub: string;
10
+ /** Fallback out-of-band mientras el correo de invitación no entrega (F-11). */
11
+ temporaryPassword: string;
12
+ }
@@ -37,3 +37,8 @@ export * from './mfa/MfaStatusResponse';
37
37
  export { AuthorizeDenyReason } from './enums/AuthorizeDenyReason';
38
38
  export * from './dtos/AuthorizeRequest';
39
39
  export * from './dtos/AuthorizeResponse';
40
+
41
+ // F-11 — onboarding de tenant (POST backoffice createTenant). CreateTenantRequest lleva
42
+ // decoradores class-validator → export de valor; CreateTenantResponse es interface → type-only.
43
+ export * from './dtos/CreateTenantRequest';
44
+ export type { CreateTenantResponse } from './dtos/CreateTenantResponse';
@@ -1,77 +0,0 @@
1
- import 'reflect-metadata';
2
- import { plainToInstance } from 'class-transformer';
3
- import { validate } from 'class-validator';
4
- import { MfaPoolConfig } from '../../../src/cognitoBackofficeConnector/dtos/MfaPoolConfig';
5
-
6
- describe('MfaTypesRequiresOne validator (cross-field validation)', () => {
7
- async function validateMfaConfig(data: unknown) {
8
- const instance = plainToInstance(MfaPoolConfig, data, { excludeExtraneousValues: true });
9
- return validate(instance as object, { whitelist: true });
10
- }
11
-
12
- describe('requireMfa: true', () => {
13
- it('mfaTypes con 1 elemento (SOFTWARE_TOKEN_MFA) → válido', async () => {
14
- const errors = await validateMfaConfig({
15
- requireMfa: true,
16
- mfaTypes: ['SOFTWARE_TOKEN_MFA'],
17
- });
18
- expect(errors).toHaveLength(0);
19
- });
20
-
21
- it('mfaTypes con 1 elemento (EMAIL_OTP) → válido', async () => {
22
- const errors = await validateMfaConfig({
23
- requireMfa: true,
24
- mfaTypes: ['EMAIL_OTP'],
25
- });
26
- expect(errors).toHaveLength(0);
27
- });
28
-
29
- it('mfaTypes con ambos elementos → válido (MFA exclusivo A17 se decide por user en login)', async () => {
30
- const errors = await validateMfaConfig({
31
- requireMfa: true,
32
- mfaTypes: ['SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
33
- });
34
- expect(errors).toHaveLength(0);
35
- });
36
-
37
- it('mfaTypes vacío → INVÁLIDO (MfaTypesRequiresOne)', async () => {
38
- const errors = await validateMfaConfig({
39
- requireMfa: true,
40
- mfaTypes: [],
41
- });
42
- expect(errors.length).toBeGreaterThan(0);
43
- const constraints = errors[0]?.constraints ?? {};
44
- expect(Object.keys(constraints)).toContain('MfaTypesRequiresOneWhenMfaRequired');
45
- });
46
- });
47
-
48
- describe('requireMfa: false', () => {
49
- it('mfaTypes vacío → válido (cuando MFA no se requiere, no hace falta tipo)', async () => {
50
- const errors = await validateMfaConfig({
51
- requireMfa: false,
52
- mfaTypes: [],
53
- });
54
- expect(errors).toHaveLength(0);
55
- });
56
-
57
- it('mfaTypes con elementos → válido también (caller decidió pasar tipos pero no requiere MFA — ok)', async () => {
58
- const errors = await validateMfaConfig({
59
- requireMfa: false,
60
- mfaTypes: ['SOFTWARE_TOKEN_MFA'],
61
- });
62
- expect(errors).toHaveLength(0);
63
- });
64
- });
65
-
66
- describe('mfaTypes con valor inválido fuera del enum', () => {
67
- it('mfaTypes con "SMS_MFA" → INVÁLIDO (IsIn rechaza)', async () => {
68
- const errors = await validateMfaConfig({
69
- requireMfa: true,
70
- mfaTypes: ['SMS_MFA'],
71
- });
72
- expect(errors.length).toBeGreaterThan(0);
73
- const constraints = errors[0]?.constraints ?? {};
74
- expect(Object.keys(constraints).join(',')).toMatch(/isIn/i);
75
- });
76
- });
77
- });
@@ -1,21 +0,0 @@
1
- import { Expose } from 'class-transformer';
2
- import { IsArray, IsBoolean, IsIn, Validate } from 'class-validator';
3
- import { MfaTypesRequiresOne } from '../validators/MfaTypesRequiresOne';
4
-
5
- // EMAIL_OTP removido (3.41.0): el cognito-backoffice-connector no provisiona
6
- // EmailMfaConfiguration a nivel pool (requiere infra SES que no está montada
7
- // y la integración con messages-lambda aún no tiene diseño). Ver TD-020 +
8
- // DEC-001 en cognito-backoffice-connector/docs/. Si se reintroduce, agregar
9
- // 'EMAIL_OTP' a este array.
10
- const ALLOWED_MFA_TYPES = ['SOFTWARE_TOKEN_MFA'] as const;
11
- export type AllowedMfaType = (typeof ALLOWED_MFA_TYPES)[number];
12
-
13
- export class MfaPoolConfig {
14
- @Expose() @IsBoolean() requireMfa!: boolean;
15
-
16
- @Expose()
17
- @IsArray()
18
- @IsIn(ALLOWED_MFA_TYPES, { each: true })
19
- @Validate(MfaTypesRequiresOne)
20
- mfaTypes!: AllowedMfaType[];
21
- }
@@ -1,29 +0,0 @@
1
- import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
2
-
3
- /**
4
- * Cross-field validator: si `requireMfa: true`, entonces `mfaTypes` debe tener
5
- * al menos 1 elemento. Si `requireMfa: false`, `mfaTypes` puede ser vacío.
6
- *
7
- * Razón: cuando el pool nace con MFA habilitado, el connector llama
8
- * `SetUserPoolMfaConfigCommand` con la lista de tipos del DTO. Si el array
9
- * llega vacío con `requireMfa: true`, el SDK rechaza con InvalidParameterException
10
- * y el pool queda en estado inconsistente (MfaConfiguration:'ON' sin tipos).
11
- * Mejor rechazar en validación del DTO antes de tocar AWS.
12
- *
13
- * Ver pivote v1.4.1 TD-017 cerrado + spec doc §1 R3.
14
- */
15
- @ValidatorConstraint({ name: 'MfaTypesRequiresOneWhenMfaRequired', async: false })
16
- export class MfaTypesRequiresOne implements ValidatorConstraintInterface {
17
- validate(mfaTypes: unknown, args: ValidationArguments): boolean {
18
- const obj = args.object as { requireMfa?: boolean };
19
- if (obj.requireMfa === true) {
20
- return Array.isArray(mfaTypes) && mfaTypes.length >= 1;
21
- }
22
- // requireMfa: false → cualquier mfaTypes pasa.
23
- return true;
24
- }
25
-
26
- defaultMessage(): string {
27
- return 'mfaTypes requiere al menos un tipo cuando requireMfa=true';
28
- }
29
- }