@excofy/utils 1.0.11 → 1.0.13
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.
- package/.secrets.sample +1 -0
- package/dist/index.cjs +152 -0
- package/dist/index.d.cts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +150 -0
- package/package.json +4 -2
- package/src/helpers/crypto.ts +202 -0
- package/src/helpers/generateCode.ts +26 -0
- package/src/helpers/validator.ts +13 -0
- package/src/index.ts +4 -0
- package/tests/helpers/webCrypto.spec.ts +126 -0
- package/tsconfig.json +1 -1
package/.secrets.sample
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NPM_TOKEN=
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
createValidator: () => createValidator,
|
|
34
|
+
cryptoUtils: () => cryptoUtils,
|
|
35
|
+
generateCode: () => generateCode,
|
|
34
36
|
htmlEntityDecode: () => htmlEntityDecode,
|
|
35
37
|
numberUtils: () => number_exports,
|
|
36
38
|
slugUtils: () => slug_exports,
|
|
@@ -435,6 +437,15 @@ function createValidator() {
|
|
|
435
437
|
}
|
|
436
438
|
return validator;
|
|
437
439
|
},
|
|
440
|
+
sanitizeAlphaSpaces() {
|
|
441
|
+
if (shouldSkipValidation()) return validator;
|
|
442
|
+
if (typeof current.value === "string") {
|
|
443
|
+
const cleaned = current.value.replace(/[^A-Za-zÀ-ÿ\s]/g, "");
|
|
444
|
+
current.value = cleaned;
|
|
445
|
+
current.inputs[current.field] = cleaned;
|
|
446
|
+
}
|
|
447
|
+
return validator;
|
|
448
|
+
},
|
|
438
449
|
sanitizeDigits() {
|
|
439
450
|
if (shouldSkipValidation()) return validator;
|
|
440
451
|
if (typeof current.value === "string") {
|
|
@@ -635,11 +646,150 @@ function createValidator() {
|
|
|
635
646
|
};
|
|
636
647
|
}
|
|
637
648
|
|
|
649
|
+
// src/helpers/generateCode.ts
|
|
650
|
+
var generateCode = (length = 6, alphanumeric = true) => {
|
|
651
|
+
const digits = "0123456789";
|
|
652
|
+
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
653
|
+
const charset = alphanumeric ? digits + letters : digits;
|
|
654
|
+
let result = "";
|
|
655
|
+
for (let i = 0; i < length; i++) {
|
|
656
|
+
const randomIndex = Math.floor(Math.random() * charset.length);
|
|
657
|
+
result += charset[randomIndex];
|
|
658
|
+
}
|
|
659
|
+
return result;
|
|
660
|
+
};
|
|
661
|
+
|
|
638
662
|
// src/helpers/string.ts
|
|
639
663
|
var stringUtils = {
|
|
640
664
|
removeFileExtension: (fileName) => fileName.replace(/\.[^/.]+$/, "")
|
|
641
665
|
};
|
|
642
666
|
|
|
667
|
+
// src/helpers/crypto.ts
|
|
668
|
+
var encoder = new TextEncoder();
|
|
669
|
+
var decoder = new TextDecoder();
|
|
670
|
+
var toBase64 = (buffer) => btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
671
|
+
var fromBase64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
672
|
+
var signatureKey = async (AUTH_SIGN_SECRET) => crypto.subtle.importKey(
|
|
673
|
+
"raw",
|
|
674
|
+
encoder.encode(AUTH_SIGN_SECRET),
|
|
675
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
676
|
+
false,
|
|
677
|
+
["sign", "verify"]
|
|
678
|
+
);
|
|
679
|
+
var payloadKey = async (AUTH_PAYLOAD_SECRET) => crypto.subtle.importKey(
|
|
680
|
+
"raw",
|
|
681
|
+
encoder.encode(AUTH_PAYLOAD_SECRET.substring(0, 32)),
|
|
682
|
+
{ name: "AES-GCM", length: 256 },
|
|
683
|
+
true,
|
|
684
|
+
["encrypt", "decrypt"]
|
|
685
|
+
);
|
|
686
|
+
var encryptPayload = async ({
|
|
687
|
+
AUTH_PAYLOAD_SECRET,
|
|
688
|
+
payload
|
|
689
|
+
}) => {
|
|
690
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
691
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
692
|
+
const buffer = await crypto.subtle.encrypt(
|
|
693
|
+
{ name: "AES-GCM", iv },
|
|
694
|
+
key,
|
|
695
|
+
encoder.encode(payload)
|
|
696
|
+
);
|
|
697
|
+
return `${toBase64(iv)}.${toBase64(buffer)}`;
|
|
698
|
+
};
|
|
699
|
+
var decryptPayload = async ({
|
|
700
|
+
AUTH_PAYLOAD_SECRET,
|
|
701
|
+
token
|
|
702
|
+
}) => {
|
|
703
|
+
const [header, payload] = token.split(".");
|
|
704
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
705
|
+
const buffer = await crypto.subtle.decrypt(
|
|
706
|
+
{ name: "AES-GCM", iv: fromBase64(header).buffer },
|
|
707
|
+
key,
|
|
708
|
+
fromBase64(payload).buffer
|
|
709
|
+
);
|
|
710
|
+
return decoder.decode(buffer);
|
|
711
|
+
};
|
|
712
|
+
var cryptoUtils = {
|
|
713
|
+
uuidV4: () => crypto.randomUUID(),
|
|
714
|
+
hash: async (password, salt) => {
|
|
715
|
+
const importedKey = await crypto.subtle.importKey(
|
|
716
|
+
"raw",
|
|
717
|
+
encoder.encode(password),
|
|
718
|
+
{ name: "PBKDF2" },
|
|
719
|
+
false,
|
|
720
|
+
["deriveBits"]
|
|
721
|
+
);
|
|
722
|
+
const derivedKey = await crypto.subtle.deriveBits(
|
|
723
|
+
{
|
|
724
|
+
name: "PBKDF2",
|
|
725
|
+
salt: encoder.encode(String(salt)),
|
|
726
|
+
iterations: 1e3,
|
|
727
|
+
hash: "SHA-256"
|
|
728
|
+
},
|
|
729
|
+
importedKey,
|
|
730
|
+
256
|
|
731
|
+
);
|
|
732
|
+
return Array.from(new Uint8Array(derivedKey)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
733
|
+
},
|
|
734
|
+
isMatch: async (password, hash, salt = 10) => {
|
|
735
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
736
|
+
return hashed === hash;
|
|
737
|
+
},
|
|
738
|
+
generateAccessToken: async ({
|
|
739
|
+
AUTH_PAYLOAD_SECRET,
|
|
740
|
+
AUTH_SIGN_SECRET,
|
|
741
|
+
payload
|
|
742
|
+
}) => {
|
|
743
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
744
|
+
const payloadEncrypted = await encryptPayload({
|
|
745
|
+
AUTH_PAYLOAD_SECRET,
|
|
746
|
+
payload
|
|
747
|
+
});
|
|
748
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
749
|
+
"HMAC",
|
|
750
|
+
key,
|
|
751
|
+
encoder.encode(payload)
|
|
752
|
+
);
|
|
753
|
+
const signature = toBase64(signatureBuffer);
|
|
754
|
+
return `${payloadEncrypted}.${signature}`;
|
|
755
|
+
},
|
|
756
|
+
validateAccessToken: async ({
|
|
757
|
+
AUTH_PAYLOAD_SECRET,
|
|
758
|
+
AUTH_SIGN_SECRET,
|
|
759
|
+
accessToken
|
|
760
|
+
}) => {
|
|
761
|
+
let payloadDecrypted;
|
|
762
|
+
try {
|
|
763
|
+
const [header, payload, signature] = accessToken.split(".");
|
|
764
|
+
payloadDecrypted = await decryptPayload({
|
|
765
|
+
AUTH_PAYLOAD_SECRET,
|
|
766
|
+
token: `${header}.${payload}`
|
|
767
|
+
});
|
|
768
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
769
|
+
const valid = await crypto.subtle.verify(
|
|
770
|
+
"HMAC",
|
|
771
|
+
key,
|
|
772
|
+
fromBase64(signature).buffer,
|
|
773
|
+
encoder.encode(payloadDecrypted)
|
|
774
|
+
);
|
|
775
|
+
if (!valid) throw new Error("Invalid access token");
|
|
776
|
+
} catch (err) {
|
|
777
|
+
throw new Error("Invalid access token");
|
|
778
|
+
}
|
|
779
|
+
const { expiresAt } = JSON.parse(payloadDecrypted);
|
|
780
|
+
if (Date.now() > new Date(expiresAt).getTime()) {
|
|
781
|
+
throw new Error("Expired access token");
|
|
782
|
+
}
|
|
783
|
+
return payloadDecrypted;
|
|
784
|
+
},
|
|
785
|
+
sha256ToHex: async (data) => {
|
|
786
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
|
|
787
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
788
|
+
},
|
|
789
|
+
encryptPayload,
|
|
790
|
+
decryptPayload
|
|
791
|
+
};
|
|
792
|
+
|
|
643
793
|
// src/helpers/slug.ts
|
|
644
794
|
var slug_exports = {};
|
|
645
795
|
__export(slug_exports, {
|
|
@@ -680,6 +830,8 @@ var toDecimal = (value) => {
|
|
|
680
830
|
// Annotate the CommonJS export names for ESM import in node:
|
|
681
831
|
0 && (module.exports = {
|
|
682
832
|
createValidator,
|
|
833
|
+
cryptoUtils,
|
|
834
|
+
generateCode,
|
|
683
835
|
htmlEntityDecode,
|
|
684
836
|
numberUtils,
|
|
685
837
|
slugUtils,
|
package/dist/index.d.cts
CHANGED
|
@@ -31,6 +31,7 @@ interface ValidatorField {
|
|
|
31
31
|
transform<U>(fn: (value: TInputValue) => U): ValidatorField;
|
|
32
32
|
slug(message: string): ValidatorField;
|
|
33
33
|
sanitize(): ValidatorField;
|
|
34
|
+
sanitizeAlphaSpaces(): ValidatorField;
|
|
34
35
|
sanitizeDigits(): ValidatorField;
|
|
35
36
|
sanitizePreservingNewlines(): ValidatorField;
|
|
36
37
|
sanitizeHTML(tags?: AllowedTag[]): ValidatorField;
|
|
@@ -84,10 +85,53 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
|
|
|
84
85
|
};
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Gera um código de validação.
|
|
90
|
+
*
|
|
91
|
+
* @param {number} [length=6] - Número de caracteres do código.
|
|
92
|
+
* @param {boolean} [alphanumeric=true] - Se `true`, gera código alfanumérico (letras e números).
|
|
93
|
+
* Se `false`, gera apenas dígitos numéricos.
|
|
94
|
+
* @returns {string} Código gerado.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* generateCode(); // "482901"
|
|
98
|
+
* generateCode(8); // "83910274"
|
|
99
|
+
* generateCode(8, true); // "A9F3D2K7"
|
|
100
|
+
*/
|
|
101
|
+
declare const generateCode: (length?: number, alphanumeric?: boolean) => string;
|
|
102
|
+
|
|
87
103
|
declare const stringUtils: {
|
|
88
104
|
removeFileExtension: (fileName: string) => string;
|
|
89
105
|
};
|
|
90
106
|
|
|
107
|
+
interface IGenerateAccessToken {
|
|
108
|
+
AUTH_SIGN_SECRET: string;
|
|
109
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
110
|
+
payload: string;
|
|
111
|
+
}
|
|
112
|
+
interface IValidateAccessToken {
|
|
113
|
+
AUTH_SIGN_SECRET: string;
|
|
114
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
115
|
+
accessToken: string;
|
|
116
|
+
}
|
|
117
|
+
interface ICrypto {
|
|
118
|
+
uuidV4: () => string;
|
|
119
|
+
hash: (password: string, salt: number) => Promise<string>;
|
|
120
|
+
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
121
|
+
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
122
|
+
validateAccessToken: (data: IValidateAccessToken) => Promise<string>;
|
|
123
|
+
sha256ToHex: (data: string) => Promise<string>;
|
|
124
|
+
encryptPayload: (data: {
|
|
125
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
126
|
+
payload: string;
|
|
127
|
+
}) => Promise<string>;
|
|
128
|
+
decryptPayload: (data: {
|
|
129
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
130
|
+
token: string;
|
|
131
|
+
}) => Promise<string>;
|
|
132
|
+
}
|
|
133
|
+
declare const cryptoUtils: ICrypto;
|
|
134
|
+
|
|
91
135
|
/**
|
|
92
136
|
* Generates a unique slug by appending a numeric suffix to the base slug if necessary.
|
|
93
137
|
*
|
|
@@ -150,4 +194,4 @@ declare namespace number {
|
|
|
150
194
|
export { number_toCents as toCents, number_toDecimal as toDecimal };
|
|
151
195
|
}
|
|
152
196
|
|
|
153
|
-
export { createValidator, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
|
197
|
+
export { createValidator, cryptoUtils, generateCode, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ interface ValidatorField {
|
|
|
31
31
|
transform<U>(fn: (value: TInputValue) => U): ValidatorField;
|
|
32
32
|
slug(message: string): ValidatorField;
|
|
33
33
|
sanitize(): ValidatorField;
|
|
34
|
+
sanitizeAlphaSpaces(): ValidatorField;
|
|
34
35
|
sanitizeDigits(): ValidatorField;
|
|
35
36
|
sanitizePreservingNewlines(): ValidatorField;
|
|
36
37
|
sanitizeHTML(tags?: AllowedTag[]): ValidatorField;
|
|
@@ -84,10 +85,53 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
|
|
|
84
85
|
};
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Gera um código de validação.
|
|
90
|
+
*
|
|
91
|
+
* @param {number} [length=6] - Número de caracteres do código.
|
|
92
|
+
* @param {boolean} [alphanumeric=true] - Se `true`, gera código alfanumérico (letras e números).
|
|
93
|
+
* Se `false`, gera apenas dígitos numéricos.
|
|
94
|
+
* @returns {string} Código gerado.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* generateCode(); // "482901"
|
|
98
|
+
* generateCode(8); // "83910274"
|
|
99
|
+
* generateCode(8, true); // "A9F3D2K7"
|
|
100
|
+
*/
|
|
101
|
+
declare const generateCode: (length?: number, alphanumeric?: boolean) => string;
|
|
102
|
+
|
|
87
103
|
declare const stringUtils: {
|
|
88
104
|
removeFileExtension: (fileName: string) => string;
|
|
89
105
|
};
|
|
90
106
|
|
|
107
|
+
interface IGenerateAccessToken {
|
|
108
|
+
AUTH_SIGN_SECRET: string;
|
|
109
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
110
|
+
payload: string;
|
|
111
|
+
}
|
|
112
|
+
interface IValidateAccessToken {
|
|
113
|
+
AUTH_SIGN_SECRET: string;
|
|
114
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
115
|
+
accessToken: string;
|
|
116
|
+
}
|
|
117
|
+
interface ICrypto {
|
|
118
|
+
uuidV4: () => string;
|
|
119
|
+
hash: (password: string, salt: number) => Promise<string>;
|
|
120
|
+
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
121
|
+
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
122
|
+
validateAccessToken: (data: IValidateAccessToken) => Promise<string>;
|
|
123
|
+
sha256ToHex: (data: string) => Promise<string>;
|
|
124
|
+
encryptPayload: (data: {
|
|
125
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
126
|
+
payload: string;
|
|
127
|
+
}) => Promise<string>;
|
|
128
|
+
decryptPayload: (data: {
|
|
129
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
130
|
+
token: string;
|
|
131
|
+
}) => Promise<string>;
|
|
132
|
+
}
|
|
133
|
+
declare const cryptoUtils: ICrypto;
|
|
134
|
+
|
|
91
135
|
/**
|
|
92
136
|
* Generates a unique slug by appending a numeric suffix to the base slug if necessary.
|
|
93
137
|
*
|
|
@@ -150,4 +194,4 @@ declare namespace number {
|
|
|
150
194
|
export { number_toCents as toCents, number_toDecimal as toDecimal };
|
|
151
195
|
}
|
|
152
196
|
|
|
153
|
-
export { createValidator, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
|
197
|
+
export { createValidator, cryptoUtils, generateCode, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
package/dist/index.js
CHANGED
|
@@ -401,6 +401,15 @@ function createValidator() {
|
|
|
401
401
|
}
|
|
402
402
|
return validator;
|
|
403
403
|
},
|
|
404
|
+
sanitizeAlphaSpaces() {
|
|
405
|
+
if (shouldSkipValidation()) return validator;
|
|
406
|
+
if (typeof current.value === "string") {
|
|
407
|
+
const cleaned = current.value.replace(/[^A-Za-zÀ-ÿ\s]/g, "");
|
|
408
|
+
current.value = cleaned;
|
|
409
|
+
current.inputs[current.field] = cleaned;
|
|
410
|
+
}
|
|
411
|
+
return validator;
|
|
412
|
+
},
|
|
404
413
|
sanitizeDigits() {
|
|
405
414
|
if (shouldSkipValidation()) return validator;
|
|
406
415
|
if (typeof current.value === "string") {
|
|
@@ -601,11 +610,150 @@ function createValidator() {
|
|
|
601
610
|
};
|
|
602
611
|
}
|
|
603
612
|
|
|
613
|
+
// src/helpers/generateCode.ts
|
|
614
|
+
var generateCode = (length = 6, alphanumeric = true) => {
|
|
615
|
+
const digits = "0123456789";
|
|
616
|
+
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
617
|
+
const charset = alphanumeric ? digits + letters : digits;
|
|
618
|
+
let result = "";
|
|
619
|
+
for (let i = 0; i < length; i++) {
|
|
620
|
+
const randomIndex = Math.floor(Math.random() * charset.length);
|
|
621
|
+
result += charset[randomIndex];
|
|
622
|
+
}
|
|
623
|
+
return result;
|
|
624
|
+
};
|
|
625
|
+
|
|
604
626
|
// src/helpers/string.ts
|
|
605
627
|
var stringUtils = {
|
|
606
628
|
removeFileExtension: (fileName) => fileName.replace(/\.[^/.]+$/, "")
|
|
607
629
|
};
|
|
608
630
|
|
|
631
|
+
// src/helpers/crypto.ts
|
|
632
|
+
var encoder = new TextEncoder();
|
|
633
|
+
var decoder = new TextDecoder();
|
|
634
|
+
var toBase64 = (buffer) => btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
635
|
+
var fromBase64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
636
|
+
var signatureKey = async (AUTH_SIGN_SECRET) => crypto.subtle.importKey(
|
|
637
|
+
"raw",
|
|
638
|
+
encoder.encode(AUTH_SIGN_SECRET),
|
|
639
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
640
|
+
false,
|
|
641
|
+
["sign", "verify"]
|
|
642
|
+
);
|
|
643
|
+
var payloadKey = async (AUTH_PAYLOAD_SECRET) => crypto.subtle.importKey(
|
|
644
|
+
"raw",
|
|
645
|
+
encoder.encode(AUTH_PAYLOAD_SECRET.substring(0, 32)),
|
|
646
|
+
{ name: "AES-GCM", length: 256 },
|
|
647
|
+
true,
|
|
648
|
+
["encrypt", "decrypt"]
|
|
649
|
+
);
|
|
650
|
+
var encryptPayload = async ({
|
|
651
|
+
AUTH_PAYLOAD_SECRET,
|
|
652
|
+
payload
|
|
653
|
+
}) => {
|
|
654
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
655
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
656
|
+
const buffer = await crypto.subtle.encrypt(
|
|
657
|
+
{ name: "AES-GCM", iv },
|
|
658
|
+
key,
|
|
659
|
+
encoder.encode(payload)
|
|
660
|
+
);
|
|
661
|
+
return `${toBase64(iv)}.${toBase64(buffer)}`;
|
|
662
|
+
};
|
|
663
|
+
var decryptPayload = async ({
|
|
664
|
+
AUTH_PAYLOAD_SECRET,
|
|
665
|
+
token
|
|
666
|
+
}) => {
|
|
667
|
+
const [header, payload] = token.split(".");
|
|
668
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
669
|
+
const buffer = await crypto.subtle.decrypt(
|
|
670
|
+
{ name: "AES-GCM", iv: fromBase64(header).buffer },
|
|
671
|
+
key,
|
|
672
|
+
fromBase64(payload).buffer
|
|
673
|
+
);
|
|
674
|
+
return decoder.decode(buffer);
|
|
675
|
+
};
|
|
676
|
+
var cryptoUtils = {
|
|
677
|
+
uuidV4: () => crypto.randomUUID(),
|
|
678
|
+
hash: async (password, salt) => {
|
|
679
|
+
const importedKey = await crypto.subtle.importKey(
|
|
680
|
+
"raw",
|
|
681
|
+
encoder.encode(password),
|
|
682
|
+
{ name: "PBKDF2" },
|
|
683
|
+
false,
|
|
684
|
+
["deriveBits"]
|
|
685
|
+
);
|
|
686
|
+
const derivedKey = await crypto.subtle.deriveBits(
|
|
687
|
+
{
|
|
688
|
+
name: "PBKDF2",
|
|
689
|
+
salt: encoder.encode(String(salt)),
|
|
690
|
+
iterations: 1e3,
|
|
691
|
+
hash: "SHA-256"
|
|
692
|
+
},
|
|
693
|
+
importedKey,
|
|
694
|
+
256
|
|
695
|
+
);
|
|
696
|
+
return Array.from(new Uint8Array(derivedKey)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
697
|
+
},
|
|
698
|
+
isMatch: async (password, hash, salt = 10) => {
|
|
699
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
700
|
+
return hashed === hash;
|
|
701
|
+
},
|
|
702
|
+
generateAccessToken: async ({
|
|
703
|
+
AUTH_PAYLOAD_SECRET,
|
|
704
|
+
AUTH_SIGN_SECRET,
|
|
705
|
+
payload
|
|
706
|
+
}) => {
|
|
707
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
708
|
+
const payloadEncrypted = await encryptPayload({
|
|
709
|
+
AUTH_PAYLOAD_SECRET,
|
|
710
|
+
payload
|
|
711
|
+
});
|
|
712
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
713
|
+
"HMAC",
|
|
714
|
+
key,
|
|
715
|
+
encoder.encode(payload)
|
|
716
|
+
);
|
|
717
|
+
const signature = toBase64(signatureBuffer);
|
|
718
|
+
return `${payloadEncrypted}.${signature}`;
|
|
719
|
+
},
|
|
720
|
+
validateAccessToken: async ({
|
|
721
|
+
AUTH_PAYLOAD_SECRET,
|
|
722
|
+
AUTH_SIGN_SECRET,
|
|
723
|
+
accessToken
|
|
724
|
+
}) => {
|
|
725
|
+
let payloadDecrypted;
|
|
726
|
+
try {
|
|
727
|
+
const [header, payload, signature] = accessToken.split(".");
|
|
728
|
+
payloadDecrypted = await decryptPayload({
|
|
729
|
+
AUTH_PAYLOAD_SECRET,
|
|
730
|
+
token: `${header}.${payload}`
|
|
731
|
+
});
|
|
732
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
733
|
+
const valid = await crypto.subtle.verify(
|
|
734
|
+
"HMAC",
|
|
735
|
+
key,
|
|
736
|
+
fromBase64(signature).buffer,
|
|
737
|
+
encoder.encode(payloadDecrypted)
|
|
738
|
+
);
|
|
739
|
+
if (!valid) throw new Error("Invalid access token");
|
|
740
|
+
} catch (err) {
|
|
741
|
+
throw new Error("Invalid access token");
|
|
742
|
+
}
|
|
743
|
+
const { expiresAt } = JSON.parse(payloadDecrypted);
|
|
744
|
+
if (Date.now() > new Date(expiresAt).getTime()) {
|
|
745
|
+
throw new Error("Expired access token");
|
|
746
|
+
}
|
|
747
|
+
return payloadDecrypted;
|
|
748
|
+
},
|
|
749
|
+
sha256ToHex: async (data) => {
|
|
750
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
|
|
751
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
752
|
+
},
|
|
753
|
+
encryptPayload,
|
|
754
|
+
decryptPayload
|
|
755
|
+
};
|
|
756
|
+
|
|
609
757
|
// src/helpers/slug.ts
|
|
610
758
|
var slug_exports = {};
|
|
611
759
|
__export(slug_exports, {
|
|
@@ -645,6 +793,8 @@ var toDecimal = (value) => {
|
|
|
645
793
|
};
|
|
646
794
|
export {
|
|
647
795
|
createValidator,
|
|
796
|
+
cryptoUtils,
|
|
797
|
+
generateCode,
|
|
648
798
|
htmlEntityDecode,
|
|
649
799
|
number_exports as numberUtils,
|
|
650
800
|
slug_exports as slugUtils,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@excofy/utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Biblioteca de utilitários para o Excofy",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"lint": "biome lint",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
|
-
"deploy": "act"
|
|
18
|
+
"deploy": "act",
|
|
19
|
+
"test": "tsx --test 'tests/**/*.spec.ts' --testTimeout=10000"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|
|
21
22
|
"type": "git",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"@types/big.js": "^6.2.2",
|
|
34
35
|
"@types/node": "^24.0.3",
|
|
35
36
|
"tsup": "^8.5.0",
|
|
37
|
+
"tsx": "^4.20.4",
|
|
36
38
|
"typescript": "^5.8.3"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
interface IGenerateAccessToken {
|
|
2
|
+
AUTH_SIGN_SECRET: string;
|
|
3
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
4
|
+
payload: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface IValidateAccessToken {
|
|
8
|
+
AUTH_SIGN_SECRET: string;
|
|
9
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
10
|
+
accessToken: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ICrypto {
|
|
14
|
+
uuidV4: () => string;
|
|
15
|
+
hash: (password: string, salt: number) => Promise<string>;
|
|
16
|
+
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
17
|
+
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
18
|
+
validateAccessToken: (data: IValidateAccessToken) => Promise<string>;
|
|
19
|
+
sha256ToHex: (data: string) => Promise<string>;
|
|
20
|
+
encryptPayload: (data: {
|
|
21
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
22
|
+
payload: string;
|
|
23
|
+
}) => Promise<string>;
|
|
24
|
+
decryptPayload: (data: {
|
|
25
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
26
|
+
token: string;
|
|
27
|
+
}) => Promise<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* --------------------- Helpers básicos --------------------- */
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
|
|
34
|
+
const toBase64 = (buffer: ArrayBuffer | Uint8Array): string =>
|
|
35
|
+
btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
36
|
+
|
|
37
|
+
const fromBase64 = (str: string): Uint8Array =>
|
|
38
|
+
Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
39
|
+
|
|
40
|
+
const signatureKey = async (AUTH_SIGN_SECRET: string) =>
|
|
41
|
+
crypto.subtle.importKey(
|
|
42
|
+
'raw',
|
|
43
|
+
encoder.encode(AUTH_SIGN_SECRET),
|
|
44
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
45
|
+
false,
|
|
46
|
+
['sign', 'verify']
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const payloadKey = async (AUTH_PAYLOAD_SECRET: string) =>
|
|
50
|
+
crypto.subtle.importKey(
|
|
51
|
+
'raw',
|
|
52
|
+
encoder.encode(AUTH_PAYLOAD_SECRET.substring(0, 32)),
|
|
53
|
+
{ name: 'AES-GCM', length: 256 },
|
|
54
|
+
true,
|
|
55
|
+
['encrypt', 'decrypt']
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/* --------------------- Payload (encrypt/decrypt) --------------------- */
|
|
59
|
+
const encryptPayload = async ({
|
|
60
|
+
AUTH_PAYLOAD_SECRET,
|
|
61
|
+
payload,
|
|
62
|
+
}: {
|
|
63
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
64
|
+
payload: string;
|
|
65
|
+
}): Promise<string> => {
|
|
66
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
67
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
68
|
+
|
|
69
|
+
const buffer = await crypto.subtle.encrypt(
|
|
70
|
+
{ name: 'AES-GCM', iv },
|
|
71
|
+
key,
|
|
72
|
+
encoder.encode(payload)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return `${toBase64(iv)}.${toBase64(buffer)}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const decryptPayload = async ({
|
|
79
|
+
AUTH_PAYLOAD_SECRET,
|
|
80
|
+
token,
|
|
81
|
+
}: {
|
|
82
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
83
|
+
token: string;
|
|
84
|
+
}): Promise<string> => {
|
|
85
|
+
const [header, payload] = token.split('.');
|
|
86
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
87
|
+
|
|
88
|
+
const buffer = await crypto.subtle.decrypt(
|
|
89
|
+
{ name: 'AES-GCM', iv: fromBase64(header).buffer as ArrayBuffer },
|
|
90
|
+
key,
|
|
91
|
+
fromBase64(payload).buffer as ArrayBuffer
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return decoder.decode(buffer);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/* --------------------- Main API --------------------- */
|
|
98
|
+
export const cryptoUtils: ICrypto = {
|
|
99
|
+
uuidV4: (): string => crypto.randomUUID(),
|
|
100
|
+
|
|
101
|
+
hash: async (password: string, salt: number): Promise<string> => {
|
|
102
|
+
const importedKey = await crypto.subtle.importKey(
|
|
103
|
+
'raw',
|
|
104
|
+
encoder.encode(password),
|
|
105
|
+
{ name: 'PBKDF2' },
|
|
106
|
+
false,
|
|
107
|
+
['deriveBits']
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const derivedKey = await crypto.subtle.deriveBits(
|
|
111
|
+
{
|
|
112
|
+
name: 'PBKDF2',
|
|
113
|
+
salt: encoder.encode(String(salt)),
|
|
114
|
+
iterations: 1000,
|
|
115
|
+
hash: 'SHA-256',
|
|
116
|
+
},
|
|
117
|
+
importedKey,
|
|
118
|
+
256
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return Array.from(new Uint8Array(derivedKey))
|
|
122
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
123
|
+
.join('');
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
isMatch: async (password, hash, salt = 10): Promise<boolean> => {
|
|
127
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
128
|
+
return hashed === hash;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
generateAccessToken: async ({
|
|
132
|
+
AUTH_PAYLOAD_SECRET,
|
|
133
|
+
AUTH_SIGN_SECRET,
|
|
134
|
+
payload,
|
|
135
|
+
}) => {
|
|
136
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
137
|
+
|
|
138
|
+
// cifra payload
|
|
139
|
+
const payloadEncrypted = await encryptPayload({
|
|
140
|
+
AUTH_PAYLOAD_SECRET,
|
|
141
|
+
payload,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// assinatura
|
|
145
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
146
|
+
'HMAC',
|
|
147
|
+
key,
|
|
148
|
+
encoder.encode(payload)
|
|
149
|
+
);
|
|
150
|
+
const signature = toBase64(signatureBuffer);
|
|
151
|
+
|
|
152
|
+
return `${payloadEncrypted}.${signature}`;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
validateAccessToken: async ({
|
|
156
|
+
AUTH_PAYLOAD_SECRET,
|
|
157
|
+
AUTH_SIGN_SECRET,
|
|
158
|
+
accessToken,
|
|
159
|
+
}) => {
|
|
160
|
+
let payloadDecrypted: string;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const [header, payload, signature] = accessToken.split('.');
|
|
164
|
+
|
|
165
|
+
payloadDecrypted = await decryptPayload({
|
|
166
|
+
AUTH_PAYLOAD_SECRET,
|
|
167
|
+
token: `${header}.${payload}`,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
171
|
+
const valid = await crypto.subtle.verify(
|
|
172
|
+
'HMAC',
|
|
173
|
+
key,
|
|
174
|
+
fromBase64(signature).buffer as ArrayBuffer,
|
|
175
|
+
encoder.encode(payloadDecrypted)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!valid) throw new Error('Invalid access token');
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// qualquer falha na decriptação ou verificação -> token inválido
|
|
181
|
+
throw new Error('Invalid access token');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// verifica expiração **fora do try/catch**, para não ser capturada como token inválido
|
|
185
|
+
const { expiresAt } = JSON.parse(payloadDecrypted);
|
|
186
|
+
if (Date.now() > new Date(expiresAt).getTime()) {
|
|
187
|
+
throw new Error('Expired access token');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return payloadDecrypted;
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
sha256ToHex: async (data: string): Promise<string> => {
|
|
194
|
+
const buffer = await crypto.subtle.digest('SHA-256', encoder.encode(data));
|
|
195
|
+
return Array.from(new Uint8Array(buffer))
|
|
196
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
197
|
+
.join('');
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
encryptPayload,
|
|
201
|
+
decryptPayload,
|
|
202
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gera um código de validação.
|
|
3
|
+
*
|
|
4
|
+
* @param {number} [length=6] - Número de caracteres do código.
|
|
5
|
+
* @param {boolean} [alphanumeric=true] - Se `true`, gera código alfanumérico (letras e números).
|
|
6
|
+
* Se `false`, gera apenas dígitos numéricos.
|
|
7
|
+
* @returns {string} Código gerado.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* generateCode(); // "482901"
|
|
11
|
+
* generateCode(8); // "83910274"
|
|
12
|
+
* generateCode(8, true); // "A9F3D2K7"
|
|
13
|
+
*/
|
|
14
|
+
export const generateCode = (length = 6, alphanumeric = true): string => {
|
|
15
|
+
const digits = '0123456789';
|
|
16
|
+
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
17
|
+
const charset = alphanumeric ? digits + letters : digits;
|
|
18
|
+
|
|
19
|
+
let result = '';
|
|
20
|
+
for (let i = 0; i < length; i++) {
|
|
21
|
+
const randomIndex = Math.floor(Math.random() * charset.length);
|
|
22
|
+
result += charset[randomIndex];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
};
|
package/src/helpers/validator.ts
CHANGED
|
@@ -38,6 +38,7 @@ interface ValidatorField {
|
|
|
38
38
|
transform<U>(fn: (value: TInputValue) => U): ValidatorField;
|
|
39
39
|
slug(message: string): ValidatorField;
|
|
40
40
|
sanitize(): ValidatorField;
|
|
41
|
+
sanitizeAlphaSpaces(): ValidatorField;
|
|
41
42
|
sanitizeDigits(): ValidatorField;
|
|
42
43
|
sanitizePreservingNewlines(): ValidatorField;
|
|
43
44
|
sanitizeHTML(tags?: AllowedTag[]): ValidatorField;
|
|
@@ -408,6 +409,18 @@ export function createValidator<
|
|
|
408
409
|
return validator;
|
|
409
410
|
},
|
|
410
411
|
|
|
412
|
+
sanitizeAlphaSpaces() {
|
|
413
|
+
if (shouldSkipValidation()) return validator;
|
|
414
|
+
|
|
415
|
+
if (typeof current.value === 'string') {
|
|
416
|
+
const cleaned = current.value.replace(/[^A-Za-zÀ-ÿ\s]/g, '');
|
|
417
|
+
current.value = cleaned;
|
|
418
|
+
current.inputs[current.field] = cleaned as TRaw[keyof TRaw];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return validator;
|
|
422
|
+
},
|
|
423
|
+
|
|
411
424
|
sanitizeDigits() {
|
|
412
425
|
if (shouldSkipValidation()) return validator;
|
|
413
426
|
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createValidator } from './helpers/validator';
|
|
2
2
|
import { htmlEntityDecode } from './helpers/sanitize';
|
|
3
|
+
import { generateCode } from './helpers/generateCode';
|
|
3
4
|
import { stringUtils } from './helpers/string';
|
|
5
|
+
import { cryptoUtils } from './helpers/crypto';
|
|
4
6
|
import * as slugUtils from './helpers/slug';
|
|
5
7
|
import * as numberUtils from './helpers/number';
|
|
6
8
|
|
|
@@ -10,4 +12,6 @@ export {
|
|
|
10
12
|
slugUtils,
|
|
11
13
|
numberUtils,
|
|
12
14
|
stringUtils,
|
|
15
|
+
cryptoUtils,
|
|
16
|
+
generateCode,
|
|
13
17
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { cryptoUtils } from '../../src/helpers/crypto';
|
|
4
|
+
|
|
5
|
+
const AUTH_SIGN_SECRET = '12345678901234567890123456789012'; // 32 chars
|
|
6
|
+
const AUTH_PAYLOAD_SECRET = 'abcdefghijklmnopqrstuvxz12345678'; // 32 chars
|
|
7
|
+
|
|
8
|
+
describe('cryptoUtils helper', () => {
|
|
9
|
+
it('uuidV4 deve gerar um UUID válido', () => {
|
|
10
|
+
const id = cryptoUtils.uuidV4();
|
|
11
|
+
assert.match(
|
|
12
|
+
id,
|
|
13
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('hash e isMatch devem funcionar corretamente', async () => {
|
|
18
|
+
const password = 'mypassword';
|
|
19
|
+
const salt = 1234;
|
|
20
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
21
|
+
|
|
22
|
+
assert.equal(await cryptoUtils.isMatch(password, hashed, salt), true);
|
|
23
|
+
assert.equal(
|
|
24
|
+
await cryptoUtils.isMatch('wrongpassword', hashed, salt),
|
|
25
|
+
false
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('encryptPayload e decryptPayload devem ser inversos', async () => {
|
|
30
|
+
const payload = JSON.stringify({ foo: 'bar' });
|
|
31
|
+
|
|
32
|
+
const encrypted = await cryptoUtils.encryptPayload({
|
|
33
|
+
AUTH_PAYLOAD_SECRET,
|
|
34
|
+
payload,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const decrypted = await cryptoUtils.decryptPayload({
|
|
38
|
+
AUTH_PAYLOAD_SECRET,
|
|
39
|
+
token: encrypted,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.equal(decrypted, payload);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sha256ToHex deve gerar hash esperado', async () => {
|
|
46
|
+
const data = 'hello';
|
|
47
|
+
const hash = await cryptoUtils.sha256ToHex(data);
|
|
48
|
+
|
|
49
|
+
// hash SHA-256 conhecido de "hello"
|
|
50
|
+
assert.equal(
|
|
51
|
+
hash,
|
|
52
|
+
'2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('generateAccessToken e validateAccessToken devem funcionar', async () => {
|
|
57
|
+
const payload = JSON.stringify({
|
|
58
|
+
userId: 42,
|
|
59
|
+
expiresAt: new Date(Date.now() + 1000 * 60).toISOString(), // +1 min
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const token = await cryptoUtils.generateAccessToken({
|
|
63
|
+
AUTH_SIGN_SECRET,
|
|
64
|
+
AUTH_PAYLOAD_SECRET,
|
|
65
|
+
payload,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const decrypted = await cryptoUtils.validateAccessToken({
|
|
69
|
+
AUTH_SIGN_SECRET,
|
|
70
|
+
AUTH_PAYLOAD_SECRET,
|
|
71
|
+
accessToken: token,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.equal(decrypted, payload);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('validateAccessToken deve falhar com token expirado', async () => {
|
|
78
|
+
const payload = JSON.stringify({
|
|
79
|
+
userId: 42,
|
|
80
|
+
expiresAt: new Date(Date.now() - 1000).toISOString(), // já expirado
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const token = await cryptoUtils.generateAccessToken({
|
|
84
|
+
AUTH_SIGN_SECRET,
|
|
85
|
+
AUTH_PAYLOAD_SECRET,
|
|
86
|
+
payload,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await assert.rejects(
|
|
90
|
+
() =>
|
|
91
|
+
cryptoUtils.validateAccessToken({
|
|
92
|
+
AUTH_SIGN_SECRET,
|
|
93
|
+
AUTH_PAYLOAD_SECRET,
|
|
94
|
+
accessToken: token,
|
|
95
|
+
}),
|
|
96
|
+
/Expired access token/
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('validateAccessToken deve falhar com assinatura inválida', async () => {
|
|
101
|
+
const payload = JSON.stringify({
|
|
102
|
+
userId: 42,
|
|
103
|
+
expiresAt: new Date(Date.now() + 1000 * 60).toISOString(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const token = await cryptoUtils.generateAccessToken({
|
|
107
|
+
AUTH_SIGN_SECRET,
|
|
108
|
+
AUTH_PAYLOAD_SECRET,
|
|
109
|
+
payload,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// altera assinatura (última parte do token)
|
|
113
|
+
const [header, body, signature] = token.split('.');
|
|
114
|
+
const tamperedToken = `${header}.${body}.${signature.slice(1)}A`;
|
|
115
|
+
|
|
116
|
+
await assert.rejects(
|
|
117
|
+
() =>
|
|
118
|
+
cryptoUtils.validateAccessToken({
|
|
119
|
+
AUTH_SIGN_SECRET,
|
|
120
|
+
AUTH_PAYLOAD_SECRET,
|
|
121
|
+
accessToken: tamperedToken,
|
|
122
|
+
}),
|
|
123
|
+
/Invalid access token/
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|