@excofy/utils 2.4.0 → 2.5.1
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/dist/index.cjs +15 -0
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -0
- package/package.json +2 -2
- package/src/helpers/crypto.ts +30 -12
- package/src/index.ts +5 -1
- package/tests/helpers/validator.spec.ts +75 -0
- package/tsconfig.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
cryptoUtils: () => cryptoUtils,
|
|
37
37
|
generateCode: () => generateCode,
|
|
38
38
|
htmlEntityDecode: () => htmlEntityDecode,
|
|
39
|
+
htmlToPlainText: () => sanitizeValue,
|
|
39
40
|
numberUtils: () => number_exports,
|
|
40
41
|
slugUtils: () => slug_exports,
|
|
41
42
|
stringUtils: () => stringUtils
|
|
@@ -809,6 +810,19 @@ async function verifyDeterministicHash(inputCode, storedHash, secret) {
|
|
|
809
810
|
}
|
|
810
811
|
var cryptoUtils = {
|
|
811
812
|
uuidV4: () => crypto.randomUUID(),
|
|
813
|
+
ulid: () => {
|
|
814
|
+
function randomChar() {
|
|
815
|
+
const chars = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
816
|
+
return chars[Math.floor(Math.random() * chars.length)];
|
|
817
|
+
}
|
|
818
|
+
const timestamp = Date.now();
|
|
819
|
+
const timestampStr = timestamp.toString(36).toUpperCase().padStart(10, "0");
|
|
820
|
+
let randomPart = "";
|
|
821
|
+
for (let i = 0; i < 16; i++) {
|
|
822
|
+
randomPart += randomChar();
|
|
823
|
+
}
|
|
824
|
+
return timestampStr + randomPart;
|
|
825
|
+
},
|
|
812
826
|
hash: async (password, salt = 10) => {
|
|
813
827
|
const importedKey = await crypto.subtle.importKey(
|
|
814
828
|
"raw",
|
|
@@ -960,6 +974,7 @@ var divide = (numerator, denominator) => {
|
|
|
960
974
|
cryptoUtils,
|
|
961
975
|
generateCode,
|
|
962
976
|
htmlEntityDecode,
|
|
977
|
+
htmlToPlainText,
|
|
963
978
|
numberUtils,
|
|
964
979
|
slugUtils,
|
|
965
980
|
stringUtils
|
package/dist/index.d.cts
CHANGED
|
@@ -6,6 +6,7 @@ type AllowedTag = Tag | {
|
|
|
6
6
|
tag: Tag;
|
|
7
7
|
attributes?: Attributes[];
|
|
8
8
|
};
|
|
9
|
+
declare function sanitizeValue(value: string, allowedTags?: AllowedTag[]): string;
|
|
9
10
|
declare function htmlEntityDecode(str?: string): string;
|
|
10
11
|
|
|
11
12
|
type TMessage = {
|
|
@@ -122,6 +123,7 @@ interface IValidateAccessToken {
|
|
|
122
123
|
}
|
|
123
124
|
interface ICrypto {
|
|
124
125
|
uuidV4: () => string;
|
|
126
|
+
ulid: () => string;
|
|
125
127
|
hash: (password: string, salt: number) => Promise<string>;
|
|
126
128
|
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
127
129
|
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
@@ -240,4 +242,4 @@ declare class ExpiredTokenError extends CryptoError {
|
|
|
240
242
|
constructor(message?: string);
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
export { ExpiredTokenError, InvalidTokenError, createValidator, cryptoUtils, generateCode, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
|
245
|
+
export { ExpiredTokenError, InvalidTokenError, createValidator, cryptoUtils, generateCode, htmlEntityDecode, sanitizeValue as htmlToPlainText, number as numberUtils, slug as slugUtils, stringUtils };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ type AllowedTag = Tag | {
|
|
|
6
6
|
tag: Tag;
|
|
7
7
|
attributes?: Attributes[];
|
|
8
8
|
};
|
|
9
|
+
declare function sanitizeValue(value: string, allowedTags?: AllowedTag[]): string;
|
|
9
10
|
declare function htmlEntityDecode(str?: string): string;
|
|
10
11
|
|
|
11
12
|
type TMessage = {
|
|
@@ -122,6 +123,7 @@ interface IValidateAccessToken {
|
|
|
122
123
|
}
|
|
123
124
|
interface ICrypto {
|
|
124
125
|
uuidV4: () => string;
|
|
126
|
+
ulid: () => string;
|
|
125
127
|
hash: (password: string, salt: number) => Promise<string>;
|
|
126
128
|
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
127
129
|
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
@@ -240,4 +242,4 @@ declare class ExpiredTokenError extends CryptoError {
|
|
|
240
242
|
constructor(message?: string);
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
export { ExpiredTokenError, InvalidTokenError, createValidator, cryptoUtils, generateCode, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
|
245
|
+
export { ExpiredTokenError, InvalidTokenError, createValidator, cryptoUtils, generateCode, htmlEntityDecode, sanitizeValue as htmlToPlainText, number as numberUtils, slug as slugUtils, stringUtils };
|
package/dist/index.js
CHANGED
|
@@ -771,6 +771,19 @@ async function verifyDeterministicHash(inputCode, storedHash, secret) {
|
|
|
771
771
|
}
|
|
772
772
|
var cryptoUtils = {
|
|
773
773
|
uuidV4: () => crypto.randomUUID(),
|
|
774
|
+
ulid: () => {
|
|
775
|
+
function randomChar() {
|
|
776
|
+
const chars = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
777
|
+
return chars[Math.floor(Math.random() * chars.length)];
|
|
778
|
+
}
|
|
779
|
+
const timestamp = Date.now();
|
|
780
|
+
const timestampStr = timestamp.toString(36).toUpperCase().padStart(10, "0");
|
|
781
|
+
let randomPart = "";
|
|
782
|
+
for (let i = 0; i < 16; i++) {
|
|
783
|
+
randomPart += randomChar();
|
|
784
|
+
}
|
|
785
|
+
return timestampStr + randomPart;
|
|
786
|
+
},
|
|
774
787
|
hash: async (password, salt = 10) => {
|
|
775
788
|
const importedKey = await crypto.subtle.importKey(
|
|
776
789
|
"raw",
|
|
@@ -921,6 +934,7 @@ export {
|
|
|
921
934
|
cryptoUtils,
|
|
922
935
|
generateCode,
|
|
923
936
|
htmlEntityDecode,
|
|
937
|
+
sanitizeValue as htmlToPlainText,
|
|
924
938
|
number_exports as numberUtils,
|
|
925
939
|
slug_exports as slugUtils,
|
|
926
940
|
stringUtils
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@excofy/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "Biblioteca de utilitários para o Excofy",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@biomejs/biome": "^1.9.4",
|
|
34
34
|
"@types/big.js": "^6.2.2",
|
|
35
|
-
"@types/node": "^24.
|
|
35
|
+
"@types/node": "^24.10.13",
|
|
36
36
|
"tsup": "^8.5.0",
|
|
37
37
|
"tsx": "^4.20.4",
|
|
38
38
|
"typescript": "^5.8.3"
|
package/src/helpers/crypto.ts
CHANGED
|
@@ -17,6 +17,7 @@ interface IValidateAccessToken {
|
|
|
17
17
|
|
|
18
18
|
interface ICrypto {
|
|
19
19
|
uuidV4: () => string;
|
|
20
|
+
ulid: () => string;
|
|
20
21
|
hash: (password: string, salt: number) => Promise<string>;
|
|
21
22
|
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
22
23
|
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
@@ -34,7 +35,7 @@ interface ICrypto {
|
|
|
34
35
|
verifyDeterministicHash: (
|
|
35
36
|
inputCode: string,
|
|
36
37
|
storedHash: string,
|
|
37
|
-
secret: string
|
|
38
|
+
secret: string,
|
|
38
39
|
) => Promise<boolean>;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -54,7 +55,7 @@ const signatureKey = async (AUTH_SIGN_SECRET: string) =>
|
|
|
54
55
|
encoder.encode(AUTH_SIGN_SECRET),
|
|
55
56
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
56
57
|
false,
|
|
57
|
-
['sign', 'verify']
|
|
58
|
+
['sign', 'verify'],
|
|
58
59
|
);
|
|
59
60
|
|
|
60
61
|
const payloadKey = async (AUTH_PAYLOAD_SECRET: string) =>
|
|
@@ -63,7 +64,7 @@ const payloadKey = async (AUTH_PAYLOAD_SECRET: string) =>
|
|
|
63
64
|
encoder.encode(AUTH_PAYLOAD_SECRET.substring(0, 32)),
|
|
64
65
|
{ name: 'AES-GCM', length: 256 },
|
|
65
66
|
true,
|
|
66
|
-
['encrypt', 'decrypt']
|
|
67
|
+
['encrypt', 'decrypt'],
|
|
67
68
|
);
|
|
68
69
|
|
|
69
70
|
/* --------------------- Payload (encrypt/decrypt) --------------------- */
|
|
@@ -80,7 +81,7 @@ const encryptPayload = async ({
|
|
|
80
81
|
const buffer = await crypto.subtle.encrypt(
|
|
81
82
|
{ name: 'AES-GCM', iv },
|
|
82
83
|
key,
|
|
83
|
-
encoder.encode(payload)
|
|
84
|
+
encoder.encode(payload),
|
|
84
85
|
);
|
|
85
86
|
|
|
86
87
|
return `${toBase64(iv)}.${toBase64(buffer)}`;
|
|
@@ -99,7 +100,7 @@ const decryptPayload = async ({
|
|
|
99
100
|
const buffer = await crypto.subtle.decrypt(
|
|
100
101
|
{ name: 'AES-GCM', iv: fromBase64(header).buffer as ArrayBuffer },
|
|
101
102
|
key,
|
|
102
|
-
fromBase64(payload).buffer as ArrayBuffer
|
|
103
|
+
fromBase64(payload).buffer as ArrayBuffer,
|
|
103
104
|
);
|
|
104
105
|
|
|
105
106
|
return decoder.decode(buffer);
|
|
@@ -115,14 +116,14 @@ const decryptPayload = async ({
|
|
|
115
116
|
*/
|
|
116
117
|
export async function generateDeterministicHash(
|
|
117
118
|
code: string,
|
|
118
|
-
secret: string
|
|
119
|
+
secret: string,
|
|
119
120
|
): Promise<string> {
|
|
120
121
|
const key = await crypto.subtle.importKey(
|
|
121
122
|
'raw',
|
|
122
123
|
encoder.encode(secret),
|
|
123
124
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
124
125
|
false,
|
|
125
|
-
['sign']
|
|
126
|
+
['sign'],
|
|
126
127
|
);
|
|
127
128
|
|
|
128
129
|
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(code));
|
|
@@ -143,7 +144,7 @@ export async function generateDeterministicHash(
|
|
|
143
144
|
export async function verifyDeterministicHash(
|
|
144
145
|
inputCode: string,
|
|
145
146
|
storedHash: string,
|
|
146
|
-
secret: string
|
|
147
|
+
secret: string,
|
|
147
148
|
): Promise<boolean> {
|
|
148
149
|
const hash = await generateDeterministicHash(inputCode, secret);
|
|
149
150
|
return hash === storedHash;
|
|
@@ -153,13 +154,30 @@ export async function verifyDeterministicHash(
|
|
|
153
154
|
export const cryptoUtils: ICrypto = {
|
|
154
155
|
uuidV4: (): string => crypto.randomUUID(),
|
|
155
156
|
|
|
157
|
+
ulid: (): string => {
|
|
158
|
+
function randomChar() {
|
|
159
|
+
const chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // Crockford's Base32
|
|
160
|
+
return chars[Math.floor(Math.random() * chars.length)];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const timestamp = Date.now();
|
|
164
|
+
const timestampStr = timestamp.toString(36).toUpperCase().padStart(10, '0');
|
|
165
|
+
|
|
166
|
+
let randomPart = '';
|
|
167
|
+
for (let i = 0; i < 16; i++) {
|
|
168
|
+
randomPart += randomChar();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return timestampStr + randomPart;
|
|
172
|
+
},
|
|
173
|
+
|
|
156
174
|
hash: async (password: string, salt = 10): Promise<string> => {
|
|
157
175
|
const importedKey = await crypto.subtle.importKey(
|
|
158
176
|
'raw',
|
|
159
177
|
encoder.encode(password),
|
|
160
178
|
{ name: 'PBKDF2' },
|
|
161
179
|
false,
|
|
162
|
-
['deriveBits']
|
|
180
|
+
['deriveBits'],
|
|
163
181
|
);
|
|
164
182
|
|
|
165
183
|
const derivedKey = await crypto.subtle.deriveBits(
|
|
@@ -170,7 +188,7 @@ export const cryptoUtils: ICrypto = {
|
|
|
170
188
|
hash: 'SHA-256',
|
|
171
189
|
},
|
|
172
190
|
importedKey,
|
|
173
|
-
256
|
|
191
|
+
256,
|
|
174
192
|
);
|
|
175
193
|
|
|
176
194
|
return Array.from(new Uint8Array(derivedKey))
|
|
@@ -200,7 +218,7 @@ export const cryptoUtils: ICrypto = {
|
|
|
200
218
|
const signatureBuffer = await crypto.subtle.sign(
|
|
201
219
|
'HMAC',
|
|
202
220
|
key,
|
|
203
|
-
encoder.encode(payload)
|
|
221
|
+
encoder.encode(payload),
|
|
204
222
|
);
|
|
205
223
|
const signature = toBase64(signatureBuffer);
|
|
206
224
|
|
|
@@ -227,7 +245,7 @@ export const cryptoUtils: ICrypto = {
|
|
|
227
245
|
'HMAC',
|
|
228
246
|
key,
|
|
229
247
|
fromBase64(signature).buffer as ArrayBuffer,
|
|
230
|
-
encoder.encode(payloadDecrypted)
|
|
248
|
+
encoder.encode(payloadDecrypted),
|
|
231
249
|
);
|
|
232
250
|
|
|
233
251
|
if (!valid) throw new Error('Invalid access token');
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createValidator } from './helpers/validator';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
htmlEntityDecode,
|
|
4
|
+
sanitizeValue as htmlToPlainText,
|
|
5
|
+
} from './helpers/sanitize';
|
|
3
6
|
import { generateCode } from './helpers/generateCode';
|
|
4
7
|
import { stringUtils } from './helpers/string';
|
|
5
8
|
import { cryptoUtils } from './helpers/crypto';
|
|
@@ -9,6 +12,7 @@ import * as numberUtils from './helpers/number';
|
|
|
9
12
|
export {
|
|
10
13
|
createValidator,
|
|
11
14
|
htmlEntityDecode,
|
|
15
|
+
htmlToPlainText,
|
|
12
16
|
slugUtils,
|
|
13
17
|
numberUtils,
|
|
14
18
|
stringUtils,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createValidator } from '../../src/helpers/validator';
|
|
4
|
+
|
|
5
|
+
describe('validator', () => {
|
|
6
|
+
describe('asNumber', () => {
|
|
7
|
+
it('deve converter uma string "10" para o número 10', () => {
|
|
8
|
+
type InputRaw = { limit: string };
|
|
9
|
+
type InputParsed = { limit: number };
|
|
10
|
+
|
|
11
|
+
const v = createValidator<InputRaw, InputParsed>();
|
|
12
|
+
v.setInputs({ limit: '10' });
|
|
13
|
+
v.validate('limit')
|
|
14
|
+
.isRequired('Limite é obrigatório')
|
|
15
|
+
.type('string', 'Limite deve ser uma string')
|
|
16
|
+
.asNumber('Limite deve ser um número válido');
|
|
17
|
+
|
|
18
|
+
const inputs = v.getSanitizedInputs();
|
|
19
|
+
|
|
20
|
+
assert.equal(inputs.limit, 10);
|
|
21
|
+
assert.equal(typeof inputs.limit, 'number');
|
|
22
|
+
assert.equal(v.hasErrors(), false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('deve adicionar erro quando o valor não pode ser convertido para número', () => {
|
|
26
|
+
type InputRaw = { limit: string };
|
|
27
|
+
type InputParsed = { limit: number };
|
|
28
|
+
|
|
29
|
+
const v = createValidator<InputRaw, InputParsed>();
|
|
30
|
+
v.setInputs({ limit: 'abc' });
|
|
31
|
+
v.validate('limit')
|
|
32
|
+
.isRequired('Limite é obrigatório')
|
|
33
|
+
.type('string', 'Limite deve ser uma string')
|
|
34
|
+
.asNumber('Limite deve ser um número válido');
|
|
35
|
+
|
|
36
|
+
assert.equal(v.hasErrors(), true);
|
|
37
|
+
const errors = v.getFlatErrors();
|
|
38
|
+
assert.equal(errors.limit[0].message, 'Limite deve ser um número válido');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('deve converter um número diretamente', () => {
|
|
42
|
+
type InputRaw = { limit: number };
|
|
43
|
+
type InputParsed = { limit: number };
|
|
44
|
+
|
|
45
|
+
const v = createValidator<InputRaw, InputParsed>();
|
|
46
|
+
v.setInputs({ limit: 25 });
|
|
47
|
+
v.validate('limit')
|
|
48
|
+
.isRequired('Limite é obrigatório')
|
|
49
|
+
.type('number', 'Limite deve ser um número')
|
|
50
|
+
.asNumber('Limite deve ser um número válido');
|
|
51
|
+
|
|
52
|
+
const inputs = v.getSanitizedInputs();
|
|
53
|
+
|
|
54
|
+
assert.equal(inputs.limit, 25);
|
|
55
|
+
assert.equal(typeof inputs.limit, 'number');
|
|
56
|
+
assert.equal(v.hasErrors(), false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('deve lidar com valores opcionais quando não fornecidos', () => {
|
|
60
|
+
type InputRaw = { limit?: string };
|
|
61
|
+
type InputParsed = { limit?: number };
|
|
62
|
+
|
|
63
|
+
const v = createValidator<InputRaw, InputParsed>();
|
|
64
|
+
v.setInputs({});
|
|
65
|
+
v.validate('limit')
|
|
66
|
+
.isNotRequired()
|
|
67
|
+
.type('string', 'Limite deve ser uma string')
|
|
68
|
+
.asNumber('Limite deve ser um número válido');
|
|
69
|
+
|
|
70
|
+
assert.equal(v.hasErrors(), false);
|
|
71
|
+
const inputs = v.getSanitizedInputs();
|
|
72
|
+
assert.equal(inputs.limit, undefined);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
package/tsconfig.json
CHANGED