toolbox-x 1.0.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.
- package/LICENSE +201 -0
- package/dist/Color-B3mgF9Dh.d.cts +486 -0
- package/dist/Color-D38Xrw65.d.mts +486 -0
- package/dist/Stylog-Df7eq3-j.d.cts +519 -0
- package/dist/Stylog-jvlLcMQq.d.mts +519 -0
- package/dist/array-DvW0zIu6.d.mts +130 -0
- package/dist/array-rUnEVisO.d.cts +130 -0
- package/dist/basics-D_eSv0cu.cjs +132 -0
- package/dist/basics-Dp_aEK81.mjs +115 -0
- package/dist/basics-WEYWlnRO.d.cts +95 -0
- package/dist/basics-uBSfkBEI.d.mts +95 -0
- package/dist/case-BWIt8Ash.mjs +449 -0
- package/dist/case-C-S-b5YP.d.cts +327 -0
- package/dist/case-CS8Ii3A7.cjs +526 -0
- package/dist/case-CybASFPD.d.mts +327 -0
- package/dist/change-case.cjs +32 -0
- package/dist/change-case.d.cts +18 -0
- package/dist/change-case.d.mts +18 -0
- package/dist/change-case.mjs +19 -0
- package/dist/colors.cjs +574 -0
- package/dist/colors.d.cts +355 -0
- package/dist/colors.d.mts +355 -0
- package/dist/colors.mjs +547 -0
- package/dist/constants-2gAw23_7.mjs +144 -0
- package/dist/constants-B34K0QPi.d.cts +21 -0
- package/dist/constants-BIBDKY1u.cjs +924 -0
- package/dist/constants-BWT-810U.cjs +158 -0
- package/dist/constants-BwbHnXlM.mjs +662 -0
- package/dist/constants-BxN9l5el.cjs +74 -0
- package/dist/constants-CLS_bgKD.d.mts +847 -0
- package/dist/constants-D73iFu8g.mjs +171 -0
- package/dist/constants-DAfRxaa8.mjs +62 -0
- package/dist/constants-DQYeCjlx.cjs +207 -0
- package/dist/constants-Deeie-iH.d.mts +21 -0
- package/dist/constants-DpTG9RP6.d.mts +29 -0
- package/dist/constants-DqwnkJ_d.cjs +740 -0
- package/dist/constants-DvRUY_FY.cjs +150 -0
- package/dist/constants-VcRtQu0K.d.cts +29 -0
- package/dist/constants-X5hm1UtB.mjs +912 -0
- package/dist/constants-eNd-iYsV.mjs +134 -0
- package/dist/constants-qm8FafmD.d.cts +847 -0
- package/dist/constants.cjs +415 -0
- package/dist/constants.d.cts +184 -0
- package/dist/constants.d.mts +184 -0
- package/dist/constants.mjs +378 -0
- package/dist/convert-BOCgUv2D.cjs +252 -0
- package/dist/convert-Bn4jFomQ.mjs +169 -0
- package/dist/convert-BrzlG-m_.cjs +475 -0
- package/dist/convert-DhaUoPVU.mjs +368 -0
- package/dist/converter-1P90_RcP.d.mts +402 -0
- package/dist/converter-CmkcAppi.d.cts +402 -0
- package/dist/converter.cjs +780 -0
- package/dist/converter.d.cts +29 -0
- package/dist/converter.d.mts +29 -0
- package/dist/converter.mjs +771 -0
- package/dist/countries-CIpmtEzV.cjs +1469 -0
- package/dist/countries-Cy0xiqS3.mjs +1463 -0
- package/dist/css-colors-Bx947Ng3.d.cts +179 -0
- package/dist/css-colors-CXCDqQbG.cjs +186 -0
- package/dist/css-colors-CXTp1vvy.d.mts +179 -0
- package/dist/css-colors-DfUW3nTR.mjs +180 -0
- package/dist/date.cjs +332 -0
- package/dist/date.d.cts +213 -0
- package/dist/date.d.mts +213 -0
- package/dist/date.mjs +298 -0
- package/dist/dom.cjs +461 -0
- package/dist/dom.d.cts +228 -0
- package/dist/dom.d.mts +228 -0
- package/dist/dom.mjs +429 -0
- package/dist/form-BMFVGUrN.d.mts +118 -0
- package/dist/form-DRFbryvK.d.cts +118 -0
- package/dist/guards-3kaUX66g.mjs +157 -0
- package/dist/guards-C8gkvIHb.cjs +240 -0
- package/dist/guards-DdyU4h4o.mjs +110 -0
- package/dist/guards-Efhp1mNy.cjs +151 -0
- package/dist/guards.cjs +172 -0
- package/dist/guards.d.cts +399 -0
- package/dist/guards.d.mts +399 -0
- package/dist/guards.mjs +75 -0
- package/dist/hash-B6JPEyAz.d.mts +131 -0
- package/dist/hash-NTpeKYB_.d.cts +131 -0
- package/dist/hash.cjs +2126 -0
- package/dist/hash.d.cts +1239 -0
- package/dist/hash.d.mts +1239 -0
- package/dist/hash.mjs +2095 -0
- package/dist/http-status-BAZdtr7-.d.mts +65 -0
- package/dist/http-status-U_3MtoGb.d.cts +65 -0
- package/dist/http-status.cjs +173 -0
- package/dist/http-status.d.cts +142 -0
- package/dist/http-status.d.mts +142 -0
- package/dist/http-status.mjs +171 -0
- package/dist/index.cjs +2551 -0
- package/dist/index.d.cts +1493 -0
- package/dist/index.d.mts +1493 -0
- package/dist/index.mjs +2357 -0
- package/dist/object-B0TV3eHx.d.mts +8052 -0
- package/dist/object-Blq0Amdv.d.cts +8052 -0
- package/dist/objectify-CDs0Fbr1.mjs +417 -0
- package/dist/objectify-DIJ-OBmo.cjs +524 -0
- package/dist/paginator.cjs +245 -0
- package/dist/paginator.d.cts +144 -0
- package/dist/paginator.d.mts +144 -0
- package/dist/paginator.mjs +243 -0
- package/dist/parse-2ubxXZRp.cjs +211 -0
- package/dist/parse-N7g942uy.mjs +164 -0
- package/dist/pluralizer-BjMIc6uT.d.mts +42 -0
- package/dist/pluralizer-Cb6ZmrDl.d.cts +42 -0
- package/dist/pluralizer.cjs +678 -0
- package/dist/pluralizer.d.cts +152 -0
- package/dist/pluralizer.d.mts +152 -0
- package/dist/pluralizer.mjs +676 -0
- package/dist/primitives-B26uZolQ.cjs +228 -0
- package/dist/primitives-KsFUp3kQ.mjs +144 -0
- package/dist/specials-D48_IZbd.d.mts +108 -0
- package/dist/specials-DzLr1ZgU.cjs +477 -0
- package/dist/specials-LVONlKbQ.d.cts +108 -0
- package/dist/specials-uhDuRg8H.mjs +292 -0
- package/dist/string-CBAbxaG1.d.mts +258 -0
- package/dist/string-CsNsm_65.d.cts +258 -0
- package/dist/stylog.cjs +621 -0
- package/dist/stylog.d.cts +49 -0
- package/dist/stylog.d.mts +49 -0
- package/dist/stylog.mjs +614 -0
- package/dist/timezone-B2OYK6Fh.mjs +5589 -0
- package/dist/timezone-Beh9IGpw.cjs +5625 -0
- package/dist/types/array.cjs +16 -0
- package/dist/types/array.d.cts +18 -0
- package/dist/types/array.d.mts +18 -0
- package/dist/types/array.mjs +17 -0
- package/dist/types/colors.cjs +16 -0
- package/dist/types/colors.d.cts +18 -0
- package/dist/types/colors.d.mts +18 -0
- package/dist/types/colors.mjs +17 -0
- package/dist/types/converter.cjs +16 -0
- package/dist/types/converter.d.cts +18 -0
- package/dist/types/converter.d.mts +18 -0
- package/dist/types/converter.mjs +17 -0
- package/dist/types/form.cjs +16 -0
- package/dist/types/form.d.cts +18 -0
- package/dist/types/form.d.mts +18 -0
- package/dist/types/form.mjs +17 -0
- package/dist/types/hash.cjs +16 -0
- package/dist/types/hash.d.cts +18 -0
- package/dist/types/hash.d.mts +18 -0
- package/dist/types/hash.mjs +17 -0
- package/dist/types/http-status.cjs +16 -0
- package/dist/types/http-status.d.cts +18 -0
- package/dist/types/http-status.d.mts +18 -0
- package/dist/types/http-status.mjs +17 -0
- package/dist/types/index.cjs +16 -0
- package/dist/types/index.d.cts +18 -0
- package/dist/types/index.d.mts +18 -0
- package/dist/types/index.mjs +17 -0
- package/dist/types/number.cjs +16 -0
- package/dist/types/number.d.cts +18 -0
- package/dist/types/number.d.mts +18 -0
- package/dist/types/number.mjs +17 -0
- package/dist/types/object.cjs +16 -0
- package/dist/types/object.d.cts +18 -0
- package/dist/types/object.d.mts +18 -0
- package/dist/types/object.mjs +17 -0
- package/dist/types/pluralizer.cjs +16 -0
- package/dist/types/pluralizer.d.cts +18 -0
- package/dist/types/pluralizer.d.mts +18 -0
- package/dist/types/pluralizer.mjs +17 -0
- package/dist/types/string.cjs +16 -0
- package/dist/types/string.d.cts +18 -0
- package/dist/types/string.d.mts +18 -0
- package/dist/types/string.mjs +17 -0
- package/dist/types/stylog.cjs +16 -0
- package/dist/types/stylog.d.cts +18 -0
- package/dist/types/stylog.d.mts +18 -0
- package/dist/types/stylog.mjs +17 -0
- package/dist/types/utils.cjs +16 -0
- package/dist/types/utils.d.cts +18 -0
- package/dist/types/utils.d.mts +18 -0
- package/dist/types/utils.mjs +17 -0
- package/dist/types/verbalizer.cjs +16 -0
- package/dist/types/verbalizer.d.cts +30 -0
- package/dist/types/verbalizer.d.mts +30 -0
- package/dist/types/verbalizer.mjs +17 -0
- package/dist/utilities-CLUmdQeV.cjs +140 -0
- package/dist/utilities-m5yFKqLd.mjs +105 -0
- package/dist/utils-ClW9LA6f.mjs +449 -0
- package/dist/utils-DLFRgXUC.cjs +568 -0
- package/dist/verbalizer.cjs +998 -0
- package/dist/verbalizer.d.cts +148 -0
- package/dist/verbalizer.d.mts +148 -0
- package/dist/verbalizer.mjs +996 -0
- package/package.json +249 -0
package/dist/hash.mjs
ADDED
|
@@ -0,0 +1,2095 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 - present Nazmul Hassan
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { a as isNonEmptyString, d as isString } from "./primitives-KsFUp3kQ.mjs";
|
|
18
|
+
import { E as isObjectWithKeys, c as isHexString, m as isUUID, n as isBinaryString, t as isBase64, w as isNotEmptyObject } from "./specials-uhDuRg8H.mjs";
|
|
19
|
+
import { c as _toSeconds, s as _secToDate, t as parseMSec } from "./parse-N7g942uy.mjs";
|
|
20
|
+
import { h as stripJsonEdgeGarbage, m as stableStringify } from "./utils-ClW9LA6f.mjs";
|
|
21
|
+
import { t as generateRandomID } from "./basics-Dp_aEK81.mjs";
|
|
22
|
+
|
|
23
|
+
//#region src/hash/utils.ts
|
|
24
|
+
/**
|
|
25
|
+
* * Generates a random hexadecimal string of the specified length.
|
|
26
|
+
*
|
|
27
|
+
* @param length - Number of hex characters to generate.
|
|
28
|
+
* @param uppercase - Whether to return uppercase `A–F` characters. Defaults to `false` (lowercase).
|
|
29
|
+
*
|
|
30
|
+
* @returns A randomly generated hexadecimal string.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* - This function generates a random hexadecimal string of the specified length.
|
|
34
|
+
* - It uses {@link crypto.getRandomValues} when available for secure randomness, and falls back to {@link Math.random} if not.
|
|
35
|
+
* - The output is a string of hex characters (`0–9`, `a–f` or `A–F`) with no prefixes or separators.
|
|
36
|
+
* - If `length` is `0` or negative, an empty string is returned.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // 16-character lowercase hex
|
|
40
|
+
* const id = randomHex(16);
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // 8-character uppercase hex
|
|
44
|
+
* const token = randomHex(8, true);
|
|
45
|
+
*/
|
|
46
|
+
function randomHex(length, uppercase = false) {
|
|
47
|
+
if (length <= 0) return "";
|
|
48
|
+
const expected = _bytesToRandomHex(new Uint8Array(Math.ceil(length / 2))).slice(0, length);
|
|
49
|
+
return uppercase ? expected.toUpperCase() : expected;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* * Converts a UTF-8 string to a byte array (`Uint8Array`).
|
|
53
|
+
*
|
|
54
|
+
* This function encodes a JavaScript string into UTF-8 bytes, handling all Unicode code points including supplementary characters (surrogate pairs).
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // Basic ASCII
|
|
59
|
+
* const asciiBytes = utf8ToBytes('hello');
|
|
60
|
+
* // Returns:
|
|
61
|
+
* Uint8Array(5) [104, 101, 108, 108, 111]
|
|
62
|
+
*
|
|
63
|
+
* // Unicode characters
|
|
64
|
+
* const unicodeBytes = utf8ToBytes('Hello পৃথিবী!');
|
|
65
|
+
* // Returns:
|
|
66
|
+
* Uint8Array(25) [
|
|
67
|
+
* 72, 101, 108, 108, 111, 32,
|
|
68
|
+
* 224, 166, 170, 224, 167, 131,
|
|
69
|
+
* 224, 166, 165, 224, 166, 191,
|
|
70
|
+
* 224, 166, 172, 224, 167, 128,
|
|
71
|
+
* 33
|
|
72
|
+
* ]
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @param str - The input string to encode as UTF-8 bytes.
|
|
76
|
+
* @returns A `Uint8Array` containing the UTF-8 encoded bytes.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* - The encoding follows the UTF-8 specification:
|
|
80
|
+
* - 1-byte sequence for code points U+0000 to U+007F (ASCII)
|
|
81
|
+
* - 2-byte sequence for code points U+0080 to U+07FF
|
|
82
|
+
* - 3-byte sequence for code points U+0800 to U+FFFF
|
|
83
|
+
* - 4-byte sequence for code points U+10000 to U+10FFFF (surrogate pairs)
|
|
84
|
+
*
|
|
85
|
+
* **Note:** Invalid surrogate pairs in the input string are silently ignored.
|
|
86
|
+
*
|
|
87
|
+
* @see {@link bytesToUtf8} for the inverse operation
|
|
88
|
+
*/
|
|
89
|
+
function utf8ToBytes(str) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (let i = 0; i < str.length; i++) {
|
|
92
|
+
const code = str.charCodeAt(i);
|
|
93
|
+
if (code < 128) out.push(code);
|
|
94
|
+
else if (code < 2048) out.push(192 | code >> 6, 128 | code & 63);
|
|
95
|
+
else if (code >= 55296 && code <= 57343) {
|
|
96
|
+
if (code < 56320 && i + 1 < str.length) {
|
|
97
|
+
const hi = code;
|
|
98
|
+
const lo = str.charCodeAt(++i);
|
|
99
|
+
const codePoint = 65536 + (hi - 55296 << 10) + (lo - 56320);
|
|
100
|
+
out.push(240 | codePoint >> 18, 128 | codePoint >> 12 & 63, 128 | codePoint >> 6 & 63, 128 | codePoint & 63);
|
|
101
|
+
}
|
|
102
|
+
} else out.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
|
|
103
|
+
}
|
|
104
|
+
return new Uint8Array(out);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* * Converts `UTF-8` encoded bytes back to a string.
|
|
108
|
+
*
|
|
109
|
+
* This function decodes a `Uint8Array` containing `UTF-8` bytes into a JavaScript string.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* // Decode UTF-8 bytes
|
|
114
|
+
* const bytes = new Uint8Array([104, 101, 108, 108, 111]);
|
|
115
|
+
* const str = bytesToUtf8(bytes);
|
|
116
|
+
* // Returns: 'hello'
|
|
117
|
+
*
|
|
118
|
+
* // Round-trip conversion
|
|
119
|
+
* const original = 'Hello 🌍';
|
|
120
|
+
* const bytes = utf8ToBytes(original);
|
|
121
|
+
* const decoded = bytesToUtf8(bytes);
|
|
122
|
+
* console.log(original === decoded); // true
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @param bytes - A `Uint8Array` containing `UTF-8` encoded bytes.
|
|
126
|
+
* @returns The decoded string.
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* - The function handles all valid `UTF-8` sequences:
|
|
130
|
+
* - 1-byte sequences (0xxxxxxx) → ASCII characters
|
|
131
|
+
* - 2-byte sequences (110xxxxx 10xxxxxx)
|
|
132
|
+
* - 3-byte sequences (1110xxxx 10xxxxxx 10xxxxxx)
|
|
133
|
+
* - 4-byte sequences (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) → surrogate pairs
|
|
134
|
+
*
|
|
135
|
+
* @see {@link utf8ToBytes} for the inverse operation
|
|
136
|
+
*/
|
|
137
|
+
function bytesToUtf8(bytes) {
|
|
138
|
+
let out = "";
|
|
139
|
+
let i = 0;
|
|
140
|
+
while (i < bytes.length) {
|
|
141
|
+
const b1 = bytes[i++];
|
|
142
|
+
if (b1 < 128) out += String.fromCharCode(b1);
|
|
143
|
+
else if (b1 >= 192 && b1 < 224) {
|
|
144
|
+
const b2 = bytes[i++];
|
|
145
|
+
out += String.fromCharCode((b1 & 31) << 6 | b2 & 63);
|
|
146
|
+
} else if (b1 >= 224 && b1 < 240) {
|
|
147
|
+
const b2 = bytes[i++];
|
|
148
|
+
const b3 = bytes[i++];
|
|
149
|
+
out += String.fromCharCode((b1 & 15) << 12 | (b2 & 63) << 6 | b3 & 63);
|
|
150
|
+
} else {
|
|
151
|
+
const b2 = bytes[i++];
|
|
152
|
+
const b3 = bytes[i++];
|
|
153
|
+
const b4 = bytes[i++];
|
|
154
|
+
let codePoint = (b1 & 7) << 18 | (b2 & 63) << 12 | (b3 & 63) << 6 | b4 & 63;
|
|
155
|
+
codePoint -= 65536;
|
|
156
|
+
out += String.fromCharCode(55296 + (codePoint >> 10 & 1023), 56320 + (codePoint & 1023));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* * Decodes a `Base64` string to bytes.
|
|
163
|
+
* - This function converts a `Base64`-encoded string back to its original byte representation.
|
|
164
|
+
* - It handles standard `Base64` encoding with '=', '+', '/' characters.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* // Decode Base64 string
|
|
169
|
+
* const bytes = base64ToBytes('aGVsbG8gd29ybGQ=');
|
|
170
|
+
* // Returns: Uint8Array(11) [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
|
|
171
|
+
*
|
|
172
|
+
* // Empty string
|
|
173
|
+
* const empty = base64ToBytes('');
|
|
174
|
+
* // Returns: Uint8Array(0) []
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* @param str - The `Base64`-encoded string to decode.
|
|
178
|
+
* @returns A `Uint8Array` containing the decoded bytes.
|
|
179
|
+
*
|
|
180
|
+
* @remarks
|
|
181
|
+
* - The function supports:
|
|
182
|
+
* - Standard `Base64` alphabet (A-Z, a-z, 0-9, +, /)
|
|
183
|
+
* - Padding with '=' characters
|
|
184
|
+
* - Ignores whitespace (though not explicitly trimmed in this implementation)
|
|
185
|
+
*
|
|
186
|
+
* @see {@link bytesToBase64} for the inverse operation
|
|
187
|
+
*/
|
|
188
|
+
function base64ToBytes(str) {
|
|
189
|
+
const _b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
190
|
+
const out = [];
|
|
191
|
+
let i = 0;
|
|
192
|
+
while (i < str.length) {
|
|
193
|
+
const h1 = _b64chars.indexOf(str[i++]);
|
|
194
|
+
const h2 = _b64chars.indexOf(str[i++]);
|
|
195
|
+
const h3 = _b64chars.indexOf(str[i++]);
|
|
196
|
+
const h4 = _b64chars.indexOf(str[i++]);
|
|
197
|
+
const o1 = h1 << 2 | h2 >> 4;
|
|
198
|
+
const o2 = (h2 & 15) << 4 | h3 >> 2;
|
|
199
|
+
const o3 = (h3 & 3) << 6 | h4;
|
|
200
|
+
out.push(o1);
|
|
201
|
+
if (h3 !== 64) out.push(o2);
|
|
202
|
+
if (h4 !== 64) out.push(o3);
|
|
203
|
+
}
|
|
204
|
+
return new Uint8Array(out);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* * Encodes bytes to a `Base64` string.
|
|
208
|
+
* - This function converts a `Uint8Array` to a `Base64`-encoded string using the standard `Base64` alphabet with padding.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* // Encode bytes to Base64
|
|
213
|
+
* const bytes = new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]);
|
|
214
|
+
* const b64 = bytesToBase64(bytes);
|
|
215
|
+
* // Returns: 'aGVsbG8gd29ybGQ='
|
|
216
|
+
*
|
|
217
|
+
* // Empty array
|
|
218
|
+
* const empty = bytesToBase64(new Uint8Array(0));
|
|
219
|
+
* // Returns: ''
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @param bytes - The bytes to encode as `Base64`.
|
|
223
|
+
* @returns The `Base64`-encoded string.
|
|
224
|
+
*
|
|
225
|
+
* @remarks
|
|
226
|
+
* The encoding uses:
|
|
227
|
+
* - Standard `Base64` alphabet (A-Z, a-z, 0-9, +, /)
|
|
228
|
+
* - '=' padding for incomplete groups
|
|
229
|
+
* - No line breaks or whitespace
|
|
230
|
+
*
|
|
231
|
+
* This is a pure JavaScript implementation that doesn't rely on `btoa()`.
|
|
232
|
+
*
|
|
233
|
+
* @see {@link base64ToBytes} for the inverse operation
|
|
234
|
+
*/
|
|
235
|
+
function bytesToBase64(bytes) {
|
|
236
|
+
const _b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
237
|
+
let out = "";
|
|
238
|
+
let i = 0;
|
|
239
|
+
while (i < bytes.length) {
|
|
240
|
+
const o1 = bytes[i++];
|
|
241
|
+
const o2 = bytes[i++];
|
|
242
|
+
const o3 = bytes[i++];
|
|
243
|
+
const h1 = o1 >> 2;
|
|
244
|
+
const h2 = (o1 & 3) << 4 | o2 >> 4;
|
|
245
|
+
const h3 = (o2 & 15) << 2 | o3 >> 6;
|
|
246
|
+
const h4 = o3 & 63;
|
|
247
|
+
out += _b64chars[h1] + _b64chars[h2] + _b64chars[isNaN(o2) ? 64 : h3] + _b64chars[isNaN(o3) ? 64 : h4];
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* * Concatenates multiple `Uint8Array`s into a single `Uint8Array`.
|
|
253
|
+
* - This function efficiently combines multiple byte arrays without creating intermediate strings or arrays.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* // Concatenate multiple arrays
|
|
258
|
+
* const a = new Uint8Array([1, 2, 3]);
|
|
259
|
+
* const b = new Uint8Array([4, 5]);
|
|
260
|
+
* const c = new Uint8Array([6, 7, 8, 9]);
|
|
261
|
+
* const result = concatBytes(a, b, c);
|
|
262
|
+
* // Returns: Uint8Array(9) [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
263
|
+
*
|
|
264
|
+
* // Single array
|
|
265
|
+
* const single = concatBytes(new Uint8Array([1, 2, 3]));
|
|
266
|
+
* // Returns: Uint8Array(3) [1, 2, 3]
|
|
267
|
+
*
|
|
268
|
+
* // No arrays
|
|
269
|
+
* const empty = concatBytes();
|
|
270
|
+
* // Returns: Uint8Array(0) []
|
|
271
|
+
* ```
|
|
272
|
+
*
|
|
273
|
+
* @param parts - One or more `Uint8Array`s to concatenate.
|
|
274
|
+
* @returns A new `Uint8Array` containing all the bytes from the input arrays in the order they were provided.
|
|
275
|
+
*
|
|
276
|
+
* @remarks The function allocates a single `Uint8Array` of the total combined length and copies all bytes into it using `set()` for optimal performance.
|
|
277
|
+
*/
|
|
278
|
+
function concatBytes(...parts) {
|
|
279
|
+
const len = parts.reduce((s, p) => s + p.length, 0);
|
|
280
|
+
const out = new Uint8Array(len);
|
|
281
|
+
let offset = 0;
|
|
282
|
+
for (const p of parts) {
|
|
283
|
+
out.set(p, offset);
|
|
284
|
+
offset += p.length;
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* * Computes the `SHA-256` hash of raw bytes.
|
|
290
|
+
* - This is a pure JavaScript implementation of the `SHA-256` cryptographic hash function that operates directly on byte arrays (`Uint8Array`).
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* // Hash raw bytes
|
|
295
|
+
* const bytes = new Uint8Array([104, 101, 108, 108, 111]); // "hello"
|
|
296
|
+
* const hash = sha256Bytes(bytes);
|
|
297
|
+
* // Returns: Uint8Array(32) with SHA-256 hash
|
|
298
|
+
*
|
|
299
|
+
* // Verify with string hash
|
|
300
|
+
* const strHash = sha256('hello');
|
|
301
|
+
* const bytesHash = bytesToHex(sha256Bytes(utf8ToBytes('hello')));
|
|
302
|
+
* console.log(strHash === bytesHash); // true
|
|
303
|
+
* ```
|
|
304
|
+
*
|
|
305
|
+
* @param message - The bytes to hash as a `Uint8Array`.
|
|
306
|
+
* @returns A `Uint8Array` of 32 bytes (256 bits) containing the `SHA-256` hash.
|
|
307
|
+
*
|
|
308
|
+
* @remarks
|
|
309
|
+
* - Implementation details:
|
|
310
|
+
* - Follows the `SHA-256` specification (FIPS 180-4)
|
|
311
|
+
* - Uses big-endian byte order throughout
|
|
312
|
+
* - Processes messages in 512-bit (64-byte) blocks
|
|
313
|
+
* - Applies proper padding with message length
|
|
314
|
+
* - Uses all required `SHA-256` round constants
|
|
315
|
+
* - Returns hash as 32-byte array
|
|
316
|
+
*
|
|
317
|
+
* @see {@link hmacSha256} for `HMAC-SHA256` computation
|
|
318
|
+
*/
|
|
319
|
+
function sha256Bytes(message) {
|
|
320
|
+
const H = new Uint32Array([
|
|
321
|
+
1779033703,
|
|
322
|
+
3144134277,
|
|
323
|
+
1013904242,
|
|
324
|
+
2773480762,
|
|
325
|
+
1359893119,
|
|
326
|
+
2600822924,
|
|
327
|
+
528734635,
|
|
328
|
+
1541459225
|
|
329
|
+
]);
|
|
330
|
+
const K = new Uint32Array([
|
|
331
|
+
1116352408,
|
|
332
|
+
1899447441,
|
|
333
|
+
3049323471,
|
|
334
|
+
3921009573,
|
|
335
|
+
961987163,
|
|
336
|
+
1508970993,
|
|
337
|
+
2453635748,
|
|
338
|
+
2870763221,
|
|
339
|
+
3624381080,
|
|
340
|
+
310598401,
|
|
341
|
+
607225278,
|
|
342
|
+
1426881987,
|
|
343
|
+
1925078388,
|
|
344
|
+
2162078206,
|
|
345
|
+
2614888103,
|
|
346
|
+
3248222580,
|
|
347
|
+
3835390401,
|
|
348
|
+
4022224774,
|
|
349
|
+
264347078,
|
|
350
|
+
604807628,
|
|
351
|
+
770255983,
|
|
352
|
+
1249150122,
|
|
353
|
+
1555081692,
|
|
354
|
+
1996064986,
|
|
355
|
+
2554220882,
|
|
356
|
+
2821834349,
|
|
357
|
+
2952996808,
|
|
358
|
+
3210313671,
|
|
359
|
+
3336571891,
|
|
360
|
+
3584528711,
|
|
361
|
+
113926993,
|
|
362
|
+
338241895,
|
|
363
|
+
666307205,
|
|
364
|
+
773529912,
|
|
365
|
+
1294757372,
|
|
366
|
+
1396182291,
|
|
367
|
+
1695183700,
|
|
368
|
+
1986661051,
|
|
369
|
+
2177026350,
|
|
370
|
+
2456956037,
|
|
371
|
+
2730485921,
|
|
372
|
+
2820302411,
|
|
373
|
+
3259730800,
|
|
374
|
+
3345764771,
|
|
375
|
+
3516065817,
|
|
376
|
+
3600352804,
|
|
377
|
+
4094571909,
|
|
378
|
+
275423344,
|
|
379
|
+
430227734,
|
|
380
|
+
506948616,
|
|
381
|
+
659060556,
|
|
382
|
+
883997877,
|
|
383
|
+
958139571,
|
|
384
|
+
1322822218,
|
|
385
|
+
1537002063,
|
|
386
|
+
1747873779,
|
|
387
|
+
1955562222,
|
|
388
|
+
2024104815,
|
|
389
|
+
2227730452,
|
|
390
|
+
2361852424,
|
|
391
|
+
2428436474,
|
|
392
|
+
2756734187,
|
|
393
|
+
3204031479,
|
|
394
|
+
3329325298
|
|
395
|
+
]);
|
|
396
|
+
const ml = message.length * 8;
|
|
397
|
+
const padding = new Uint8Array(message.length + 8 + 64 & -64 || 64);
|
|
398
|
+
padding.set(message, 0);
|
|
399
|
+
padding[message.length] = 128;
|
|
400
|
+
const lenView = new DataView(padding.buffer);
|
|
401
|
+
lenView.setUint32(padding.length - 8, Math.floor(ml / 4294967296), false);
|
|
402
|
+
lenView.setUint32(padding.length - 4, ml & 4294967295, false);
|
|
403
|
+
const w = new Uint32Array(64);
|
|
404
|
+
const chunkView = new DataView(padding.buffer);
|
|
405
|
+
for (let offset = 0; offset < padding.length; offset += 64) {
|
|
406
|
+
for (let t = 0; t < 16; t++) w[t] = chunkView.getUint32(offset + t * 4, false);
|
|
407
|
+
for (let t = 16; t < 64; t++) {
|
|
408
|
+
const s0 = (w[t - 15] >>> 7 | w[t - 15] << 25) ^ (w[t - 15] >>> 18 | w[t - 15] << 14) ^ w[t - 15] >>> 3;
|
|
409
|
+
const s1 = (w[t - 2] >>> 17 | w[t - 2] << 15) ^ (w[t - 2] >>> 19 | w[t - 2] << 13) ^ w[t - 2] >>> 10;
|
|
410
|
+
w[t] = w[t - 16] + s0 + w[t - 7] + s1 >>> 0;
|
|
411
|
+
}
|
|
412
|
+
let a = H[0];
|
|
413
|
+
let b = H[1];
|
|
414
|
+
let c = H[2];
|
|
415
|
+
let d = H[3];
|
|
416
|
+
let e = H[4];
|
|
417
|
+
let f = H[5];
|
|
418
|
+
let g = H[6];
|
|
419
|
+
let h = H[7];
|
|
420
|
+
for (let t = 0; t < 64; t++) {
|
|
421
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
422
|
+
const ch = e & f ^ ~e & g;
|
|
423
|
+
const temp1 = h + S1 + ch + K[t] + w[t] >>> 0;
|
|
424
|
+
const temp2 = ((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
425
|
+
h = g;
|
|
426
|
+
g = f;
|
|
427
|
+
f = e;
|
|
428
|
+
e = d + temp1 >>> 0;
|
|
429
|
+
d = c;
|
|
430
|
+
c = b;
|
|
431
|
+
b = a;
|
|
432
|
+
a = temp1 + temp2 >>> 0;
|
|
433
|
+
}
|
|
434
|
+
H[0] = H[0] + a >>> 0;
|
|
435
|
+
H[1] = H[1] + b >>> 0;
|
|
436
|
+
H[2] = H[2] + c >>> 0;
|
|
437
|
+
H[3] = H[3] + d >>> 0;
|
|
438
|
+
H[4] = H[4] + e >>> 0;
|
|
439
|
+
H[5] = H[5] + f >>> 0;
|
|
440
|
+
H[6] = H[6] + g >>> 0;
|
|
441
|
+
H[7] = H[7] + h >>> 0;
|
|
442
|
+
}
|
|
443
|
+
const out = new Uint8Array(32);
|
|
444
|
+
const outView = new DataView(out.buffer);
|
|
445
|
+
for (let i = 0; i < 8; i++) outView.setUint32(i * 4, H[i], false);
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* * Computes `HMAC-SHA256` (Hash-based Message Authentication Code using `SHA-256`).
|
|
450
|
+
* - This function implements the `HMAC` algorithm with `SHA-256` as the underlying hash function, providing message authentication and integrity verification.
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```typescript
|
|
454
|
+
* // Basic HMAC calculation
|
|
455
|
+
* const key = new TextEncoder().encode('secret-key');
|
|
456
|
+
* const message = new TextEncoder().encode('Hello, world!');
|
|
457
|
+
* const hmac = hmacSha256(key, message);
|
|
458
|
+
*
|
|
459
|
+
* // Using with string inputs
|
|
460
|
+
* const keyBytes = new TextEncoder().encode('my-key');
|
|
461
|
+
* const msgBytes = new TextEncoder().encode('data to authenticate');
|
|
462
|
+
* const hmacResult = hmacSha256(keyBytes, msgBytes);
|
|
463
|
+
* const hexResult = bytesToHex(hmacResult);
|
|
464
|
+
* ```
|
|
465
|
+
*
|
|
466
|
+
* @param key - The secret key as a `Uint8Array`.
|
|
467
|
+
* @param message - The message to authenticate as a `Uint8Array`.
|
|
468
|
+
* @returns A `Uint8Array` of 32 bytes containing the `HMAC-SHA256` tag.
|
|
469
|
+
*
|
|
470
|
+
* @remarks
|
|
471
|
+
* - Algorithm steps:
|
|
472
|
+
* - 1. Keys longer than 64 bytes are hashed with `SHA-256`
|
|
473
|
+
* - 2. Keys shorter than 64 bytes are padded with zeros
|
|
474
|
+
* - 3. Inner hash: `SHA-256((key ⊕ ipad) || message)` where ipad = 0x36 repeated
|
|
475
|
+
* - 4. Outer hash: `SHA-256((key ⊕ opad) || inner_hash)` where opad = 0x5C repeated
|
|
476
|
+
*
|
|
477
|
+
* - The implementation follows RFC 2104 and RFC 4231 specifications.
|
|
478
|
+
* - Block size for `SHA-256` HMAC is 64 bytes (512 bits).
|
|
479
|
+
*
|
|
480
|
+
* **Common use cases:**
|
|
481
|
+
* - API authentication tokens
|
|
482
|
+
* - Message integrity verification
|
|
483
|
+
* - Key derivation (as part of `HKDF`)
|
|
484
|
+
*
|
|
485
|
+
* @see {@link sha256Bytes} for the underlying hash function
|
|
486
|
+
*/
|
|
487
|
+
function hmacSha256(key, message) {
|
|
488
|
+
const blockSize = 64;
|
|
489
|
+
let k = key;
|
|
490
|
+
if (k.length > blockSize) k = sha256Bytes(k);
|
|
491
|
+
if (k.length < blockSize) {
|
|
492
|
+
const tmp = new Uint8Array(blockSize);
|
|
493
|
+
tmp.set(k, 0);
|
|
494
|
+
k = tmp;
|
|
495
|
+
}
|
|
496
|
+
const oKeyPad = new Uint8Array(blockSize);
|
|
497
|
+
const iKeyPad = new Uint8Array(blockSize);
|
|
498
|
+
for (let i = 0; i < blockSize; i++) {
|
|
499
|
+
oKeyPad[i] = k[i] ^ 92;
|
|
500
|
+
iKeyPad[i] = k[i] ^ 54;
|
|
501
|
+
}
|
|
502
|
+
const inner = new Uint8Array(iKeyPad.length + message.length);
|
|
503
|
+
inner.set(iKeyPad, 0);
|
|
504
|
+
inner.set(message, iKeyPad.length);
|
|
505
|
+
const innerHash = sha256Bytes(inner);
|
|
506
|
+
const outer = new Uint8Array(oKeyPad.length + innerHash.length);
|
|
507
|
+
outer.set(oKeyPad, 0);
|
|
508
|
+
outer.set(innerHash, oKeyPad.length);
|
|
509
|
+
return sha256Bytes(outer);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* * Converts a `Uint8Array` to a `Uint32Array` with big-endian byte order.
|
|
513
|
+
* - This function groups bytes into 32-bit integers, reading them in big-endian (most significant byte first) order. Missing bytes are treated as zero.
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* // Convert bytes to 32-bit integers
|
|
518
|
+
* const bytes = new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC]);
|
|
519
|
+
* const words = uint8To32ArrayBE(bytes);
|
|
520
|
+
* // Returns: Uint32Array(2) [0x12345678, 0x9ABC0000] or equivalent: Uint32Array(2) [ 305419896, 2596012032 ]
|
|
521
|
+
*
|
|
522
|
+
* // Partial final word
|
|
523
|
+
* const partial = new Uint8Array([0xFF, 0xEE, 0xDD]);
|
|
524
|
+
* const words2 = uint8To32ArrayBE(partial);
|
|
525
|
+
* // Returns: Uint32Array(1) [0xFFEEDD00] or equivalent: Uint32Array(1) [ 4293844224 ]
|
|
526
|
+
* ```
|
|
527
|
+
*
|
|
528
|
+
* @param bytes - The bytes to convert to 32-bit words.
|
|
529
|
+
* @returns A `Uint32Array` containing the 32-bit big-endian words.
|
|
530
|
+
*
|
|
531
|
+
* @remarks
|
|
532
|
+
* - Input length doesn't need to be a multiple of 4
|
|
533
|
+
* - Missing bytes in the final word are padded with zeros
|
|
534
|
+
* - Byte order: `bytes[0]` is the most significant byte of `out[0]`
|
|
535
|
+
* - This is useful for cryptographic operations that work with 32-bit words
|
|
536
|
+
*/
|
|
537
|
+
function uint8To32ArrayBE(bytes) {
|
|
538
|
+
const len = Math.ceil(bytes.length / 4);
|
|
539
|
+
const out = new Uint32Array(len);
|
|
540
|
+
for (let i = 0; i < len; i++) {
|
|
541
|
+
const base = i * 4;
|
|
542
|
+
out[i] = (bytes[base] || 0) << 24 | (bytes[base + 1] || 0) << 16 | (bytes[base + 2] || 0) << 8 | (bytes[base + 3] || 0) << 0;
|
|
543
|
+
}
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* * Converts a 32-bit integer into a 4-byte `Uint8Array` in big-endian (network) byte order.
|
|
548
|
+
* - This function takes a 32-bit integer and encodes it as 4 bytes with the most significant byte first (big-endian order), which is the standard for network protocols and many cryptographic operations.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```typescript
|
|
552
|
+
* // Convert integer to bytes
|
|
553
|
+
* const bytes = intTo4BytesBE(0x12345678);
|
|
554
|
+
* // Returns: Uint8Array(4) [0x12, 0x34, 0x56, 0x78] or equivalent: Uint8Array(4) [ 18, 52, 86, 120 ]
|
|
555
|
+
*
|
|
556
|
+
* // Maximum 32-bit value
|
|
557
|
+
* const maxBytes = intTo4BytesBE(0xFFFFFFFF);
|
|
558
|
+
* // Returns: Uint8Array(4) [0xFF, 0xFF, 0xFF, 0xFF]
|
|
559
|
+
*
|
|
560
|
+
* // Zero
|
|
561
|
+
* const zeroBytes = intTo4BytesBE(0);
|
|
562
|
+
* // Returns: Uint8Array(4) [0x00, 0x00, 0x00, 0x00]
|
|
563
|
+
* ```
|
|
564
|
+
*
|
|
565
|
+
* @param n - The 32-bit integer to convert. Values beyond 32 bits will be truncated.
|
|
566
|
+
* @returns A 4-byte `Uint8Array` representing the value in big-endian format.
|
|
567
|
+
*
|
|
568
|
+
* @remarks
|
|
569
|
+
* - The function uses unsigned 32-bit arithmetic (`>>>` operator)
|
|
570
|
+
* - Only the lower 32 bits of the input are used (truncation)
|
|
571
|
+
* - Output is always exactly 4 bytes
|
|
572
|
+
* - Big-endian order: byte[0] = most significant, byte[3] = least significant
|
|
573
|
+
*
|
|
574
|
+
* **Common use cases:**
|
|
575
|
+
* - Encoding message lengths in network protocols
|
|
576
|
+
* - Preparing data for cryptographic operations
|
|
577
|
+
* - Converting integers for storage or transmission
|
|
578
|
+
*
|
|
579
|
+
* @see {@link uint8To32ArrayBE} for bytes to 32-bit integers conversion
|
|
580
|
+
*/
|
|
581
|
+
function intTo4BytesBE(n) {
|
|
582
|
+
const b = new Uint8Array(4);
|
|
583
|
+
b[0] = n >>> 24 & 255;
|
|
584
|
+
b[1] = n >>> 16 & 255;
|
|
585
|
+
b[2] = n >>> 8 & 255;
|
|
586
|
+
b[3] = n & 255;
|
|
587
|
+
return b;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* * Converts a `Uint8Array` to a lowercase hexadecimal string.
|
|
591
|
+
* - This function encodes binary data (bytes) as a hexadecimal string, with each byte represented as two lowercase hexadecimal digits (0-9, a-f).
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```typescript
|
|
595
|
+
* // Convert bytes to hex
|
|
596
|
+
* const bytes = new Uint8Array([0x12, 0xAB, 0xFF, 0x00]);
|
|
597
|
+
* const hex = bytesToHex(bytes);
|
|
598
|
+
* // Returns: '12abff00'
|
|
599
|
+
*
|
|
600
|
+
* // Empty array
|
|
601
|
+
* const emptyHex = bytesToHex(new Uint8Array(0));
|
|
602
|
+
* // Returns: ''
|
|
603
|
+
*
|
|
604
|
+
* // SHA-256 hash to hex
|
|
605
|
+
* const hashBytes = sha256Bytes(utf8ToBytes('hello'));
|
|
606
|
+
* const hashHex = bytesToHex(hashBytes);
|
|
607
|
+
* // Returns: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
|
|
608
|
+
* ```
|
|
609
|
+
*
|
|
610
|
+
* @param bytes - The bytes to convert to hexadecimal representation.
|
|
611
|
+
* @returns A lowercase hexadecimal string where each byte is represented by two characters (00-ff).
|
|
612
|
+
*
|
|
613
|
+
* @remarks
|
|
614
|
+
* - Always returns lowercase letters (a-f)
|
|
615
|
+
* - Zero pads single-digit hex values (e.g., 0x0F → "0f", not "f")
|
|
616
|
+
* - Efficient O(n) implementation using string concatenation
|
|
617
|
+
* - No prefix (e.g., no "0x" at the beginning)
|
|
618
|
+
*
|
|
619
|
+
* **Common use cases:**
|
|
620
|
+
* - Displaying cryptographic hashes and signatures
|
|
621
|
+
* - Debugging binary data
|
|
622
|
+
* - Converting binary data for JSON serialization
|
|
623
|
+
* - Creating hex-encoded strings for APIs and protocols
|
|
624
|
+
*
|
|
625
|
+
* @see {@link hexToBytes} for reverse process
|
|
626
|
+
*/
|
|
627
|
+
function bytesToHex(bytes) {
|
|
628
|
+
let hex = "";
|
|
629
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
630
|
+
const byte = bytes[i].toString(16).padStart(2, "0");
|
|
631
|
+
hex += byte;
|
|
632
|
+
}
|
|
633
|
+
return hex;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* * Converts a hexadecimal string to a `Uint8Array`.
|
|
637
|
+
* - This function decodes a hexadecimal-encoded string into its raw byte representation, where every two hexadecimal characters (00–ff) are converted into one byte.
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* // Convert hex to bytes
|
|
641
|
+
* const hex = '12abff00';
|
|
642
|
+
* const bytes = hexToBytes(hex);
|
|
643
|
+
* // Returns: Uint8Array(4) [18, 171, 255, 0]
|
|
644
|
+
*
|
|
645
|
+
* // Empty string
|
|
646
|
+
* const emptyBytes = hexToBytes('');
|
|
647
|
+
* // Returns: Uint8Array []
|
|
648
|
+
*
|
|
649
|
+
* // Decode SHA-256 hash from hex
|
|
650
|
+
* const hashHex = '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824';
|
|
651
|
+
* const hashBytes = hexToBytes(hashHex);
|
|
652
|
+
* // Returns: Uint8Array(32)
|
|
653
|
+
*
|
|
654
|
+
* @param hex - A hexadecimal string where each byte is represented by two characters (00–ff).
|
|
655
|
+
* @returns A `Uint8Array` containing the decoded bytes. Returns an empty array for invalid input.
|
|
656
|
+
*
|
|
657
|
+
* @remarks
|
|
658
|
+
* - Accepts lowercase and uppercase hexadecimal characters (0–9, a–f, A–F) with or without space between bytes
|
|
659
|
+
* - Ignores no prefixes (e.g., does not support "0x")
|
|
660
|
+
* - Requires an even number of hexadecimal characters
|
|
661
|
+
* - Efficient O(n) implementation with direct byte parsing
|
|
662
|
+
*
|
|
663
|
+
* **Common use cases:**
|
|
664
|
+
* - Decoding cryptographic hashes and signatures
|
|
665
|
+
* - Parsing hex-encoded binary payloads
|
|
666
|
+
* - Reconstructing binary data from storage or transport formats
|
|
667
|
+
* - Working with low-level protocols and binary APIs
|
|
668
|
+
*
|
|
669
|
+
* @see {@link bytesToHex} for the reverse process
|
|
670
|
+
*/
|
|
671
|
+
function hexToBytes(hex) {
|
|
672
|
+
if (!isHexString(hex)) return new Uint8Array();
|
|
673
|
+
const bytes = _splitByCharLength(hex, 2).map((h) => parseInt(h, 16));
|
|
674
|
+
return new Uint8Array(bytes);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/hash/helpers.ts
|
|
679
|
+
/** Adds two 32-bit numbers */
|
|
680
|
+
function _add32(x, y) {
|
|
681
|
+
return x + y & 4294967295;
|
|
682
|
+
}
|
|
683
|
+
/** Converts a 32-bit number to a 4-byte hex string */
|
|
684
|
+
function _numToHex(n) {
|
|
685
|
+
return [
|
|
686
|
+
0,
|
|
687
|
+
8,
|
|
688
|
+
16,
|
|
689
|
+
24
|
|
690
|
+
].map((shift) => (n >> shift & 255).toString(16).padStart(2, "0")).join("");
|
|
691
|
+
}
|
|
692
|
+
/** Converts a 64-character string block to an array of 16 numbers */
|
|
693
|
+
function _stringToNumbers(s) {
|
|
694
|
+
return Array.from({ length: 16 }, (_, i) => {
|
|
695
|
+
const x = i << 2;
|
|
696
|
+
return s.charCodeAt(x) | s.charCodeAt(x + 1) << 8 | s.charCodeAt(x + 2) << 16 | s.charCodeAt(x + 3) << 24;
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/** Common MD5 transformation function */
|
|
700
|
+
function _transform(q, a, b, x, s, t) {
|
|
701
|
+
const a1 = _add32(_add32(a, q), _add32(x, t));
|
|
702
|
+
return _add32(a1 << s | a1 >>> 32 - s, b);
|
|
703
|
+
}
|
|
704
|
+
/** Round 1 operation */
|
|
705
|
+
const ff = (a, b, c, d, x, s, t) => {
|
|
706
|
+
return _transform(b & c | ~b & d, a, b, x, s, t);
|
|
707
|
+
};
|
|
708
|
+
/** Round 2 operation */
|
|
709
|
+
const gg = (a, b, c, d, x, s, t) => {
|
|
710
|
+
return _transform(b & d | c & ~d, a, b, x, s, t);
|
|
711
|
+
};
|
|
712
|
+
/** Round 3 operation */
|
|
713
|
+
const hh = (a, b, c, d, x, s, t) => {
|
|
714
|
+
return _transform(b ^ c ^ d, a, b, x, s, t);
|
|
715
|
+
};
|
|
716
|
+
/** Round 4 operation */
|
|
717
|
+
const ii = (a, b, c, d, x, s, t) => {
|
|
718
|
+
return _transform(c ^ (b | ~d), a, b, x, s, t);
|
|
719
|
+
};
|
|
720
|
+
/** Performs one MD5 cycle on a 4-element state with 16-word block */
|
|
721
|
+
function _md5cycle(x, k) {
|
|
722
|
+
let a = x[0];
|
|
723
|
+
let b = x[1];
|
|
724
|
+
let c = x[2];
|
|
725
|
+
let d = x[3];
|
|
726
|
+
a = ff(a, b, c, d, k[0], 7, -680876936);
|
|
727
|
+
d = ff(d, a, b, c, k[1], 12, -389564586);
|
|
728
|
+
c = ff(c, d, a, b, k[2], 17, 606105819);
|
|
729
|
+
b = ff(b, c, d, a, k[3], 22, -1044525330);
|
|
730
|
+
a = ff(a, b, c, d, k[4], 7, -176418897);
|
|
731
|
+
d = ff(d, a, b, c, k[5], 12, 1200080426);
|
|
732
|
+
c = ff(c, d, a, b, k[6], 17, -1473231341);
|
|
733
|
+
b = ff(b, c, d, a, k[7], 22, -45705983);
|
|
734
|
+
a = ff(a, b, c, d, k[8], 7, 1770035416);
|
|
735
|
+
d = ff(d, a, b, c, k[9], 12, -1958414417);
|
|
736
|
+
c = ff(c, d, a, b, k[10], 17, -42063);
|
|
737
|
+
b = ff(b, c, d, a, k[11], 22, -1990404162);
|
|
738
|
+
a = ff(a, b, c, d, k[12], 7, 1804603682);
|
|
739
|
+
d = ff(d, a, b, c, k[13], 12, -40341101);
|
|
740
|
+
c = ff(c, d, a, b, k[14], 17, -1502002290);
|
|
741
|
+
b = ff(b, c, d, a, k[15], 22, 1236535329);
|
|
742
|
+
a = gg(a, b, c, d, k[1], 5, -165796510);
|
|
743
|
+
d = gg(d, a, b, c, k[6], 9, -1069501632);
|
|
744
|
+
c = gg(c, d, a, b, k[11], 14, 643717713);
|
|
745
|
+
b = gg(b, c, d, a, k[0], 20, -373897302);
|
|
746
|
+
a = gg(a, b, c, d, k[5], 5, -701558691);
|
|
747
|
+
d = gg(d, a, b, c, k[10], 9, 38016083);
|
|
748
|
+
c = gg(c, d, a, b, k[15], 14, -660478335);
|
|
749
|
+
b = gg(b, c, d, a, k[4], 20, -405537848);
|
|
750
|
+
a = gg(a, b, c, d, k[9], 5, 568446438);
|
|
751
|
+
d = gg(d, a, b, c, k[14], 9, -1019803690);
|
|
752
|
+
c = gg(c, d, a, b, k[3], 14, -187363961);
|
|
753
|
+
b = gg(b, c, d, a, k[8], 20, 1163531501);
|
|
754
|
+
a = gg(a, b, c, d, k[13], 5, -1444681467);
|
|
755
|
+
d = gg(d, a, b, c, k[2], 9, -51403784);
|
|
756
|
+
c = gg(c, d, a, b, k[7], 14, 1735328473);
|
|
757
|
+
b = gg(b, c, d, a, k[12], 20, -1926607734);
|
|
758
|
+
a = hh(a, b, c, d, k[5], 4, -378558);
|
|
759
|
+
d = hh(d, a, b, c, k[8], 11, -2022574463);
|
|
760
|
+
c = hh(c, d, a, b, k[11], 16, 1839030562);
|
|
761
|
+
b = hh(b, c, d, a, k[14], 23, -35309556);
|
|
762
|
+
a = hh(a, b, c, d, k[1], 4, -1530992060);
|
|
763
|
+
d = hh(d, a, b, c, k[4], 11, 1272893353);
|
|
764
|
+
c = hh(c, d, a, b, k[7], 16, -155497632);
|
|
765
|
+
b = hh(b, c, d, a, k[10], 23, -1094730640);
|
|
766
|
+
a = hh(a, b, c, d, k[13], 4, 681279174);
|
|
767
|
+
d = hh(d, a, b, c, k[0], 11, -358537222);
|
|
768
|
+
c = hh(c, d, a, b, k[3], 16, -722521979);
|
|
769
|
+
b = hh(b, c, d, a, k[6], 23, 76029189);
|
|
770
|
+
a = hh(a, b, c, d, k[9], 4, -640364487);
|
|
771
|
+
d = hh(d, a, b, c, k[12], 11, -421815835);
|
|
772
|
+
c = hh(c, d, a, b, k[15], 16, 530742520);
|
|
773
|
+
b = hh(b, c, d, a, k[2], 23, -995338651);
|
|
774
|
+
a = ii(a, b, c, d, k[0], 6, -198630844);
|
|
775
|
+
d = ii(d, a, b, c, k[7], 10, 1126891415);
|
|
776
|
+
c = ii(c, d, a, b, k[14], 15, -1416354905);
|
|
777
|
+
b = ii(b, c, d, a, k[5], 21, -57434055);
|
|
778
|
+
a = ii(a, b, c, d, k[12], 6, 1700485571);
|
|
779
|
+
d = ii(d, a, b, c, k[3], 10, -1894986606);
|
|
780
|
+
c = ii(c, d, a, b, k[10], 15, -1051523);
|
|
781
|
+
b = ii(b, c, d, a, k[1], 21, -2054922799);
|
|
782
|
+
a = ii(a, b, c, d, k[8], 6, 1873313359);
|
|
783
|
+
d = ii(d, a, b, c, k[15], 10, -30611744);
|
|
784
|
+
c = ii(c, d, a, b, k[6], 15, -1560198380);
|
|
785
|
+
b = ii(b, c, d, a, k[13], 21, 1309151649);
|
|
786
|
+
a = ii(a, b, c, d, k[4], 6, -145523070);
|
|
787
|
+
d = ii(d, a, b, c, k[11], 10, -1120210379);
|
|
788
|
+
c = ii(c, d, a, b, k[2], 15, 718787259);
|
|
789
|
+
b = ii(b, c, d, a, k[9], 21, -343485551);
|
|
790
|
+
x[0] = _add32(a, x[0]);
|
|
791
|
+
x[1] = _add32(b, x[1]);
|
|
792
|
+
x[2] = _add32(c, x[2]);
|
|
793
|
+
x[3] = _add32(d, x[3]);
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Computes UUID timestamp in 100-nanosecond intervals since
|
|
797
|
+
* 00:00:00.00 15 October 1582 (Gregorian epoch).
|
|
798
|
+
*/
|
|
799
|
+
function _uuidTimestamp() {
|
|
800
|
+
return (BigInt(Date.now()) + 12219292800000n) * 10000n;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Generates a random 48-bit node ID.
|
|
804
|
+
* LSB of first byte must be 1 to indicate a randomly generated node.
|
|
805
|
+
*/
|
|
806
|
+
function _randomNode48() {
|
|
807
|
+
const node = randomHex(12).split("");
|
|
808
|
+
return (parseInt(node.slice(0, 2).join(""), 16) | 1).toString(16).padStart(2, "0") + node.slice(2).join("");
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Generates random hex string of given byte length using crypto API if available, otherwise falls back to Math.random.
|
|
812
|
+
* @param bytes Instance of {@link Uint8Array} to fill with random values.
|
|
813
|
+
* @returns Hex string representation of the random bytes.
|
|
814
|
+
*/
|
|
815
|
+
function _bytesToRandomHex(bytes) {
|
|
816
|
+
if (crypto?.getRandomValues) crypto.getRandomValues(bytes);
|
|
817
|
+
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
818
|
+
let hex = "";
|
|
819
|
+
for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
820
|
+
return hex;
|
|
821
|
+
}
|
|
822
|
+
/** * Generates a 14-bit clock sequence (2 bytes, but only 14 bits used). */
|
|
823
|
+
function _clockSeq14() {
|
|
824
|
+
return (parseInt(randomHex(4), 16) & 16383).toString(16).padStart(4, "0");
|
|
825
|
+
}
|
|
826
|
+
/** Convert a hex string to UUID format */
|
|
827
|
+
function _formatUUID(h, v, up) {
|
|
828
|
+
const part3 = String(v) + h.slice(13, 16);
|
|
829
|
+
const part4 = _hexVariant(h.slice(16, 18)) + h.slice(18, 20);
|
|
830
|
+
const formatted = [
|
|
831
|
+
h.slice(0, 8),
|
|
832
|
+
h.slice(8, 12),
|
|
833
|
+
part3,
|
|
834
|
+
part4,
|
|
835
|
+
h.slice(20, 32)
|
|
836
|
+
].join("-");
|
|
837
|
+
return up ? formatted.toUpperCase() : formatted;
|
|
838
|
+
}
|
|
839
|
+
/** Ensure UUID variant is RFC4122 compliant */
|
|
840
|
+
function _hexVariant(hex) {
|
|
841
|
+
return (parseInt(hex, 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
842
|
+
}
|
|
843
|
+
/** Check if the uuid `options` is compatible for `v3` and `v5` */
|
|
844
|
+
function _isOptionV3V5(opt) {
|
|
845
|
+
if (isObjectWithKeys(opt, ["name", "namespace"])) return isUUID(opt?.namespace) && isNonEmptyString(opt?.name);
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
/** Check UUID version after checking for valid UUID from `v1-v8` */
|
|
849
|
+
function _checkUUIDVersion(value, v) {
|
|
850
|
+
return isUUID(value) && value[14] === v;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Compares two encrypted strings or byte arrays in constant time.
|
|
854
|
+
* Prevents timing attacks by ensuring equal-time checks regardless of data differences.
|
|
855
|
+
*/
|
|
856
|
+
function _constantTimeEquals(a, b) {
|
|
857
|
+
if (a.length !== b.length) return false;
|
|
858
|
+
const _getRes = (x, idx) => {
|
|
859
|
+
return isString(x) ? x.charCodeAt(idx) : x[idx];
|
|
860
|
+
};
|
|
861
|
+
let res = 0;
|
|
862
|
+
for (let i = 0; i < a.length; i++) res |= _getRes(a, i) ^ _getRes(b, i);
|
|
863
|
+
return res === 0;
|
|
864
|
+
}
|
|
865
|
+
/** Split string by substring length, intended to be used internally for hex and binary converters only */
|
|
866
|
+
function _splitByCharLength(str, splitByChars = 2) {
|
|
867
|
+
if (!isNonEmptyString(str)) return [];
|
|
868
|
+
const sanitized = str.replace(/\s+/g, "");
|
|
869
|
+
const result = [];
|
|
870
|
+
for (let i = 0; i < sanitized.length; i += splitByChars) result.push(sanitized.slice(i, i + splitByChars));
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
/** Pad start of a byte with 0 for `hex` or `binary` */
|
|
874
|
+
function _padStartWith0(byte, type) {
|
|
875
|
+
return byte.toString(type === "hex" ? 16 : 2).padStart(type === "hex" ? 2 : 8, "0");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/hash/Cipher.ts
|
|
880
|
+
/**
|
|
881
|
+
* @class Lightweight stream-cipher–style encryption utility using `HMAC-SHA256` for keystream generation and authentication.
|
|
882
|
+
* - The class derives separate encryption and MAC keys from the provided secret.
|
|
883
|
+
*
|
|
884
|
+
* @remarks
|
|
885
|
+
* - **The encryption scheme is:**
|
|
886
|
+
* - keystream = `HMAC(encKey, iv || counter)`
|
|
887
|
+
* - ciphertext = `plaintext XOR keystream`
|
|
888
|
+
* - tag = `HMAC(macKey, iv || ciphertext)`
|
|
889
|
+
* - This is a custom construction and should not be used for production-grade cryptographic security.
|
|
890
|
+
* - `Cipher` class is a pure JS implementation. It does not rely on `crypto` or Web APIs.
|
|
891
|
+
*/
|
|
892
|
+
var Cipher = class {
|
|
893
|
+
#secretBytes;
|
|
894
|
+
#encKey;
|
|
895
|
+
#macKey;
|
|
896
|
+
/**
|
|
897
|
+
* * Creates a new `Cipher` instance using the provided secret.
|
|
898
|
+
*
|
|
899
|
+
* @param secret - The secret string used to derive encryption and MAC keys.
|
|
900
|
+
* Must be a non-empty string.
|
|
901
|
+
*/
|
|
902
|
+
constructor(secret) {
|
|
903
|
+
if (!isNonEmptyString(secret)) throw new Error("Secret must be non-empty string!");
|
|
904
|
+
this.#secretBytes = utf8ToBytes(secret);
|
|
905
|
+
this.#encKey = hmacSha256(this.#secretBytes, utf8ToBytes("enc"));
|
|
906
|
+
this.#macKey = hmacSha256(this.#secretBytes, utf8ToBytes("mac"));
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Generates a keystream of the same length as the provided data using a `HMAC`-based counter mode.
|
|
910
|
+
* The keystream is deterministic from the encryption key and IV.
|
|
911
|
+
*
|
|
912
|
+
* @param target - The byte array whose length determines the keystream size.
|
|
913
|
+
* @param iv - The initialization vector used as input to the `HMAC`.
|
|
914
|
+
* @returns A byte array representing the generated keystream.
|
|
915
|
+
*/
|
|
916
|
+
#genKeystream(target, iv) {
|
|
917
|
+
const blocks = Math.ceil(target.length / 32);
|
|
918
|
+
const keystreamParts = [];
|
|
919
|
+
for (let counter = 0; counter < blocks; counter++) keystreamParts.push(hmacSha256(this.#encKey, concatBytes(iv, intTo4BytesBE(counter))));
|
|
920
|
+
return concatBytes(...keystreamParts).subarray(0, target.length);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* * Encrypts a UTF-8 string.
|
|
924
|
+
* - The output format is: `base64( iv || ciphertext || tag )`
|
|
925
|
+
*
|
|
926
|
+
* @param text - The plaintext string to encrypt.
|
|
927
|
+
* @returns A base64-encoded encrypted token.
|
|
928
|
+
*/
|
|
929
|
+
encrypt(text) {
|
|
930
|
+
const plain = utf8ToBytes(text);
|
|
931
|
+
const iv = sha256Bytes(utf8ToBytes(String(Date.now()) + "-" + String(Math.random()))).subarray(0, 16);
|
|
932
|
+
const keystream = this.#genKeystream(plain, iv);
|
|
933
|
+
const ct = new Uint8Array(plain.length);
|
|
934
|
+
for (let i = 0; i < plain.length; i++) ct[i] = plain[i] ^ keystream[i];
|
|
935
|
+
return bytesToBase64(concatBytes(iv, ct, hmacSha256(this.#macKey, concatBytes(iv, ct))));
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* * Checks if a token is structurally valid and contains a matching MAC using the same secret.
|
|
939
|
+
*
|
|
940
|
+
* @param token - The base64-encoded encrypted blob to validate.
|
|
941
|
+
* @returns `true` if the MAC is valid, `false` otherwise.
|
|
942
|
+
*/
|
|
943
|
+
isValid(token) {
|
|
944
|
+
if (!isBase64(token)) return false;
|
|
945
|
+
const blob = base64ToBytes(token);
|
|
946
|
+
if (blob.length < 48) return false;
|
|
947
|
+
const iv = blob.subarray(0, 16);
|
|
948
|
+
const tag = blob.subarray(blob.length - 32);
|
|
949
|
+
const ct = blob.subarray(16, blob.length - 32);
|
|
950
|
+
return _constantTimeEquals(hmacSha256(this.#macKey, concatBytes(iv, ct)), tag);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* * Decrypts a previously encrypted token.
|
|
954
|
+
* - Throws an error if the tag does not match or the token is malformed.
|
|
955
|
+
*
|
|
956
|
+
* @param token - The base64-encoded token produced by `encrypt`.
|
|
957
|
+
* @returns The decrypted plaintext string.
|
|
958
|
+
*/
|
|
959
|
+
decrypt(token) {
|
|
960
|
+
if (!isBase64(token)) throw new Error("Token must be a base64 string!");
|
|
961
|
+
const blob = base64ToBytes(token);
|
|
962
|
+
if (blob.length < 48) throw new Error("Malformed or tampered token!");
|
|
963
|
+
const iv = blob.subarray(0, 16);
|
|
964
|
+
const tag = blob.subarray(blob.length - 32);
|
|
965
|
+
const ct = blob.subarray(16, blob.length - 32);
|
|
966
|
+
if (!_constantTimeEquals(hmacSha256(this.#macKey, concatBytes(iv, ct)), tag)) throw new Error("Key in the token is tampered or invalid!)");
|
|
967
|
+
const keystream = this.#genKeystream(ct, iv);
|
|
968
|
+
const pt = new Uint8Array(ct.length);
|
|
969
|
+
for (let i = 0; i < ct.length; i++) pt[i] = ct[i] ^ keystream[i];
|
|
970
|
+
return bytesToUtf8(pt);
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/hash/core.ts
|
|
976
|
+
/**
|
|
977
|
+
* * Computes the `MD5` digest of the given string using a pure JavaScript implementation.
|
|
978
|
+
*
|
|
979
|
+
* @remarks
|
|
980
|
+
* - Pure JavaScript implementation — runs on any JS engine. Does not rely on `crypto` or **Web APIs** or other external libraries.
|
|
981
|
+
* - Highly inspired by the algorithm used in {@link https://github.com/eustatos/pure-md5.git pure-md5} package.
|
|
982
|
+
*
|
|
983
|
+
* @param str - Input text to hash.
|
|
984
|
+
*
|
|
985
|
+
* @returns The `MD5` hash as a 32-character hex string.
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* const hash = md5("hello");
|
|
989
|
+
* // → "5d41402abc4b2a76b9719d911017c592" *
|
|
990
|
+
*
|
|
991
|
+
* @example
|
|
992
|
+
* // Used inside UUID v3
|
|
993
|
+
* const digest = md5(namespace + name);
|
|
994
|
+
*/
|
|
995
|
+
function md5(str) {
|
|
996
|
+
const state = [
|
|
997
|
+
1732584193,
|
|
998
|
+
-271733879,
|
|
999
|
+
-1732584194,
|
|
1000
|
+
271733878
|
|
1001
|
+
];
|
|
1002
|
+
const len = str.length;
|
|
1003
|
+
let i;
|
|
1004
|
+
for (i = 64; i <= len; i += 64) _md5cycle(state, _stringToNumbers(str.substring(i - 64, i)));
|
|
1005
|
+
const $str = str.substring(i - 64);
|
|
1006
|
+
const tail = [
|
|
1007
|
+
0,
|
|
1008
|
+
0,
|
|
1009
|
+
0,
|
|
1010
|
+
0,
|
|
1011
|
+
0,
|
|
1012
|
+
0,
|
|
1013
|
+
0,
|
|
1014
|
+
0,
|
|
1015
|
+
0,
|
|
1016
|
+
0,
|
|
1017
|
+
0,
|
|
1018
|
+
0,
|
|
1019
|
+
0,
|
|
1020
|
+
0,
|
|
1021
|
+
0,
|
|
1022
|
+
0
|
|
1023
|
+
];
|
|
1024
|
+
for (i = 0; i < $str.length; i++) tail[i >> 2] |= $str.charCodeAt(i) << (i % 4 << 3);
|
|
1025
|
+
tail[i >> 2] |= 128 << (i % 4 << 3);
|
|
1026
|
+
if (i > 55) {
|
|
1027
|
+
_md5cycle(state, tail);
|
|
1028
|
+
for (let j = 0; j < 16; j++) tail[j] = 0;
|
|
1029
|
+
}
|
|
1030
|
+
tail[14] = len * 8;
|
|
1031
|
+
_md5cycle(state, tail);
|
|
1032
|
+
return state.map(_numToHex).join("");
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* * Computes the `SHA-1` digest of the given string using a pure JavaScript implementation.
|
|
1036
|
+
*
|
|
1037
|
+
* @remarks Pure JavaScript implementation — runs on any JS engine. Does not rely on `crypto` or **Web APIs** or other external libraries.
|
|
1038
|
+
*
|
|
1039
|
+
* @param msg - Input text to hash.
|
|
1040
|
+
*
|
|
1041
|
+
* @returns The `SHA-1` hash as a 40-character hex string.
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* const hash = sha1("hello");
|
|
1045
|
+
* // → "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
|
|
1046
|
+
*
|
|
1047
|
+
* @example
|
|
1048
|
+
* // Used inside UUID v5
|
|
1049
|
+
* const digest = sha1(namespace + name);
|
|
1050
|
+
*/
|
|
1051
|
+
function sha1(msg) {
|
|
1052
|
+
const K = [
|
|
1053
|
+
1518500249,
|
|
1054
|
+
1859775393,
|
|
1055
|
+
2400959708,
|
|
1056
|
+
3395469782
|
|
1057
|
+
];
|
|
1058
|
+
const utf8 = utf8ToBytes(msg);
|
|
1059
|
+
const rotl = (n, bits) => n << bits | n >>> 32 - bits;
|
|
1060
|
+
const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
|
|
1061
|
+
const len = utf8.length;
|
|
1062
|
+
const padBytes = (len + 9) % 64 ? 64 - (len + 9) % 64 : 0;
|
|
1063
|
+
const total = len + 1 + padBytes + 8;
|
|
1064
|
+
const words = new Uint32Array(total >>> 2);
|
|
1065
|
+
for (let i = 0; i < utf8.length; i++) words[i >> 2] |= utf8[i] << 24 - i % 4 * 8;
|
|
1066
|
+
words[utf8.length >> 2] |= 128 << 24 - utf8.length % 4 * 8;
|
|
1067
|
+
words[words.length - 1] = utf8.length * 8;
|
|
1068
|
+
const h = [
|
|
1069
|
+
1732584193,
|
|
1070
|
+
4023233417,
|
|
1071
|
+
2562383102,
|
|
1072
|
+
271733878,
|
|
1073
|
+
3285377520
|
|
1074
|
+
];
|
|
1075
|
+
const W = new Uint32Array(80);
|
|
1076
|
+
for (let i = 0; i < words.length; i += 16) {
|
|
1077
|
+
for (let j = 0; j < 16; j++) W[j] = words[i + j] | 0;
|
|
1078
|
+
for (let j = 16; j < 80; j++) W[j] = rotl(W[j - 3] ^ W[j - 8] ^ W[j - 14] ^ W[j - 16], 1);
|
|
1079
|
+
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
|
|
1080
|
+
for (let j = 0; j < 80; j++) {
|
|
1081
|
+
let f, k;
|
|
1082
|
+
if (j < 20) {
|
|
1083
|
+
f = b & c | ~b & d;
|
|
1084
|
+
k = K[0];
|
|
1085
|
+
} else if (j < 40) {
|
|
1086
|
+
f = b ^ c ^ d;
|
|
1087
|
+
k = K[1];
|
|
1088
|
+
} else if (j < 60) {
|
|
1089
|
+
f = b & c | b & d | c & d;
|
|
1090
|
+
k = K[2];
|
|
1091
|
+
} else {
|
|
1092
|
+
f = b ^ c ^ d;
|
|
1093
|
+
k = K[3];
|
|
1094
|
+
}
|
|
1095
|
+
const temp = rotl(a, 5) + f + e + k + W[j] | 0;
|
|
1096
|
+
e = d;
|
|
1097
|
+
d = c;
|
|
1098
|
+
c = rotl(b, 30);
|
|
1099
|
+
b = a;
|
|
1100
|
+
a = temp;
|
|
1101
|
+
}
|
|
1102
|
+
h[0] = h[0] + a | 0;
|
|
1103
|
+
h[1] = h[1] + b | 0;
|
|
1104
|
+
h[2] = h[2] + c | 0;
|
|
1105
|
+
h[3] = h[3] + d | 0;
|
|
1106
|
+
h[4] = h[4] + e | 0;
|
|
1107
|
+
}
|
|
1108
|
+
return h.map(toHex).join("");
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* * Computes the `SHA-256` hash of a `UTF-8` string and returns it as a lowercase hexadecimal string.
|
|
1112
|
+
*
|
|
1113
|
+
* @param msg - The input string to hash. Can contain any `UTF-8` characters.
|
|
1114
|
+
* @returns A 64-character lowercase hexadecimal string representing the `SHA-256` hash.
|
|
1115
|
+
*
|
|
1116
|
+
* @remarks Pure JavaScript implementation — runs on any JS engine. Does not rely on `crypto` or **Web APIs** or other external libraries.
|
|
1117
|
+
*
|
|
1118
|
+
* @example
|
|
1119
|
+
* ```typescript
|
|
1120
|
+
* // Basic usage
|
|
1121
|
+
* const hash = sha256('hello');
|
|
1122
|
+
* // Returns: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
|
|
1123
|
+
*
|
|
1124
|
+
* // Empty string
|
|
1125
|
+
* const emptyHash = sha256('');
|
|
1126
|
+
* // Returns: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
|
1127
|
+
*
|
|
1128
|
+
* // Unicode string
|
|
1129
|
+
* const unicodeHash = sha256('Hello পৃথিবী!');
|
|
1130
|
+
* // Returns: '7037e204b825b83553ba336a6ec35b796d505599286ae864729ed6cb33ae9fe1'
|
|
1131
|
+
* ```
|
|
1132
|
+
*
|
|
1133
|
+
* @see {@link https://toolbox.nazmul-nhb.dev/docs/utilities/hash/encoding#sha256bytes sha256Bytes} for hashing raw bytes
|
|
1134
|
+
* @see {@link https://toolbox.nazmul-nhb.dev/docs/utilities/hash/encoding#utf8tobytes utf8ToBytes} for converting string to bytes
|
|
1135
|
+
* @see {@link https://toolbox.nazmul-nhb.dev/docs/utilities/hash/encoding#bytestohex bytesToHex} for converting bytes to a hexadecimal string
|
|
1136
|
+
*/
|
|
1137
|
+
function sha256(msg) {
|
|
1138
|
+
if (!isString(msg)) throw new TypeError("Input must be of type string!");
|
|
1139
|
+
return bytesToHex(sha256Bytes(utf8ToBytes(msg)));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/hash/Signet.ts
|
|
1144
|
+
/**
|
|
1145
|
+
* @class A lightweight, secure implementation of JWT-like tokens using `HMAC-SHA256` signatures.
|
|
1146
|
+
* - This class provides methods to create, verify, and decode tokens with a simple API similar to JSON Web Tokens (`JWT`)
|
|
1147
|
+
* but with a smaller footprint and zero dependencies.
|
|
1148
|
+
*
|
|
1149
|
+
* @remarks
|
|
1150
|
+
* - **Features:**
|
|
1151
|
+
* - `HMAC-SHA256` signatures for security
|
|
1152
|
+
* - Time-based claims (expiration, not-before)
|
|
1153
|
+
* - Standard claims (audience, issuer, subject)
|
|
1154
|
+
* - Constant-time signature comparison to prevent timing attacks
|
|
1155
|
+
* - Automatic date conversion for timestamp claims
|
|
1156
|
+
* - `Base64` URL-safe encoding (standard `Base64` in this implementation)
|
|
1157
|
+
*
|
|
1158
|
+
* - **Security considerations:**
|
|
1159
|
+
* - Keep the secret key secure and rotate periodically
|
|
1160
|
+
* - Use appropriate token expiration times
|
|
1161
|
+
* - Validate all claims relevant to your application
|
|
1162
|
+
* - Store tokens securely (HTTP-only cookies recommended for web)
|
|
1163
|
+
*
|
|
1164
|
+
* @example
|
|
1165
|
+
* ```typescript
|
|
1166
|
+
* // Create a token signer
|
|
1167
|
+
* const signet = new Signet('my-secret-key');
|
|
1168
|
+
*
|
|
1169
|
+
* // Sign a token with custom payload and options
|
|
1170
|
+
* const token = signet.sign(
|
|
1171
|
+
* { userId: 123, role: 'admin' },
|
|
1172
|
+
* {
|
|
1173
|
+
* expiresIn: '1h',
|
|
1174
|
+
* audience: 'my-app',
|
|
1175
|
+
* issuer: 'auth-service'
|
|
1176
|
+
* }
|
|
1177
|
+
* );
|
|
1178
|
+
*
|
|
1179
|
+
* // Verify a token
|
|
1180
|
+
* const result = signet.verify(token, {
|
|
1181
|
+
* audience: 'my-app',
|
|
1182
|
+
* issuer: 'auth-service'
|
|
1183
|
+
* });
|
|
1184
|
+
*
|
|
1185
|
+
* if (result.isValid) {
|
|
1186
|
+
* console.log('Valid token for user:', result.payload.userId);
|
|
1187
|
+
* } else {
|
|
1188
|
+
* console.log('Invalid token:', result.error);
|
|
1189
|
+
* }
|
|
1190
|
+
*
|
|
1191
|
+
* // Decode without verification
|
|
1192
|
+
* const decoded = signet.decode(token);
|
|
1193
|
+
* console.log('Token payload:', decoded.payload);
|
|
1194
|
+
* ```
|
|
1195
|
+
*/
|
|
1196
|
+
var Signet = class {
|
|
1197
|
+
#secretBytes;
|
|
1198
|
+
/**
|
|
1199
|
+
* * Creates a new `Signet` instance with the specified secret key.
|
|
1200
|
+
*
|
|
1201
|
+
* @param secret - The secret key used for signing and verifying tokens.
|
|
1202
|
+
* Must be a non-empty string.
|
|
1203
|
+
*
|
|
1204
|
+
* @throws If the secret is not a non-empty string.
|
|
1205
|
+
*
|
|
1206
|
+
* @remarks
|
|
1207
|
+
* - The secret is converted to `UTF-8` bytes and stored internally.
|
|
1208
|
+
* - Choose a strong secret (at least 32 characters) and store it securely.
|
|
1209
|
+
* - For production, consider using key rotation strategies.
|
|
1210
|
+
*
|
|
1211
|
+
* @example
|
|
1212
|
+
* ```typescript
|
|
1213
|
+
* // Initialize with a secret key
|
|
1214
|
+
* const signet = new Signet('super-secret-key-123');
|
|
1215
|
+
*
|
|
1216
|
+
* // Use environment variable for the secret
|
|
1217
|
+
* const signet = new Signet(process.env.JWT_SECRET!);
|
|
1218
|
+
* ```
|
|
1219
|
+
*/
|
|
1220
|
+
constructor(secret) {
|
|
1221
|
+
if (!isNonEmptyString(secret)) throw new Error("Secret must be a non-empty string!");
|
|
1222
|
+
this.#secretBytes = utf8ToBytes(secret);
|
|
1223
|
+
}
|
|
1224
|
+
/** Decodes a token without verifying its signature. */
|
|
1225
|
+
#decode(token) {
|
|
1226
|
+
if (!isNonEmptyString(token)) throw new Error("Token must be a non-empty string!");
|
|
1227
|
+
const parts = token.split(".");
|
|
1228
|
+
if (parts.length !== 3) throw new Error("Token is tampered or malformed!");
|
|
1229
|
+
const [hdr, pld, signature] = parts;
|
|
1230
|
+
const headerBytes = base64ToBytes(hdr);
|
|
1231
|
+
const payloadBytes = base64ToBytes(pld);
|
|
1232
|
+
const headerStr = stripJsonEdgeGarbage(bytesToUtf8(headerBytes));
|
|
1233
|
+
const payloadStr = stripJsonEdgeGarbage(bytesToUtf8(payloadBytes));
|
|
1234
|
+
let header;
|
|
1235
|
+
try {
|
|
1236
|
+
header = JSON.parse(headerStr);
|
|
1237
|
+
} catch {
|
|
1238
|
+
throw new Error("Cannot parse header!");
|
|
1239
|
+
}
|
|
1240
|
+
let payload;
|
|
1241
|
+
try {
|
|
1242
|
+
const { iat, iatDate, exp, expDate, nbf, nbfDate, aud, sub, iss, ...rest } = JSON.parse(payloadStr);
|
|
1243
|
+
payload = {
|
|
1244
|
+
iat,
|
|
1245
|
+
iatDate: iatDate ? new Date(iatDate) : _secToDate(iat),
|
|
1246
|
+
...exp && { exp },
|
|
1247
|
+
...exp && { expDate: expDate ? new Date(expDate) : _secToDate(exp) },
|
|
1248
|
+
...nbf && { nbf },
|
|
1249
|
+
...nbf && { nbfDate: nbfDate ? new Date(nbfDate) : _secToDate(nbf) },
|
|
1250
|
+
...aud && { aud },
|
|
1251
|
+
...sub && { sub },
|
|
1252
|
+
...iss && { iss },
|
|
1253
|
+
...rest
|
|
1254
|
+
};
|
|
1255
|
+
} catch {
|
|
1256
|
+
throw new Error("Cannot parse payload!");
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
header,
|
|
1260
|
+
payload,
|
|
1261
|
+
signature,
|
|
1262
|
+
signingInput: `${hdr}.${pld}`
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* * Creates and signs a new token with the given payload and options.
|
|
1267
|
+
*
|
|
1268
|
+
* @param payload - Custom data to include in the token payload.
|
|
1269
|
+
* Must be a `non-empty object`.
|
|
1270
|
+
* @param options - Optional configuration for token claims and expiration.
|
|
1271
|
+
*
|
|
1272
|
+
* @returns A signed token string in the format `header.payload.signature`.
|
|
1273
|
+
*
|
|
1274
|
+
* @throws If payload is not a valid object.
|
|
1275
|
+
*
|
|
1276
|
+
* @remarks
|
|
1277
|
+
* - **The token structure follows JWT format:**
|
|
1278
|
+
* - Header: Contains algorithm (`HS256`) and token type (`SIGNET+JWT`)
|
|
1279
|
+
* - Payload: Includes standard claims (`iat`, `exp`, `nbf`, `aud`, `sub`, `iss`) plus custom data
|
|
1280
|
+
* - Signature: `HMAC-SHA256(signingInput, secret)` (the result of the hash)
|
|
1281
|
+
* - Signing Inputs: `base64(header) + "." + base64(payload)` (the string that gets hashed)
|
|
1282
|
+
*
|
|
1283
|
+
* - **Automatic claims added:**
|
|
1284
|
+
* - `iat` (issued at): Current time in seconds
|
|
1285
|
+
* - `iatDate`: Current time as Date object
|
|
1286
|
+
* - If `expiresIn` is provided: `exp` and `expDate`
|
|
1287
|
+
* - If `notBefore` is provided: `nbf` and `nbfDate`
|
|
1288
|
+
*
|
|
1289
|
+
* @example
|
|
1290
|
+
* ```typescript
|
|
1291
|
+
* // Basic token with custom data
|
|
1292
|
+
* const token = signet.sign({ userId: 123, name: 'John' });
|
|
1293
|
+
*
|
|
1294
|
+
* // Token with expiration and claims
|
|
1295
|
+
* const token = signet.sign(
|
|
1296
|
+
* { userId: 123 },
|
|
1297
|
+
* {
|
|
1298
|
+
* expiresIn: '2h',
|
|
1299
|
+
* audience: 'api.example.com',
|
|
1300
|
+
* issuer: 'auth-service',
|
|
1301
|
+
* subject: 'user-123'
|
|
1302
|
+
* }
|
|
1303
|
+
* );
|
|
1304
|
+
*
|
|
1305
|
+
* // Token valid after 5 minutes
|
|
1306
|
+
* const token = signet.sign(
|
|
1307
|
+
* { action: 'reset-password' },
|
|
1308
|
+
* { notBefore: '5m' }
|
|
1309
|
+
* );
|
|
1310
|
+
* ```
|
|
1311
|
+
*/
|
|
1312
|
+
sign(payload, options) {
|
|
1313
|
+
if (!isNotEmptyObject(payload)) throw new Error("Payload must be a valid object!");
|
|
1314
|
+
const { expiresIn, notBefore, audience, issuer, subject } = options || {};
|
|
1315
|
+
const iat = _toSeconds(Date.now());
|
|
1316
|
+
const $payload = {
|
|
1317
|
+
iat,
|
|
1318
|
+
iatDate: _secToDate(iat),
|
|
1319
|
+
...expiresIn && { exp: iat + _toSeconds(parseMSec(expiresIn)) },
|
|
1320
|
+
...expiresIn && { expDate: _secToDate(iat + _toSeconds(parseMSec(expiresIn))) },
|
|
1321
|
+
...notBefore && { nbf: iat + _toSeconds(parseMSec(notBefore)) },
|
|
1322
|
+
...notBefore && { nbfDate: _secToDate(iat + _toSeconds(parseMSec(notBefore))) },
|
|
1323
|
+
...audience && { aud: audience },
|
|
1324
|
+
...subject && { sub: subject },
|
|
1325
|
+
...issuer && { iss: issuer },
|
|
1326
|
+
...payload
|
|
1327
|
+
};
|
|
1328
|
+
const headerJson = stableStringify({
|
|
1329
|
+
alg: "HS256",
|
|
1330
|
+
typ: "SIGNET+JWT"
|
|
1331
|
+
});
|
|
1332
|
+
const payloadJson = stableStringify($payload);
|
|
1333
|
+
const headerB = utf8ToBytes(headerJson);
|
|
1334
|
+
const payloadB = utf8ToBytes(payloadJson);
|
|
1335
|
+
const signingInput = `${bytesToBase64(headerB)}.${bytesToBase64(payloadB)}`;
|
|
1336
|
+
return `${signingInput}.${bytesToBase64(hmacSha256(this.#secretBytes, utf8ToBytes(signingInput)))}`;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* * Decodes a token without verifying its signature.
|
|
1340
|
+
*
|
|
1341
|
+
* @typeParam T - Type of custom data in the token payload.
|
|
1342
|
+
* @param token - The token string to decode.
|
|
1343
|
+
*
|
|
1344
|
+
* @returns The decoded token parts including header, payload, and signatures.
|
|
1345
|
+
*
|
|
1346
|
+
* @throws If the token is malformed, empty, or cannot be parsed.
|
|
1347
|
+
*
|
|
1348
|
+
* @remarks
|
|
1349
|
+
* - Use this method when you need to inspect token contents without verification.
|
|
1350
|
+
* - **Warning:** This does not validate the signature, so the data may have been tampered with.
|
|
1351
|
+
* - Always use {@link verify} method for security-critical operations.
|
|
1352
|
+
* - The payload includes both timestamp values (numbers) and {@link Date} objects for convenience.
|
|
1353
|
+
*
|
|
1354
|
+
* @example
|
|
1355
|
+
* ```typescript
|
|
1356
|
+
* // Decode token to inspect contents
|
|
1357
|
+
* const decoded = signet.decode(token);
|
|
1358
|
+
* console.log('Header:', decoded.header);
|
|
1359
|
+
* console.log('Payload:', decoded.payload);
|
|
1360
|
+
* console.log('Signature:', decoded.signature);
|
|
1361
|
+
*
|
|
1362
|
+
* // Access custom payload data with type safety
|
|
1363
|
+
* const decoded = signet.decode<{ userId: number }>(token);
|
|
1364
|
+
* const userId = decoded.payload.userId; // Type: number
|
|
1365
|
+
* ```
|
|
1366
|
+
*/
|
|
1367
|
+
decode(token) {
|
|
1368
|
+
return this.#decode(token);
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* * Checks if a token has expired based on its `exp` claim.
|
|
1372
|
+
*
|
|
1373
|
+
* @param token - The token to check.
|
|
1374
|
+
*
|
|
1375
|
+
* @returns `true` if the token has an `exp` claim and current time is past it,
|
|
1376
|
+
* `false` if token has no expiration or is still valid.
|
|
1377
|
+
*
|
|
1378
|
+
* @throws If the token is malformed or cannot be decoded.
|
|
1379
|
+
*
|
|
1380
|
+
* @remarks
|
|
1381
|
+
* - Tokens without `exp` claim are considered non-expiring (returns `false`)
|
|
1382
|
+
* - Uses current system time for comparison ({@link Date.now()})
|
|
1383
|
+
* - Does not verify the signature (use only with trusted tokens or after verification)
|
|
1384
|
+
*
|
|
1385
|
+
* @example
|
|
1386
|
+
* ```typescript
|
|
1387
|
+
* // Check expiration
|
|
1388
|
+
* if (signet.hasExpired(token)) {
|
|
1389
|
+
* console.log('Token has expired');
|
|
1390
|
+
* // Prompt user to re-authenticate
|
|
1391
|
+
* }
|
|
1392
|
+
*
|
|
1393
|
+
* // Use with other validation
|
|
1394
|
+
* const isValid = !signet.hasExpired(token) && !signet.isTooEarly(token);
|
|
1395
|
+
* ```
|
|
1396
|
+
*/
|
|
1397
|
+
hasExpired(token) {
|
|
1398
|
+
const { exp } = this.#decode(token).payload;
|
|
1399
|
+
return exp ? _toSeconds(Date.now()) > exp : false;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* * Checks if a token's `nbf` (not-before) claim indicates it's too early to use.
|
|
1403
|
+
*
|
|
1404
|
+
* @param token - The token to check.
|
|
1405
|
+
*
|
|
1406
|
+
* @returns `true` if the token has an `nbf` claim and current time is before it,
|
|
1407
|
+
* `false` if token has no `nbf` claim or is already valid.
|
|
1408
|
+
*
|
|
1409
|
+
* @throws If the token is malformed or cannot be decoded.
|
|
1410
|
+
*
|
|
1411
|
+
* @remarks
|
|
1412
|
+
* - Useful for implementing time-based access control, like activation links that shouldn't be used until a certain time.
|
|
1413
|
+
* - Uses current system time for comparison ({@link Date.now()})
|
|
1414
|
+
* - Does not verify the signature (use only with trusted tokens or after verification)
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* ```typescript
|
|
1418
|
+
* // Check if token is active yet
|
|
1419
|
+
* if (signet.isTooEarly(token)) {
|
|
1420
|
+
* console.log('Token not valid yet');
|
|
1421
|
+
* // Wait before using
|
|
1422
|
+
* }
|
|
1423
|
+
* ```
|
|
1424
|
+
*/
|
|
1425
|
+
isTooEarly(token) {
|
|
1426
|
+
const { nbf } = this.#decode(token).payload;
|
|
1427
|
+
return nbf ? _toSeconds(Date.now()) < nbf : false;
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* * Validates a token's `iss` (issuer) claim against an expected value.
|
|
1431
|
+
*
|
|
1432
|
+
* @param token - The token to check.
|
|
1433
|
+
* @param expected - The expected issuer value. If `undefined`, always returns `false`.
|
|
1434
|
+
*
|
|
1435
|
+
* @returns `true` if the token has an `iss` claim that doesn't match the expected value,
|
|
1436
|
+
* `false` if issuer matches, token has no issuer claim, or expected issuer is undefined.
|
|
1437
|
+
*
|
|
1438
|
+
* @throws If the token is malformed or cannot be decoded.
|
|
1439
|
+
*
|
|
1440
|
+
* @remarks Use this to ensure tokens come from trusted sources in multi-issuer scenarios.
|
|
1441
|
+
*
|
|
1442
|
+
* @example
|
|
1443
|
+
* ```typescript
|
|
1444
|
+
* // Validate issuer
|
|
1445
|
+
* if (signet.isInvalidIssuer(token, 'auth-service')) {
|
|
1446
|
+
* console.log('Token from unexpected issuer');
|
|
1447
|
+
* // Reject token
|
|
1448
|
+
* }
|
|
1449
|
+
*
|
|
1450
|
+
* // With optional issuer check
|
|
1451
|
+
* const issuer = process.env.EXPECTED_ISSUER;
|
|
1452
|
+
* if (issuer && signet.isInvalidIssuer(token, issuer)) {
|
|
1453
|
+
* throw new Error('Invalid issuer');
|
|
1454
|
+
* }
|
|
1455
|
+
* ```
|
|
1456
|
+
*/
|
|
1457
|
+
isInvalidIssuer(token, expected) {
|
|
1458
|
+
if (!expected) return false;
|
|
1459
|
+
const { iss } = this.#decode(token).payload;
|
|
1460
|
+
return iss ? iss !== expected : false;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* * Validates a token's `aud` (audience) claim against expected values.
|
|
1464
|
+
*
|
|
1465
|
+
* @param token - The token to check.
|
|
1466
|
+
* @param expected - The expected audience(s). Can be a string or array of strings.
|
|
1467
|
+
* If `undefined`, always returns `false`.
|
|
1468
|
+
*
|
|
1469
|
+
* @returns `true` if the token has an `aud` claim and none of its values match any of the expected audiences, `false` otherwise.
|
|
1470
|
+
*
|
|
1471
|
+
* @throws If the token is malformed or cannot be decoded.
|
|
1472
|
+
*
|
|
1473
|
+
* @remarks
|
|
1474
|
+
* - Tokens can have single audience (string) or multiple audiences (string[])
|
|
1475
|
+
* - Returns `false` (valid) if at least one audience matches
|
|
1476
|
+
* - Useful for multi-tenant or multi-service architectures
|
|
1477
|
+
*
|
|
1478
|
+
* @example
|
|
1479
|
+
* ```typescript
|
|
1480
|
+
* // Single audience check
|
|
1481
|
+
* if (signet.isInvalidAudience(token, 'api.example.com')) {
|
|
1482
|
+
* console.log('Token not intended for this audience');
|
|
1483
|
+
* }
|
|
1484
|
+
*
|
|
1485
|
+
* // Multiple allowed audiences
|
|
1486
|
+
* const validAudiences = ['web-app', 'mobile-app', 'admin-panel'];
|
|
1487
|
+
* if (signet.isInvalidAudience(token, validAudiences)) {
|
|
1488
|
+
* throw new Error('Invalid audience');
|
|
1489
|
+
* }
|
|
1490
|
+
*
|
|
1491
|
+
* // Token with multiple audiences
|
|
1492
|
+
* // Token payload: { aud: ['web-app', 'mobile-app'] }
|
|
1493
|
+
* // Check if at least one matches
|
|
1494
|
+
* const isValid = !signet.isInvalidAudience(token, ['web-app', 'admin-panel']);
|
|
1495
|
+
* // Returns false (valid) because 'web-app' matches
|
|
1496
|
+
* ```
|
|
1497
|
+
*/
|
|
1498
|
+
isInvalidAudience(token, expected) {
|
|
1499
|
+
if (!expected) return false;
|
|
1500
|
+
const { aud } = this.#decode(token).payload;
|
|
1501
|
+
if (!aud) return false;
|
|
1502
|
+
const payloadAud = Array.isArray(aud) ? aud : [aud];
|
|
1503
|
+
const expectedAud = Array.isArray(expected) ? expected : [expected];
|
|
1504
|
+
return payloadAud.some((tokenAud) => expectedAud.includes(tokenAud));
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* * Validates a token's `sub` (subject) claim against an expected value.
|
|
1508
|
+
*
|
|
1509
|
+
* @param token - The token to check.
|
|
1510
|
+
* @param expected - The expected subject value. If `undefined`, always returns `false`.
|
|
1511
|
+
*
|
|
1512
|
+
* @returns `true` if the token has a `sub` claim that doesn't match the expected value,
|
|
1513
|
+
* `false` if subject matches, token has no subject claim, or expected subject is undefined.
|
|
1514
|
+
*
|
|
1515
|
+
* @throws If the token is malformed or cannot be decoded.
|
|
1516
|
+
*
|
|
1517
|
+
* @remarks
|
|
1518
|
+
* - Use this to ensure tokens are being used by the intended user/entity.
|
|
1519
|
+
* - Common for authorization checks where tokens should be user-specific.
|
|
1520
|
+
*
|
|
1521
|
+
* @example
|
|
1522
|
+
* ```typescript
|
|
1523
|
+
* // Validate subject
|
|
1524
|
+
* const userId = 'user-123';
|
|
1525
|
+
* if (signet.isInvalidSubject(token, userId)) {
|
|
1526
|
+
* console.log('Token not for this user');
|
|
1527
|
+
* // Reject request
|
|
1528
|
+
* }
|
|
1529
|
+
*
|
|
1530
|
+
* // Optional subject validation
|
|
1531
|
+
* const expectedSubject = getExpectedSubjectFromRequest();
|
|
1532
|
+
* if (expectedSubject && signet.isInvalidSubject(token, expectedSubject)) {
|
|
1533
|
+
* return response.status(403).send('Invalid subject');
|
|
1534
|
+
* }
|
|
1535
|
+
* ```
|
|
1536
|
+
*/
|
|
1537
|
+
isInvalidSubject(token, expected) {
|
|
1538
|
+
if (!expected) return false;
|
|
1539
|
+
const { sub } = this.#decode(token).payload;
|
|
1540
|
+
return sub ? sub !== expected : false;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* * Verifies a token's signature and validates all claims.
|
|
1544
|
+
*
|
|
1545
|
+
* @typeParam T - Type of custom data in the token payload.
|
|
1546
|
+
* @param token - The token string to verify.
|
|
1547
|
+
* @param options - Optional validation criteria for token claims.
|
|
1548
|
+
*
|
|
1549
|
+
* @returns A {@link VerifiedToken} object indicating success or failure.
|
|
1550
|
+
* - If valid: `{ isValid: true, payload: SignetPayload<T> }`
|
|
1551
|
+
* - If invalid: `{ isValid: false, error: string }`
|
|
1552
|
+
*
|
|
1553
|
+
* @remarks
|
|
1554
|
+
* - **Performs the following checks in order:**
|
|
1555
|
+
* - Token structure (3 parts separated by dots)
|
|
1556
|
+
* - Base64 decoding of header and payload
|
|
1557
|
+
* - JSON parsing of header and payload
|
|
1558
|
+
* - Signature verification (constant-time comparison)
|
|
1559
|
+
* - Expiration check (if `exp` claim exists)
|
|
1560
|
+
* - Not-before check (if `nbf` claim exists)
|
|
1561
|
+
* - Issuer validation (if provided in options)
|
|
1562
|
+
* - Audience validation (if provided in options)
|
|
1563
|
+
* - Subject validation (if provided in options)
|
|
1564
|
+
*
|
|
1565
|
+
* - **This is the recommended method for most token validation scenarios.**
|
|
1566
|
+
*
|
|
1567
|
+
* @example
|
|
1568
|
+
* ```typescript
|
|
1569
|
+
* // Basic verification
|
|
1570
|
+
* const result = signet.verify(token);
|
|
1571
|
+
* if (result.isValid) {
|
|
1572
|
+
* console.log('Valid token:', result.payload);
|
|
1573
|
+
* } else {
|
|
1574
|
+
* console.log('Invalid token:', result.error);
|
|
1575
|
+
* }
|
|
1576
|
+
*
|
|
1577
|
+
* // With claim validation
|
|
1578
|
+
* const result = signet.verify(token, {
|
|
1579
|
+
* audience: 'api.example.com',
|
|
1580
|
+
* issuer: 'auth-service',
|
|
1581
|
+
* subject: 'user-123'
|
|
1582
|
+
* });
|
|
1583
|
+
*
|
|
1584
|
+
* // Type-safe custom payload
|
|
1585
|
+
* interface UserToken {
|
|
1586
|
+
* userId: number;
|
|
1587
|
+
* role: string;
|
|
1588
|
+
* }
|
|
1589
|
+
* const result = signet.verify<UserToken>(token);
|
|
1590
|
+
* if (result.isValid) {
|
|
1591
|
+
* const { userId, role } = result.payload;
|
|
1592
|
+
* // userId and role are typed
|
|
1593
|
+
* }
|
|
1594
|
+
* ```
|
|
1595
|
+
*/
|
|
1596
|
+
verify(token, options) {
|
|
1597
|
+
try {
|
|
1598
|
+
const { signature, signingInput, payload } = this.#decode(token);
|
|
1599
|
+
const { audience, issuer, subject } = options || {};
|
|
1600
|
+
if (!_constantTimeEquals(signature, bytesToBase64(hmacSha256(this.#secretBytes, utf8ToBytes(signingInput))))) throw new Error("Invalid or tampered signature!");
|
|
1601
|
+
if (this.hasExpired(token)) throw new Error("Token has expired!");
|
|
1602
|
+
if (this.isTooEarly(token)) throw new Error("Token is not active yet!");
|
|
1603
|
+
if (this.isInvalidIssuer(token, issuer)) throw new Error("Invalid token issuer!");
|
|
1604
|
+
if (this.isInvalidAudience(token, audience)) throw new Error("Invalid token audience(s!");
|
|
1605
|
+
if (this.isInvalidSubject(token, subject)) throw new Error("Invalid token subject!");
|
|
1606
|
+
return {
|
|
1607
|
+
isValid: true,
|
|
1608
|
+
payload
|
|
1609
|
+
};
|
|
1610
|
+
} catch (e) {
|
|
1611
|
+
return {
|
|
1612
|
+
isValid: false,
|
|
1613
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* * Verifies a token and throws an error if invalid.
|
|
1619
|
+
*
|
|
1620
|
+
* @typeParam T - Type of custom data in the token payload.
|
|
1621
|
+
* @param token - The token string to verify.
|
|
1622
|
+
* @param options - Optional validation criteria for token claims.
|
|
1623
|
+
*
|
|
1624
|
+
* @returns A valid {@link VerifiedToken} with `isValid: true`.
|
|
1625
|
+
*
|
|
1626
|
+
* @throws If the token is invalid, with a message describing the failure.
|
|
1627
|
+
*
|
|
1628
|
+
* @remarks
|
|
1629
|
+
* - This method is a convenience wrapper around {@link verify} that throws instead of returning an error object.
|
|
1630
|
+
* - Useful for `express`-style middleware or when you want to handle authentication failures with exceptions.
|
|
1631
|
+
* - The thrown error message is the same as the `error` property in the invalid result from {@link verify}.
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```typescript
|
|
1635
|
+
* // Use in middleware/guard
|
|
1636
|
+
* function authMiddleware(req, res, next) {
|
|
1637
|
+
* const token = req.headers.authorization?.replace('Bearer ', '');
|
|
1638
|
+
*
|
|
1639
|
+
* try {
|
|
1640
|
+
* const result = signet.verifyOrThrow(token, {
|
|
1641
|
+
* audience: 'api.example.com'
|
|
1642
|
+
* });
|
|
1643
|
+
* req.user = result.payload;
|
|
1644
|
+
* next();
|
|
1645
|
+
* } catch (error) {
|
|
1646
|
+
* res.status(401).json({ error: error.message });
|
|
1647
|
+
* }
|
|
1648
|
+
* }
|
|
1649
|
+
*
|
|
1650
|
+
* // In application code
|
|
1651
|
+
* try {
|
|
1652
|
+
* const result = signet.verifyOrThrow(token);
|
|
1653
|
+
* // Token is guaranteed valid here
|
|
1654
|
+
* processUserRequest(result.payload);
|
|
1655
|
+
* } catch (error) {
|
|
1656
|
+
* handleAuthError(error);
|
|
1657
|
+
* }
|
|
1658
|
+
* ```
|
|
1659
|
+
*/
|
|
1660
|
+
verifyOrThrow(token, options) {
|
|
1661
|
+
const res = this.verify(token, options);
|
|
1662
|
+
if (!res.isValid) throw new Error(res.error || "Invalid, malformed or expired token!");
|
|
1663
|
+
return res;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* * Extracts only the payload from a token without full verification.
|
|
1667
|
+
*
|
|
1668
|
+
* @typeParam T - Type of custom data in the token payload.
|
|
1669
|
+
* @param token - The token string to decode.
|
|
1670
|
+
*
|
|
1671
|
+
* @returns The token payload including standard claims and custom data.
|
|
1672
|
+
*
|
|
1673
|
+
* @throws If the token is malformed, empty, or cannot be parsed.
|
|
1674
|
+
*
|
|
1675
|
+
* @remarks
|
|
1676
|
+
* - This is a convenience method equivalent to `decode(token).payload`.
|
|
1677
|
+
*
|
|
1678
|
+
* - **Security Note:** This method does NOT verify the token signature.
|
|
1679
|
+
*
|
|
1680
|
+
* - **Only use it when:**
|
|
1681
|
+
* - You've already verified the token elsewhere
|
|
1682
|
+
* - The token comes from a trusted source
|
|
1683
|
+
* - You're debugging or logging
|
|
1684
|
+
* - The operation is not security-critical
|
|
1685
|
+
*
|
|
1686
|
+
* - For security-sensitive operations, always use {@link verify} or {@link verifyOrThrow} method.
|
|
1687
|
+
*
|
|
1688
|
+
* @example
|
|
1689
|
+
* ```typescript
|
|
1690
|
+
* // Quick payload extraction for non-critical operations
|
|
1691
|
+
* const payload = signet.decodePayload(token);
|
|
1692
|
+
* console.log('User ID:', payload.userId);
|
|
1693
|
+
* console.log('Issued at:', payload.iatDate);
|
|
1694
|
+
*
|
|
1695
|
+
* // Type-safe with custom interface
|
|
1696
|
+
* interface AppToken {
|
|
1697
|
+
* userId: number;
|
|
1698
|
+
* permissions: string[];
|
|
1699
|
+
* }
|
|
1700
|
+
* const payload = signet.decodePayload<AppToken>(token);
|
|
1701
|
+
* const canDelete = payload.permissions.includes('delete');
|
|
1702
|
+
* ```
|
|
1703
|
+
*/
|
|
1704
|
+
decodePayload(token) {
|
|
1705
|
+
return this.#decode(token).payload;
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
//#endregion
|
|
1710
|
+
//#region src/hash/TextCodec.ts
|
|
1711
|
+
/**
|
|
1712
|
+
* @class `TextCodec` provides **UTF-8–safe** conversions between `text`, `hex`, `binary`, and `Base64` representations using byte-level transformations.
|
|
1713
|
+
*
|
|
1714
|
+
* @example
|
|
1715
|
+
* TextCodec.utf8ToHex('ভাষা'); // 'e0 a6 ad e0 a6 be e0 a6 b7 e0 a6 be'
|
|
1716
|
+
* TextCodec.hexToUtf8('e0 a6 ad e0 a6 be'); // 'ভা'
|
|
1717
|
+
*/
|
|
1718
|
+
var TextCodec = class {
|
|
1719
|
+
constructor() {}
|
|
1720
|
+
/**
|
|
1721
|
+
* @static Validates whether a string represents a valid hexadecimal byte sequence.
|
|
1722
|
+
*
|
|
1723
|
+
* @param hex - Hex string, spaced or un-spaced (e.g. "ff 0a" or "ff0a")
|
|
1724
|
+
* @returns `true` if the input is valid hex byte string
|
|
1725
|
+
*
|
|
1726
|
+
* @example
|
|
1727
|
+
* TextCodec.isValidHex('ff 0a');
|
|
1728
|
+
*/
|
|
1729
|
+
static isValidHex(hex) {
|
|
1730
|
+
return isHexString(hex);
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* @static Validates whether a string represents a valid binary byte sequence.
|
|
1734
|
+
*
|
|
1735
|
+
* @param binary - Binary string, spaced or un-spaced
|
|
1736
|
+
* @returns `true` if the input is valid binary byte string
|
|
1737
|
+
*
|
|
1738
|
+
* @example
|
|
1739
|
+
* TextCodec.isValidBinary('01000001');
|
|
1740
|
+
*/
|
|
1741
|
+
static isValidBinary(binary) {
|
|
1742
|
+
return isBinaryString(binary);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* @static Validates whether a string represents a valid Base64-encoded string.
|
|
1746
|
+
*
|
|
1747
|
+
* @param b64 - Base64 string to check
|
|
1748
|
+
* @returns `true` if the input is valid Base64-encoded string
|
|
1749
|
+
*
|
|
1750
|
+
* @example
|
|
1751
|
+
* TextCodec.isValidBase64('SGVsbG8=');
|
|
1752
|
+
*/
|
|
1753
|
+
static isValidBase64(b64) {
|
|
1754
|
+
return isBase64(b64);
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* @static Converts UTF-8 text into hexadecimal byte representation.
|
|
1758
|
+
*
|
|
1759
|
+
* @param text - UTF-8 text to convert
|
|
1760
|
+
* @param spaced - Whether to separate bytes with spaces, defaults to `true`
|
|
1761
|
+
* @returns Hexadecimal byte string
|
|
1762
|
+
*
|
|
1763
|
+
* @example
|
|
1764
|
+
* TextCodec.utf8ToHex('Hi');
|
|
1765
|
+
*/
|
|
1766
|
+
static utf8ToHex(text, spaced = true) {
|
|
1767
|
+
return [...utf8ToBytes(text)].map((b) => _padStartWith0(b, "hex")).join(spaced ? " " : "");
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* @static Converts UTF-8 text into binary byte representation.
|
|
1771
|
+
*
|
|
1772
|
+
* @param text - UTF-8 text to convert
|
|
1773
|
+
* @param spaced - Whether to separate bytes with spaces, defaults to `true`
|
|
1774
|
+
* @returns Binary byte string
|
|
1775
|
+
*
|
|
1776
|
+
* @example
|
|
1777
|
+
* TextCodec.utf8ToBinary('A');
|
|
1778
|
+
*/
|
|
1779
|
+
static utf8ToBinary(text, spaced = true) {
|
|
1780
|
+
return [...utf8ToBytes(text)].map((b) => _padStartWith0(b, "binary")).join(spaced ? " " : "");
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* @static Converts hexadecimal byte string into UTF-8 text.
|
|
1784
|
+
*
|
|
1785
|
+
* @param hex - Hexadecimal byte string
|
|
1786
|
+
* @returns Decoded UTF-8 text
|
|
1787
|
+
*
|
|
1788
|
+
* @example
|
|
1789
|
+
* TextCodec.hexToUtf8('48 69');
|
|
1790
|
+
*/
|
|
1791
|
+
static hexToUtf8(hex) {
|
|
1792
|
+
return bytesToUtf8(hexToBytes(hex));
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* @static Converts binary byte string into UTF-8 text.
|
|
1796
|
+
*
|
|
1797
|
+
* @param binary - Binary byte string
|
|
1798
|
+
* @returns Decoded UTF-8 text
|
|
1799
|
+
*
|
|
1800
|
+
* @example
|
|
1801
|
+
* TextCodec.binaryToUtf8('01001000 01101001');
|
|
1802
|
+
*/
|
|
1803
|
+
static binaryToUtf8(binary) {
|
|
1804
|
+
if (!isBinaryString(binary)) return "";
|
|
1805
|
+
const bytes = _splitByCharLength(binary, 8).map((b) => parseInt(b, 2));
|
|
1806
|
+
return bytesToUtf8(new Uint8Array(bytes));
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* @static Converts hexadecimal byte string into binary byte string.
|
|
1810
|
+
*
|
|
1811
|
+
* @param hex - Hexadecimal byte string
|
|
1812
|
+
* @param spaced - Whether to separate bytes with spaces, defaults to `true`
|
|
1813
|
+
* @returns Binary byte string
|
|
1814
|
+
*
|
|
1815
|
+
* @example
|
|
1816
|
+
* TextCodec.hexToBinary('ff');
|
|
1817
|
+
*/
|
|
1818
|
+
static hexToBinary(hex, spaced = true) {
|
|
1819
|
+
if (!isHexString(hex)) return "";
|
|
1820
|
+
return _splitByCharLength(hex, 2).map((h) => _padStartWith0(parseInt(h, 16), "binary")).join(spaced ? " " : "");
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* @static Converts binary byte string into hexadecimal byte string.
|
|
1824
|
+
*
|
|
1825
|
+
* @param binary - Binary byte string
|
|
1826
|
+
* @param spaced - Whether to separate bytes with spaces, defaults to `true`
|
|
1827
|
+
* @returns Hexadecimal byte string
|
|
1828
|
+
*
|
|
1829
|
+
* @example
|
|
1830
|
+
* TextCodec.binaryToHex('00000001');
|
|
1831
|
+
*/
|
|
1832
|
+
static binaryToHex(binary, spaced = true) {
|
|
1833
|
+
if (!isBinaryString(binary)) return "";
|
|
1834
|
+
return _splitByCharLength(binary, 8).map((b) => _padStartWith0(parseInt(b, 2), "hex")).join(spaced ? " " : "");
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* @static Converts a Base64-encoded string into UTF-8 text.
|
|
1838
|
+
*
|
|
1839
|
+
* @param b64 - Base64 encoded string
|
|
1840
|
+
* @returns Decoded UTF-8 text
|
|
1841
|
+
*
|
|
1842
|
+
* @example
|
|
1843
|
+
* TextCodec.base64ToUtf8('SGVsbG8=');
|
|
1844
|
+
*/
|
|
1845
|
+
static base64ToUtf8(b64) {
|
|
1846
|
+
if (!isBase64(b64)) return "";
|
|
1847
|
+
return bytesToUtf8(base64ToBytes(b64));
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* @static Converts UTF-8 text into a Base64-encoded string.
|
|
1851
|
+
*
|
|
1852
|
+
* @param text - UTF-8 text to encode
|
|
1853
|
+
* @returns Base64 encoded string
|
|
1854
|
+
*
|
|
1855
|
+
* @example
|
|
1856
|
+
* TextCodec.utf8ToBase64('Hello');
|
|
1857
|
+
*/
|
|
1858
|
+
static utf8ToBase64(text) {
|
|
1859
|
+
if (!isNonEmptyString(text)) return "";
|
|
1860
|
+
return bytesToBase64(utf8ToBytes(text));
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* @static Converts Base64 directly into hexadecimal byte string.
|
|
1864
|
+
*
|
|
1865
|
+
* @param b64 - Base64 encoded string
|
|
1866
|
+
* @param spaced - Whether to separate bytes with spaces, defaults to `true`
|
|
1867
|
+
* @returns Hexadecimal byte string
|
|
1868
|
+
*
|
|
1869
|
+
* @example
|
|
1870
|
+
* TextCodec.base64ToHex('SGVsbG8=');
|
|
1871
|
+
*/
|
|
1872
|
+
static base64ToHex(b64, spaced = true) {
|
|
1873
|
+
return this.utf8ToHex(this.base64ToUtf8(b64), spaced);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* @static Converts Base64 directly into binary byte string.
|
|
1877
|
+
*
|
|
1878
|
+
* @param b64 - Base64 encoded string
|
|
1879
|
+
* @param spaced - Whether to separate bytes with spaces, defaults to `true`
|
|
1880
|
+
* @returns Binary byte string
|
|
1881
|
+
*
|
|
1882
|
+
* @example
|
|
1883
|
+
* TextCodec.base64ToBinary('SGVsbG8=');
|
|
1884
|
+
*/
|
|
1885
|
+
static base64ToBinary(b64, spaced = true) {
|
|
1886
|
+
return this.utf8ToBinary(this.base64ToUtf8(b64), spaced);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* @static Converts hexadecimal byte string into a Base64 string.
|
|
1890
|
+
*
|
|
1891
|
+
* @param hex - Hexadecimal byte string
|
|
1892
|
+
* @returns Base64 encoded string
|
|
1893
|
+
*
|
|
1894
|
+
* @example
|
|
1895
|
+
* TextCodec.hexToBase64('48 69');
|
|
1896
|
+
*/
|
|
1897
|
+
static hexToBase64(hex) {
|
|
1898
|
+
return this.utf8ToBase64(this.hexToUtf8(hex));
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* @static Converts binary byte string into a Base64 string.
|
|
1902
|
+
*
|
|
1903
|
+
* @param binary - Binary byte string
|
|
1904
|
+
* @returns Base64 encoded string
|
|
1905
|
+
*
|
|
1906
|
+
* @example
|
|
1907
|
+
* TextCodec.binaryToBase64('01001000 01101001');
|
|
1908
|
+
*/
|
|
1909
|
+
static binaryToBase64(binary) {
|
|
1910
|
+
return this.utf8ToBase64(this.binaryToUtf8(binary));
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
//#endregion
|
|
1915
|
+
//#region src/hash/uuid.ts
|
|
1916
|
+
/**
|
|
1917
|
+
* * Generates UUIDs across all major RFC-compliant versions (1, 3, 4, 5, 6, 7, 8), following standards from `RFC4122`. Default version is `v4`.
|
|
1918
|
+
*
|
|
1919
|
+
* - **Version behavior:**
|
|
1920
|
+
* - `v1` → Timestamp & node-identifier–based
|
|
1921
|
+
* - `v3` → MD5(namespace + name)
|
|
1922
|
+
* - `v4` → Pure random (correct variant + version injection)
|
|
1923
|
+
* - `v5` → SHA-1(namespace + name)
|
|
1924
|
+
* - `v6` → Re-ordered timestamp variant of `v1` (lexicographically sortable)
|
|
1925
|
+
* - `v7` → Unix-time–based, monotonic-friendly
|
|
1926
|
+
* - `v8` → Custom layout, '“Future'` variant (timestamp + randomness)
|
|
1927
|
+
*
|
|
1928
|
+
* @param options Controls version, formatting, and required fields for `v3` and `v5`.
|
|
1929
|
+
* @returns A 5-parts UUID string formatted with correct version/variant bits.
|
|
1930
|
+
*
|
|
1931
|
+
* @example
|
|
1932
|
+
* // Generate a random UUID v4
|
|
1933
|
+
* const id = uuid();
|
|
1934
|
+
*
|
|
1935
|
+
* @example
|
|
1936
|
+
* // Generate uppercase v7
|
|
1937
|
+
* const id = uuid({ version: 'v7', uppercase: true });
|
|
1938
|
+
*
|
|
1939
|
+
* @example
|
|
1940
|
+
* // Generate v5 UUID
|
|
1941
|
+
* const id = uuid({
|
|
1942
|
+
* version: 'v5',
|
|
1943
|
+
* namespace: '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
|
|
1944
|
+
* name: 'example'
|
|
1945
|
+
* });
|
|
1946
|
+
*
|
|
1947
|
+
* @remarks
|
|
1948
|
+
* - This utility provides a complete, engine-agnostic UUID generator with full RFC compliance, predictable formatting, and reliable uniqueness characteristics, suitable for browsers, Node.js, and restricted JavaScript runtimes.
|
|
1949
|
+
* - **Notes**
|
|
1950
|
+
* - `v1` and `v6` use a generated pseudo-node identifier.
|
|
1951
|
+
* - `v4` and `v8` uses {@link Math.random} when {@link crypto.getRandomValues} is unavailable, ensuring broad compatibility.
|
|
1952
|
+
* - `v3` and `v5` use internal `MD5`/`SHA-1` implementations and remain fully deterministic.
|
|
1953
|
+
* - `v7` **do not rely on crypto APIs**, preserving engine-agnostic behavior.
|
|
1954
|
+
*
|
|
1955
|
+
* - **Limitations**
|
|
1956
|
+
* - `v1`/`v6`: Node identifier is pseudo-random, not derived from real MAC addresses (for privacy).
|
|
1957
|
+
* - `v3`/`v5`: Hash algorithms (`MD5`/`SHA-1`) follow RFC specs but are not cryptographically secure.
|
|
1958
|
+
* - `v7`: Millisecond precision; extremely high throughput may still cause rare collisions.
|
|
1959
|
+
* - `v8`: Uses a simple timestamp + randomness layout; custom layouts are not supported here.
|
|
1960
|
+
*
|
|
1961
|
+
* - Use {@link https://toolbox.nazmul-nhb.dev/docs/utilities/string/generateRandomID generateRandomID} for customized id generation or {@link https://toolbox.nazmul-nhb.dev/docs/utilities/hash/randomHex randomHex} for hex-only random string with custom length.
|
|
1962
|
+
*/
|
|
1963
|
+
function uuid(options) {
|
|
1964
|
+
const { version = "v4", uppercase = false } = options || {};
|
|
1965
|
+
switch (version) {
|
|
1966
|
+
case "v1": {
|
|
1967
|
+
const timestamp = _uuidTimestamp().toString(16).padStart(15, "0");
|
|
1968
|
+
const vTimeHigh = (parseInt(timestamp.slice(0, -12).padStart(3, "0"), 16) | 4096).toString(16).padStart(4, "0");
|
|
1969
|
+
const clockSeq = _clockSeq14();
|
|
1970
|
+
const clockSeqHi = (parseInt(clockSeq.slice(0, 2), 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
1971
|
+
return _formatUUID(timestamp.slice(-8) + timestamp.slice(-12, -8) + vTimeHigh + clockSeqHi + clockSeq.slice(2) + _randomNode48(), 1, uppercase);
|
|
1972
|
+
}
|
|
1973
|
+
case "v3":
|
|
1974
|
+
if (_isOptionV3V5(options)) return _formatUUID(md5(options.namespace + options.name), 3, uppercase);
|
|
1975
|
+
throw new Error("v3 requires valid namespace (uuid) and name!");
|
|
1976
|
+
case "v4": return _formatUUID(_bytesToRandomHex(new Uint8Array(16)), 4, uppercase);
|
|
1977
|
+
case "v5":
|
|
1978
|
+
if (_isOptionV3V5(options)) return _formatUUID(sha1(options.namespace + options.name).slice(0, 32), 5, uppercase);
|
|
1979
|
+
throw new Error("v5 requires valid namespace (uuid) and name!");
|
|
1980
|
+
case "v6": {
|
|
1981
|
+
const timestamp = _uuidTimestamp().toString(16).padStart(15, "0");
|
|
1982
|
+
const vTimeLow = (parseInt(timestamp.slice(12).padStart(3, "0"), 16) | 24576).toString(16).padStart(4, "0");
|
|
1983
|
+
const clockSeq = _clockSeq14();
|
|
1984
|
+
const clockSeqHi = (parseInt(clockSeq.slice(0, 2), 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
1985
|
+
return _formatUUID(timestamp.slice(0, 8) + timestamp.slice(8, 12) + vTimeLow + clockSeqHi + clockSeq.slice(2) + _randomNode48(), 6, uppercase);
|
|
1986
|
+
}
|
|
1987
|
+
case "v7": return _formatUUID(Date.now().toString(16).padStart(12, "0") + randomHex(20), 7, uppercase);
|
|
1988
|
+
case "v8": {
|
|
1989
|
+
const ts = BigInt(Date.now());
|
|
1990
|
+
const bytes = new Uint8Array(16);
|
|
1991
|
+
let temp = ts;
|
|
1992
|
+
for (let i = 5; i >= 0; i--) {
|
|
1993
|
+
bytes[i] = Number(temp & 255n);
|
|
1994
|
+
temp >>= 8n;
|
|
1995
|
+
}
|
|
1996
|
+
return _formatUUID(_bytesToRandomHex(bytes), 8, uppercase);
|
|
1997
|
+
}
|
|
1998
|
+
default: throw new RangeError("Unsupported UUID version!");
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* * Decodes a UUID into its internal components, including version, variant, timestamps for time-based UUIDs and other metadata.
|
|
2003
|
+
* - Supports `RFC4122` UUID versions: 1-8.
|
|
2004
|
+
*
|
|
2005
|
+
* @param uuid The UUID string to decode.
|
|
2006
|
+
* @returns A structured `DecodedUUID` object, or `null` for invalid UUIDs.
|
|
2007
|
+
*
|
|
2008
|
+
* @example
|
|
2009
|
+
* const info = decodeUUID("f47ac10b-58cc-4372-a567-0e02b2c3d479");
|
|
2010
|
+
*
|
|
2011
|
+
* @example
|
|
2012
|
+
* const info = decodeUUID(uuid({ version: "v1" }));
|
|
2013
|
+
*
|
|
2014
|
+
* @remarks
|
|
2015
|
+
* - Provides a cross-runtime UUID decoder covering essential metadata and timestamp interpretation for time-ordered UUID versions.
|
|
2016
|
+
* - **Notes**
|
|
2017
|
+
* - `v1/v6` timestamps are converted from the UUID epoch (1582-10-15) to standard Unix milliseconds.
|
|
2018
|
+
* - `v6` timestamps are lexicographically sortable and decoded accordingly.
|
|
2019
|
+
* - `v7` timestamps map directly to Unix time (48-bit millisecond precision).
|
|
2020
|
+
* - `v8` decoding is minimal because layouts are intentionally user-defined.
|
|
2021
|
+
*
|
|
2022
|
+
* - **Limitations**
|
|
2023
|
+
* - `v2` decoding is not implemented specifically.
|
|
2024
|
+
* - `v8` decoding only returns timestamp if it matches a known layout.
|
|
2025
|
+
* - `v3/v5` hash UUIDs contain no timestamp information.
|
|
2026
|
+
*/
|
|
2027
|
+
function decodeUUID(uuid) {
|
|
2028
|
+
if (!isUUID(uuid)) return null;
|
|
2029
|
+
const plain = uuid.replace(/-/g, "");
|
|
2030
|
+
const parts = uuid.toLowerCase().split("-");
|
|
2031
|
+
const version = parseInt(parts[2][0], 16);
|
|
2032
|
+
const variantNibble = parseInt(parts[3][0], 16);
|
|
2033
|
+
let variant = "RFC4122";
|
|
2034
|
+
if ((variantNibble & 8) === 0) variant = "NCS";
|
|
2035
|
+
else if ((variantNibble & 12) === 8) variant = "RFC4122";
|
|
2036
|
+
else if ((variantNibble & 14) === 12) variant = "Microsoft";
|
|
2037
|
+
else variant = "Future";
|
|
2038
|
+
const decoded = {
|
|
2039
|
+
version,
|
|
2040
|
+
variant,
|
|
2041
|
+
raw: uuid,
|
|
2042
|
+
plain,
|
|
2043
|
+
singleInt: BigInt("0x" + plain)
|
|
2044
|
+
};
|
|
2045
|
+
const UUID_EPOCH_DIFF = 122192928000000000n;
|
|
2046
|
+
if (version === 1 || version === 6) {
|
|
2047
|
+
let tsHex = parts[2].slice(1) + parts[1] + parts[0];
|
|
2048
|
+
if (version === 6) tsHex = parts[0] + parts[1] + parts[2].slice(1);
|
|
2049
|
+
decoded.timestamp = Number((BigInt("0x" + tsHex) - UUID_EPOCH_DIFF) / 10000n);
|
|
2050
|
+
decoded.node = parts[4];
|
|
2051
|
+
return decoded;
|
|
2052
|
+
}
|
|
2053
|
+
if (version === 7 || version === 8) {
|
|
2054
|
+
const tsHex = parts.join("").slice(0, 12);
|
|
2055
|
+
decoded.variant = version === 7 ? "RFC4122" : "Future";
|
|
2056
|
+
decoded.timestamp = parseInt(tsHex, 16);
|
|
2057
|
+
return decoded;
|
|
2058
|
+
}
|
|
2059
|
+
return decoded;
|
|
2060
|
+
}
|
|
2061
|
+
/** Check if a value is UUID version 1 */
|
|
2062
|
+
function isUUIDv1(value) {
|
|
2063
|
+
return _checkUUIDVersion(value, "1");
|
|
2064
|
+
}
|
|
2065
|
+
/** Check if a value is UUID version 2 */
|
|
2066
|
+
function isUUIDv2(value) {
|
|
2067
|
+
return _checkUUIDVersion(value, "2");
|
|
2068
|
+
}
|
|
2069
|
+
/** Check if a value is UUID version 3 */
|
|
2070
|
+
function isUUIDv3(value) {
|
|
2071
|
+
return _checkUUIDVersion(value, "3");
|
|
2072
|
+
}
|
|
2073
|
+
/** Check if a value is UUID version 4 */
|
|
2074
|
+
function isUUIDv4(value) {
|
|
2075
|
+
return _checkUUIDVersion(value, "4");
|
|
2076
|
+
}
|
|
2077
|
+
/** Check if a value is UUID version 5 */
|
|
2078
|
+
function isUUIDv5(value) {
|
|
2079
|
+
return _checkUUIDVersion(value, "5");
|
|
2080
|
+
}
|
|
2081
|
+
/** Check if a value is UUID version 6 */
|
|
2082
|
+
function isUUIDv6(value) {
|
|
2083
|
+
return _checkUUIDVersion(value, "6");
|
|
2084
|
+
}
|
|
2085
|
+
/** Check if a value is UUID version 7 */
|
|
2086
|
+
function isUUIDv7(value) {
|
|
2087
|
+
return _checkUUIDVersion(value, "7");
|
|
2088
|
+
}
|
|
2089
|
+
/** Check if a value is UUID version 8 */
|
|
2090
|
+
function isUUIDv8(value) {
|
|
2091
|
+
return _checkUUIDVersion(value, "8");
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
//#endregion
|
|
2095
|
+
export { Cipher, Signet, TextCodec, base64ToBytes, bytesToBase64, bytesToHex, bytesToUtf8, concatBytes, decodeUUID, generateRandomID, generateRandomID as randomID, hexToBytes, hmacSha256, intTo4BytesBE, isUUID, isUUIDv1, isUUIDv2, isUUIDv3, isUUIDv4, isUUIDv5, isUUIDv6, isUUIDv7, isUUIDv8, md5, randomHex, sha1, sha256, sha256Bytes, uint8To32ArrayBE, utf8ToBytes, uuid };
|