@excofy/utils 1.0.12 → 1.0.14

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 @@
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,
@@ -644,11 +646,150 @@ function createValidator() {
644
646
  };
645
647
  }
646
648
 
649
+ // src/helpers/generateCode.ts
650
+ var generateCode = (length = 6, alphanumeric = true) => {
651
+ const digits = "0123456789";
652
+ const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
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
+
647
662
  // src/helpers/string.ts
648
663
  var stringUtils = {
649
664
  removeFileExtension: (fileName) => fileName.replace(/\.[^/.]+$/, "")
650
665
  };
651
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
+
652
793
  // src/helpers/slug.ts
653
794
  var slug_exports = {};
654
795
  __export(slug_exports, {
@@ -689,6 +830,8 @@ var toDecimal = (value) => {
689
830
  // Annotate the CommonJS export names for ESM import in node:
690
831
  0 && (module.exports = {
691
832
  createValidator,
833
+ cryptoUtils,
834
+ generateCode,
692
835
  htmlEntityDecode,
693
836
  numberUtils,
694
837
  slugUtils,
package/dist/index.d.cts CHANGED
@@ -85,10 +85,53 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
85
85
  };
86
86
  };
87
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
+
88
103
  declare const stringUtils: {
89
104
  removeFileExtension: (fileName: string) => string;
90
105
  };
91
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
+
92
135
  /**
93
136
  * Generates a unique slug by appending a numeric suffix to the base slug if necessary.
94
137
  *
@@ -151,4 +194,4 @@ declare namespace number {
151
194
  export { number_toCents as toCents, number_toDecimal as toDecimal };
152
195
  }
153
196
 
154
- 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
@@ -85,10 +85,53 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
85
85
  };
86
86
  };
87
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
+
88
103
  declare const stringUtils: {
89
104
  removeFileExtension: (fileName: string) => string;
90
105
  };
91
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
+
92
135
  /**
93
136
  * Generates a unique slug by appending a numeric suffix to the base slug if necessary.
94
137
  *
@@ -151,4 +194,4 @@ declare namespace number {
151
194
  export { number_toCents as toCents, number_toDecimal as toDecimal };
152
195
  }
153
196
 
154
- 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
@@ -610,11 +610,150 @@ function createValidator() {
610
610
  };
611
611
  }
612
612
 
613
+ // src/helpers/generateCode.ts
614
+ var generateCode = (length = 6, alphanumeric = true) => {
615
+ const digits = "0123456789";
616
+ const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
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
+
613
626
  // src/helpers/string.ts
614
627
  var stringUtils = {
615
628
  removeFileExtension: (fileName) => fileName.replace(/\.[^/.]+$/, "")
616
629
  };
617
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
+
618
757
  // src/helpers/slug.ts
619
758
  var slug_exports = {};
620
759
  __export(slug_exports, {
@@ -654,6 +793,8 @@ var toDecimal = (value) => {
654
793
  };
655
794
  export {
656
795
  createValidator,
796
+ cryptoUtils,
797
+ generateCode,
657
798
  htmlEntityDecode,
658
799
  number_exports as numberUtils,
659
800
  slug_exports as slugUtils,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@excofy/utils",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
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/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
+ });
package/tsconfig.json CHANGED
@@ -13,6 +13,6 @@
13
13
  "resolveJsonModule": true,
14
14
  "isolatedModules": true
15
15
  },
16
- "include": ["src"],
16
+ "include": ["src", "tests"],
17
17
  "exclude": ["dist", "node_modules", "**/*.test.ts"]
18
18
  }