@fast-china/utils 2.1.0 → 2.1.2
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/CHANGELOG.md +24 -0
- package/README.md +32 -3
- package/README.zh.md +32 -3
- package/dist/array/index.mjs +1 -1
- package/dist/array/index.mjs.map +1 -1
- package/dist/async/index.mjs +21 -19
- package/dist/async/index.mjs.map +1 -1
- package/dist/base64/index.d.mts +10 -9
- package/dist/base64/index.mjs +15 -15
- package/dist/base64/index.mjs.map +1 -1
- package/dist/color/index.mjs +4 -4
- package/dist/color/index.mjs.map +1 -1
- package/dist/crypto/index.d.mts +9 -8
- package/dist/crypto/index.mjs +37 -37
- package/dist/crypto/index.mjs.map +1 -1
- package/dist/date/index.mjs +3 -3
- package/dist/date/index.mjs.map +1 -1
- package/dist/dom/style.mjs +3 -3
- package/dist/dom/style.mjs.map +1 -1
- package/dist/identity/index.mjs +4 -4
- package/dist/identity/index.mjs.map +1 -1
- package/dist/index.d.mts +4 -3
- package/dist/index.global.min.js +2 -2
- package/dist/index.global.min.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/internal/text.d.mts +17 -0
- package/dist/internal/text.mjs +41 -3
- package/dist/internal/text.mjs.map +1 -1
- package/dist/logger/index.d.mts +26 -22
- package/dist/logger/index.mjs +54 -25
- package/dist/logger/index.mjs.map +1 -1
- package/dist/number/index.mjs +12 -12
- package/dist/number/index.mjs.map +1 -1
- package/dist/object/index.mjs +1 -1
- package/dist/object/index.mjs.map +1 -1
- package/dist/storage/index.d.mts +14 -5
- package/dist/storage/index.mjs +39 -27
- package/dist/storage/index.mjs.map +1 -1
- package/dist/string/index.mjs +13 -13
- package/dist/string/index.mjs.map +1 -1
- package/dist/vue/emits.mjs +2 -2
- package/dist/vue/emits.mjs.map +1 -1
- package/dist/vue/func.mjs +1 -1
- package/dist/vue/func.mjs.map +1 -1
- package/dist/vue/install.mjs +12 -12
- package/dist/vue/install.mjs.map +1 -1
- package/dist/vue/props.d.mts +1 -1
- package/dist/vue/props.mjs.map +1 -1
- package/dist/vue/render.mjs +1 -1
- package/dist/vue/render.mjs.map +1 -1
- package/docs/API.md +34 -6
- package/docs/API.zh-CN.md +33 -6
- package/docs/RUNTIME_CONTRACT.md +9 -4
- package/package.json +9 -9
package/dist/base64/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { encodeUtf8, getTextDecoder } from "../internal/text.mjs";
|
|
1
|
+
import { createDecodedText, encodeUtf8, getTextDecoder } from "../internal/text.mjs";
|
|
2
2
|
import { randomInt } from "../number/index.mjs";
|
|
3
3
|
//#region src/base64/index.ts
|
|
4
4
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -9,7 +9,7 @@ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
|
|
9
9
|
* @returns 带有具体变体名称的 `TypeError`。
|
|
10
10
|
*/
|
|
11
11
|
const invalidBase64 = (variant) => {
|
|
12
|
-
return /* @__PURE__ */ new TypeError(
|
|
12
|
+
return /* @__PURE__ */ new TypeError(`该值不是有效的 ${variant === "url" ? "Base64URL" : "Base64"}。`);
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
15
|
* 校验并规范化 Base64 文本。
|
|
@@ -90,7 +90,7 @@ const decodeUtf8 = (bytes) => {
|
|
|
90
90
|
try {
|
|
91
91
|
return textDecoder.decode(bytes);
|
|
92
92
|
} catch (cause) {
|
|
93
|
-
throw new TypeError("
|
|
93
|
+
throw new TypeError("解码后的字节不是有效的 UTF-8。", { cause });
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
/**
|
|
@@ -127,11 +127,11 @@ function encodeBase64(value) {
|
|
|
127
127
|
* 将标准 Base64 解码为 UTF-8 文本。
|
|
128
128
|
*
|
|
129
129
|
* @param value - Base64 文本。
|
|
130
|
-
* @returns
|
|
130
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 Unicode 字符串。
|
|
131
131
|
* @throws Base64 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。
|
|
132
132
|
*/
|
|
133
133
|
function decodeBase64(value) {
|
|
134
|
-
return decodeUtf8(decodeBase64Bytes(value));
|
|
134
|
+
return createDecodedText(decodeUtf8(decodeBase64Bytes(value)));
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
137
|
* 将任意字节编码为无填充 Base64URL。
|
|
@@ -167,11 +167,11 @@ function encodeBase64Url(value) {
|
|
|
167
167
|
* 将 Base64URL 解码为 UTF-8 文本。
|
|
168
168
|
*
|
|
169
169
|
* @param value - 带填充或无填充的 Base64URL 文本。
|
|
170
|
-
* @returns
|
|
170
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 Unicode 字符串。
|
|
171
171
|
* @throws Base64URL 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。
|
|
172
172
|
*/
|
|
173
173
|
function decodeBase64Url(value) {
|
|
174
|
-
return decodeUtf8(decodeBase64UrlBytes(value));
|
|
174
|
+
return createDecodedText(decodeUtf8(decodeBase64UrlBytes(value)));
|
|
175
175
|
}
|
|
176
176
|
/**
|
|
177
177
|
* 把 Latin-1 文本编码为标准 Base64。
|
|
@@ -184,7 +184,7 @@ function encodeLatin1Base64(value) {
|
|
|
184
184
|
const bytes = new Uint8Array(value.length);
|
|
185
185
|
for (let index = 0; index < value.length; index += 1) {
|
|
186
186
|
const code = value.charCodeAt(index);
|
|
187
|
-
if (code > 255) throw new TypeError("
|
|
187
|
+
if (code > 255) throw new TypeError("待编码字符串包含超出 Latin-1 范围的字符。");
|
|
188
188
|
bytes[index] = code;
|
|
189
189
|
}
|
|
190
190
|
return encodeBase64Bytes(bytes);
|
|
@@ -193,14 +193,14 @@ function encodeLatin1Base64(value) {
|
|
|
193
193
|
* 把标准 Base64 解码为 Latin-1 文本。
|
|
194
194
|
*
|
|
195
195
|
* @param value - 标准 Base64 文本;允许 ASCII 空白和省略尾部填充。
|
|
196
|
-
* @returns
|
|
196
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的 Latin-1 原始字符串。
|
|
197
197
|
* @throws `TypeError` 当 Base64 格式或尾部位非法。
|
|
198
198
|
*/
|
|
199
199
|
function decodeLatin1Base64(value) {
|
|
200
200
|
const bytes = decodeBase64Bytes(value);
|
|
201
201
|
let result = "";
|
|
202
202
|
for (const byte of bytes) result += String.fromCharCode(byte);
|
|
203
|
-
return result;
|
|
203
|
+
return createDecodedText(result);
|
|
204
204
|
}
|
|
205
205
|
const randomPrefixAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
206
206
|
const defaultRandomPrefixLength = 6;
|
|
@@ -342,7 +342,7 @@ const base64PasswordDictionary = Object.freeze([
|
|
|
342
342
|
* @throws `RangeError` 当长度不是非负安全整数。
|
|
343
343
|
*/
|
|
344
344
|
const assertPrefixLength = (length) => {
|
|
345
|
-
if (!Number.isSafeInteger(length) || length < 0) throw new RangeError("prefixStrLength
|
|
345
|
+
if (!Number.isSafeInteger(length) || length < 0) throw new RangeError("`prefixStrLength` 必须是非负安全整数。");
|
|
346
346
|
};
|
|
347
347
|
/**
|
|
348
348
|
* 生成 SecureBase64 兼容格式使用的随机前缀。
|
|
@@ -411,16 +411,16 @@ function encodeSecureBase64(value, prefixLength = defaultRandomPrefixLength) {
|
|
|
411
411
|
*
|
|
412
412
|
* @param value - SecureBase64 文本;必须使用与编码时相同的前缀长度。
|
|
413
413
|
* @param prefixLength - 需要移除的前缀长度;默认 `6`,传入 `0` 时不移除字典字符。
|
|
414
|
-
* @returns
|
|
414
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 Unicode 字符串;空输入返回空字符串。
|
|
415
415
|
* @throws `RangeError` 当前缀长度不是非负安全整数;载荷、Base64 或 URI 编码非法时抛出 `TypeError` 或 `URIError`。
|
|
416
416
|
*/
|
|
417
417
|
function decodeSecureBase64(value, prefixLength = defaultRandomPrefixLength) {
|
|
418
|
-
if (value.length === 0) return "";
|
|
418
|
+
if (value.length === 0) return createDecodedText("");
|
|
419
419
|
assertPrefixLength(prefixLength);
|
|
420
|
-
if (prefixLength > value.length) throw new TypeError("
|
|
420
|
+
if (prefixLength > value.length) throw new TypeError("Base64 值的长度小于配置的前缀长度。");
|
|
421
421
|
let encoded = value.slice(prefixLength);
|
|
422
422
|
if (prefixLength !== 0) encoded = removeDictionaryCharacters(encoded);
|
|
423
|
-
return decodeURIComponent(decodeLatin1Base64(encoded));
|
|
423
|
+
return createDecodedText(decodeURIComponent(decodeLatin1Base64(encoded)));
|
|
424
424
|
}
|
|
425
425
|
//#endregion
|
|
426
426
|
export { decodeBase64, decodeBase64Bytes, decodeBase64Url, decodeBase64UrlBytes, decodeLatin1Base64, decodeSecureBase64, encodeBase64, encodeBase64Bytes, encodeBase64Url, encodeBase64UrlBytes, encodeLatin1Base64, encodeSecureBase64 };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/base64/index.ts"],"sourcesContent":["import { encodeUtf8, getTextDecoder } from \"../internal/text\";\nimport { randomInt } from \"../number/index\";\n\nconst alphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/** 内部共享解码器支持的两种字符表规则。 */\ntype Base64Variant = \"standard\" | \"url\";\n\n/**\n * 创建统一的 Base64 输入错误。\n *\n * @param variant - 当前解析的标准 Base64 或 Base64URL 变体。\n * @returns 带有具体变体名称的 `TypeError`。\n */\nconst invalidBase64 = (variant: Base64Variant): TypeError => {\n\treturn new TypeError(`The value is not valid ${variant === \"url\" ? \"Base64URL\" : \"Base64\"}.`);\n};\n\n/**\n * 校验并规范化 Base64 文本。\n *\n * @remarks 标准 Base64 允许 ASCII 空白,Base64URL 不允许;两者最终都会转换为标准字符表和完整填充。\n * @param value - 原始编码文本。\n * @param variant - 需要应用的字符表与空白规则。\n * @returns 使用标准字符表且长度为 4 倍数的文本。\n * @throws `TypeError` 当字符、填充位置或编码长度非法。\n */\nconst normalizeBase64 = (value: string, variant: Base64Variant): string => {\n\tconst withoutWhitespace = variant === \"standard\" ? value.replace(/[\\t\\n\\f\\r ]+/gu, \"\") : value;\n\tconst pattern = variant === \"url\" ? /^[\\w-]*={0,2}$/u : /^[A-Za-z\\d+/]*={0,2}$/u;\n\tif (!pattern.test(withoutWhitespace) || withoutWhitespace.length % 4 === 1) throw invalidBase64(variant);\n\tif (withoutWhitespace.includes(\"=\") && withoutWhitespace.length % 4 !== 0) throw invalidBase64(variant);\n\n\tconst standard = (variant === \"url\" ? withoutWhitespace.replace(/-/gu, \"+\").replace(/_/gu, \"/\") : withoutWhitespace).replace(/=+$/u, \"\");\n\treturn standard.padEnd(Math.ceil(standard.length / 4) * 4, \"=\");\n};\n\n/**\n * 使用固定字符表编码任意字节。\n *\n * @param bytes - 不会被修改的源字节。\n * @returns 带标准 `=` 填充的 Base64 文本。\n */\nconst encodeBytes = (bytes: Uint8Array): string => {\n\tlet result = \"\";\n\tfor (let index = 0; index < bytes.length; index += 3) {\n\t\tconst first = bytes[index] ?? 0;\n\t\tconst second = bytes[index + 1];\n\t\tconst third = bytes[index + 2];\n\t\tconst bitmap = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);\n\t\tresult += alphabet[(bitmap >> 18) & 63] ?? \"\";\n\t\tresult += alphabet[(bitmap >> 12) & 63] ?? \"\";\n\t\tresult += second === undefined ? \"=\" : (alphabet[(bitmap >> 6) & 63] ?? \"\");\n\t\tresult += third === undefined ? \"=\" : (alphabet[bitmap & 63] ?? \"\");\n\t}\n\treturn result;\n};\n\n/**\n * 把 Base64 或 Base64URL 文本解码为字节。\n *\n * @remarks 最后一组未使用位必须为零,以拒绝同一字节序列的非规范编码。\n * @param value - 待解码文本。\n * @param variant - 输入使用的编码变体。\n * @returns 新建的字节数组。\n * @throws `TypeError` 当输入格式或尾部位非法。\n */\nconst decodeBytes = (value: string, variant: Base64Variant): Uint8Array => {\n\tconst normalized = normalizeBase64(value, variant);\n\tif (normalized.length === 0) return new Uint8Array();\n\n\tconst padding = normalized.endsWith(\"==\") ? 2 : normalized.endsWith(\"=\") ? 1 : 0;\n\tconst result = new Uint8Array((normalized.length / 4) * 3 - padding);\n\tlet outputIndex = 0;\n\n\tfor (let index = 0; index < normalized.length; index += 4) {\n\t\tconst first = alphabet.indexOf(normalized[index] ?? \"\");\n\t\tconst second = alphabet.indexOf(normalized[index + 1] ?? \"\");\n\t\tconst thirdCharacter = normalized[index + 2] ?? \"=\";\n\t\tconst fourthCharacter = normalized[index + 3] ?? \"=\";\n\t\tconst third = thirdCharacter === \"=\" ? 0 : alphabet.indexOf(thirdCharacter);\n\t\tconst fourth = fourthCharacter === \"=\" ? 0 : alphabet.indexOf(fourthCharacter);\n\t\tif (first < 0 || second < 0 || third < 0 || fourth < 0) throw invalidBase64(variant);\n\n\t\tconst isLast = index + 4 === normalized.length;\n\t\t// 填充字符对应的未使用低位必须为零,否则不同文本可以解码为同一字节序列。\n\t\tif (isLast && ((padding === 2 && (second & 15) !== 0) || (padding === 1 && (third & 3) !== 0))) {\n\t\t\tthrow invalidBase64(variant);\n\t\t}\n\n\t\tconst bitmap = (first << 18) | (second << 12) | (third << 6) | fourth;\n\t\tif (outputIndex < result.length) result[outputIndex++] = (bitmap >> 16) & 255;\n\t\tif (outputIndex < result.length) result[outputIndex++] = (bitmap >> 8) & 255;\n\t\tif (outputIndex < result.length) result[outputIndex++] = bitmap & 255;\n\t}\n\treturn result;\n};\n\n/**\n * 以 Fatal 模式解码 UTF-8。\n *\n * @param bytes - 待解码字节。\n * @returns 解码后的 JavaScript 字符串。\n * @throws `TypeError` 当字节不是有效 UTF-8;原始平台异常保存在 `cause` 中。\n */\nconst decodeUtf8 = (bytes: Uint8Array): string => {\n\tconst textDecoder = getTextDecoder();\n\ttry {\n\t\treturn textDecoder.decode(bytes);\n\t} catch (cause) {\n\t\tthrow new TypeError(\"The decoded bytes are not valid UTF-8.\", { cause });\n\t}\n};\n\n/**\n * 将任意字节编码为标准 Base64。\n *\n * @param bytes - 不会被修改的字节序列。\n * @returns 带标准 `=` 填充的 Base64 文本。\n */\nexport function encodeBase64Bytes(bytes: Uint8Array): string {\n\treturn encodeBytes(bytes);\n}\n\n/**\n * 解码标准 Base64。\n *\n * @remarks 允许省略填充和包含 ASCII 空白,但拒绝非规范尾部位。\n * @param value - Base64 文本。\n * @returns 新建的字节数组。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function decodeBase64Bytes(value: string): Uint8Array {\n\treturn decodeBytes(value, \"standard\");\n}\n\n/**\n * 将 UTF-8 文本编码为标准 Base64。\n *\n * @param value - 任意 Unicode 字符串。\n * @returns 带标准填充的 Base64 文本。\n * @throws 缺少 Encoding API 时抛出 `Error`。\n */\nexport function encodeBase64(value: string): string {\n\treturn encodeBytes(encodeUtf8(value));\n}\n\n/**\n * 将标准 Base64 解码为 UTF-8 文本。\n *\n * @param value - Base64 文本。\n * @returns 解码后的 Unicode 字符串。\n * @throws Base64 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。\n */\nexport function decodeBase64(value: string): string {\n\treturn decodeUtf8(decodeBase64Bytes(value));\n}\n\n/**\n * 将任意字节编码为无填充 Base64URL。\n *\n * @param bytes - 不会被修改的字节序列。\n * @returns 仅使用 URL 安全字母表的文本。\n */\nexport function encodeBase64UrlBytes(bytes: Uint8Array): string {\n\treturn encodeBytes(bytes).replace(/\\+/gu, \"-\").replace(/\\//gu, \"_\").replace(/=+$/u, \"\");\n}\n\n/**\n * 解码 Base64URL 字节。\n *\n * @remarks 接受带填充和无填充形式,不接受空白或标准 Base64 的 `+`、`/`。\n * @param value - Base64URL 文本。\n * @returns 新建的字节数组。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function decodeBase64UrlBytes(value: string): Uint8Array {\n\treturn decodeBytes(value, \"url\");\n}\n\n/**\n * 将 UTF-8 文本编码为无填充 Base64URL。\n *\n * @param value - 任意 Unicode 字符串。\n * @returns 仅使用 URL 安全字母表的文本。\n * @throws 缺少 Encoding API 时抛出 `Error`。\n */\nexport function encodeBase64Url(value: string): string {\n\treturn encodeBase64UrlBytes(encodeUtf8(value));\n}\n\n/**\n * 将 Base64URL 解码为 UTF-8 文本。\n *\n * @param value - 带填充或无填充的 Base64URL 文本。\n * @returns 解码后的 Unicode 字符串。\n * @throws Base64URL 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。\n */\nexport function decodeBase64Url(value: string): string {\n\treturn decodeUtf8(decodeBase64UrlBytes(value));\n}\n\n/**\n * 把 Latin-1 文本编码为标准 Base64。\n *\n * @param value - 每个 UTF-16 码元都必须位于 0–255 的文本。\n * @returns 带标准填充的 Base64 文本。\n * @throws `TypeError` 当文本包含 Latin-1 范围外的码元。\n */\nexport function encodeLatin1Base64(value: string): string {\n\tconst bytes = new Uint8Array(value.length);\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tconst code = value.charCodeAt(index);\n\t\tif (code > 255) throw new TypeError(\"The string to be encoded contains characters outside of the Latin-1 range.\");\n\t\tbytes[index] = code;\n\t}\n\treturn encodeBase64Bytes(bytes);\n}\n\n/**\n * 把标准 Base64 解码为 Latin-1 文本。\n *\n * @param value - 标准 Base64 文本;允许 ASCII 空白和省略尾部填充。\n * @returns 每个字节直接映射为同值 UTF-16 码元的文本。\n * @throws `TypeError` 当 Base64 格式或尾部位非法。\n */\nexport function decodeLatin1Base64(value: string): string {\n\tconst bytes = decodeBase64Bytes(value);\n\tlet result = \"\";\n\tfor (const byte of bytes) result += String.fromCharCode(byte);\n\treturn result;\n}\n\nconst randomPrefixAlphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\nconst defaultRandomPrefixLength = 6;\n\n/** SecureBase64 兼容格式中一组不可变的字符插入位置。 */\ninterface Base64PasswordDictionaryEntry {\n\t/** 原始载荷中的固定字符索引;超出短载荷长度时忽略当前条目。 */\n\tindex: number;\n\t/** 从原始载荷复制字符的固定索引;数值属于持久化协议,不能重新计算。 */\n\trandomIndex: number;\n}\n\n/** 固定 SecureBase64 字典;索引、顺序与映射属于持久化格式,禁止修改。 */\nconst base64PasswordDictionary: readonly Readonly<Base64PasswordDictionaryEntry>[] = Object.freeze([\n\t{ index: 977, randomIndex: 188 },\n\t{ index: 926, randomIndex: 201 },\n\t{ index: 851, randomIndex: 225 },\n\t{ index: 700, randomIndex: 255 },\n\t{ index: 600, randomIndex: 268 },\n\t{ index: 500, randomIndex: 277 },\n\t{ index: 400, randomIndex: 288 },\n\t{ index: 330, randomIndex: 327 },\n\t{ index: 300, randomIndex: 180 },\n\t{ index: 200, randomIndex: 178 },\n\t{ index: 100, randomIndex: 124 },\n\t{ index: 98, randomIndex: 95 },\n\t{ index: 92, randomIndex: 90 },\n\t{ index: 91, randomIndex: 87 },\n\t{ index: 88, randomIndex: 84 },\n\t{ index: 82, randomIndex: 79 },\n\t{ index: 78, randomIndex: 71 },\n\t{ index: 72, randomIndex: 69 },\n\t{ index: 68, randomIndex: 66 },\n\t{ index: 59, randomIndex: 55 },\n\t{ index: 48, randomIndex: 43 },\n\t{ index: 42, randomIndex: 37 },\n\t{ index: 36, randomIndex: 30 },\n\t{ index: 33, randomIndex: 27 },\n\t{ index: 24, randomIndex: 20 },\n\t{ index: 23, randomIndex: 18 },\n\t{ index: 21, randomIndex: 16 },\n\t{ index: 17, randomIndex: 14 },\n\t{ index: 13, randomIndex: 9 },\n\t{ index: 7, randomIndex: 4 },\n\t{ index: 5, randomIndex: 3 },\n\t{ index: 2, randomIndex: 1 },\n]);\n\n/**\n * 校验 SecureBase64 兼容格式的前缀长度。\n *\n * @param length - 待校验的前缀字符数。\n * @throws `RangeError` 当长度不是非负安全整数。\n */\nconst assertPrefixLength = (length: number): void => {\n\tif (!Number.isSafeInteger(length) || length < 0) throw new RangeError(\"prefixStrLength must be a non-negative safe integer.\");\n};\n\n/**\n * 生成 SecureBase64 兼容格式使用的随机前缀。\n *\n * @remarks 优先使用 Web Crypto,能力缺失时回退到 `Math.random()`。前缀只用于降低相同明文\n * 产生相同载荷的概率,本身不构成加密,也不得承担安全用途。\n * @param length - 需要生成的字符数,调用前必须完成校验。\n * @returns 由历史字母表组成的文本。\n */\nconst createRandomPrefix = (length: number): string => {\n\tlet result = \"\";\n\tfor (let index = 0; index < length; index += 1) {\n\t\tresult += randomPrefixAlphabet[randomInt(0, randomPrefixAlphabet.length)] ?? \"\";\n\t}\n\treturn result;\n};\n\n/**\n * 按固定字典向 Base64 文本插入冗余字符。\n *\n * @remarks 字典按高索引到低索引排列,确保插入不会改变尚未处理的位置。\n * @param base64Value - 尚未插入兼容字典字符的 Base64 文本。\n * @returns 与旧持久化格式兼容的 SecureBase64 载荷。\n */\nconst insertDictionaryCharacters = (base64Value: string): string => {\n\tlet result = base64Value;\n\tfor (const item of base64PasswordDictionary) {\n\t\tif (item.index >= base64Value.length) continue;\n\t\t// 旧字典的 index=100 项会在 101–124 字符载荷中越界;退回末字符可保持单字符插入协议,旧解码器仍能移除。\n\t\tconst character = base64Value[item.randomIndex] ?? base64Value.at(-1) ?? \"\";\n\t\tresult = result.slice(0, item.index) + character + result.slice(item.index);\n\t}\n\treturn result;\n};\n\n/**\n * 移除历史字典插入的冗余字符。\n *\n * @param base64Value - 已移除随机前缀的 SecureBase64 载荷。\n * @returns 可交给标准 Base64 解码器的文本。\n */\nconst removeDictionaryCharacters = (base64Value: string): string => {\n\tlet result = base64Value;\n\tfor (let index = base64PasswordDictionary.length - 1; index >= 0; index -= 1) {\n\t\tconst item = base64PasswordDictionary[index];\n\t\tif (item !== undefined && item.index < base64Value.length) result = result.slice(0, item.index) + result.slice(item.index + 1);\n\t}\n\treturn result;\n};\n\n/**\n * 使用固定字典和随机前缀编码文本。\n *\n * @remarks 给定相同的 6 字符前缀时,默认输出与旧有效载荷逐字符兼容。旧字典在 101–124 字符载荷中引用越界;这里复制末字符作为单字符回退,\n * 使旧删除字典流程仍能解码。传入 `0` 会同时关闭随机前缀与字典插入。\n * 该格式只是可逆编码,不提供加密、完整性或身份认证。\n * @param value - 任意可由 `encodeURIComponent` 处理的 Unicode 文本。\n * @param prefixLength - 随机字母前缀长度;默认 `6`。\n * @returns 带随机前缀和兼容字典字符的 Base64 文本;空输入返回空字符串。\n * @throws `RangeError` 当前缀长度不是非负安全整数;输入包含孤立代理项时保留平台错误。\n */\nexport function encodeSecureBase64(value: string, prefixLength: number = defaultRandomPrefixLength): string {\n\tif (value.length === 0) return \"\";\n\tassertPrefixLength(prefixLength);\n\tconst prefix = createRandomPrefix(prefixLength);\n\tlet encoded = encodeLatin1Base64(encodeURIComponent(value));\n\tif (prefixLength !== 0) encoded = insertDictionaryCharacters(encoded);\n\treturn prefix + encoded;\n}\n\n/**\n * 解码 {@link encodeSecureBase64} 生成的 SecureBase64 兼容格式。\n *\n * @param value - SecureBase64 文本;必须使用与编码时相同的前缀长度。\n * @param prefixLength - 需要移除的前缀长度;默认 `6`,传入 `0` 时不移除字典字符。\n * @returns 解码后的 Unicode 文本;空输入返回空字符串。\n * @throws `RangeError` 当前缀长度不是非负安全整数;载荷、Base64 或 URI 编码非法时抛出 `TypeError` 或 `URIError`。\n */\nexport function decodeSecureBase64(value: string, prefixLength: number = defaultRandomPrefixLength): string {\n\tif (value.length === 0) return \"\";\n\tassertPrefixLength(prefixLength);\n\tif (prefixLength > value.length) throw new TypeError(\"The Base64 value is shorter than its configured prefix.\");\n\tlet encoded = value.slice(prefixLength);\n\tif (prefixLength !== 0) encoded = removeDictionaryCharacters(encoded);\n\treturn decodeURIComponent(decodeLatin1Base64(encoded));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/base64/index.ts"],"sourcesContent":["import { type DecodedText, createDecodedText, encodeUtf8, getTextDecoder } from \"../internal/text\";\nimport { randomInt } from \"../number/index\";\n\nexport type { DecodedText } from \"../internal/text\";\n\nconst alphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/** 内部共享解码器支持的两种字符表规则。 */\ntype Base64Variant = \"standard\" | \"url\";\n\n/**\n * 创建统一的 Base64 输入错误。\n *\n * @param variant - 当前解析的标准 Base64 或 Base64URL 变体。\n * @returns 带有具体变体名称的 `TypeError`。\n */\nconst invalidBase64 = (variant: Base64Variant): TypeError => {\n\treturn new TypeError(`该值不是有效的 ${variant === \"url\" ? \"Base64URL\" : \"Base64\"}。`);\n};\n\n/**\n * 校验并规范化 Base64 文本。\n *\n * @remarks 标准 Base64 允许 ASCII 空白,Base64URL 不允许;两者最终都会转换为标准字符表和完整填充。\n * @param value - 原始编码文本。\n * @param variant - 需要应用的字符表与空白规则。\n * @returns 使用标准字符表且长度为 4 倍数的文本。\n * @throws `TypeError` 当字符、填充位置或编码长度非法。\n */\nconst normalizeBase64 = (value: string, variant: Base64Variant): string => {\n\tconst withoutWhitespace = variant === \"standard\" ? value.replace(/[\\t\\n\\f\\r ]+/gu, \"\") : value;\n\tconst pattern = variant === \"url\" ? /^[\\w-]*={0,2}$/u : /^[A-Za-z\\d+/]*={0,2}$/u;\n\tif (!pattern.test(withoutWhitespace) || withoutWhitespace.length % 4 === 1) throw invalidBase64(variant);\n\tif (withoutWhitespace.includes(\"=\") && withoutWhitespace.length % 4 !== 0) throw invalidBase64(variant);\n\n\tconst standard = (variant === \"url\" ? withoutWhitespace.replace(/-/gu, \"+\").replace(/_/gu, \"/\") : withoutWhitespace).replace(/=+$/u, \"\");\n\treturn standard.padEnd(Math.ceil(standard.length / 4) * 4, \"=\");\n};\n\n/**\n * 使用固定字符表编码任意字节。\n *\n * @param bytes - 不会被修改的源字节。\n * @returns 带标准 `=` 填充的 Base64 文本。\n */\nconst encodeBytes = (bytes: Uint8Array): string => {\n\tlet result = \"\";\n\tfor (let index = 0; index < bytes.length; index += 3) {\n\t\tconst first = bytes[index] ?? 0;\n\t\tconst second = bytes[index + 1];\n\t\tconst third = bytes[index + 2];\n\t\tconst bitmap = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);\n\t\tresult += alphabet[(bitmap >> 18) & 63] ?? \"\";\n\t\tresult += alphabet[(bitmap >> 12) & 63] ?? \"\";\n\t\tresult += second === undefined ? \"=\" : (alphabet[(bitmap >> 6) & 63] ?? \"\");\n\t\tresult += third === undefined ? \"=\" : (alphabet[bitmap & 63] ?? \"\");\n\t}\n\treturn result;\n};\n\n/**\n * 把 Base64 或 Base64URL 文本解码为字节。\n *\n * @remarks 最后一组未使用位必须为零,以拒绝同一字节序列的非规范编码。\n * @param value - 待解码文本。\n * @param variant - 输入使用的编码变体。\n * @returns 新建的字节数组。\n * @throws `TypeError` 当输入格式或尾部位非法。\n */\nconst decodeBytes = (value: string, variant: Base64Variant): Uint8Array => {\n\tconst normalized = normalizeBase64(value, variant);\n\tif (normalized.length === 0) return new Uint8Array();\n\n\tconst padding = normalized.endsWith(\"==\") ? 2 : normalized.endsWith(\"=\") ? 1 : 0;\n\tconst result = new Uint8Array((normalized.length / 4) * 3 - padding);\n\tlet outputIndex = 0;\n\n\tfor (let index = 0; index < normalized.length; index += 4) {\n\t\tconst first = alphabet.indexOf(normalized[index] ?? \"\");\n\t\tconst second = alphabet.indexOf(normalized[index + 1] ?? \"\");\n\t\tconst thirdCharacter = normalized[index + 2] ?? \"=\";\n\t\tconst fourthCharacter = normalized[index + 3] ?? \"=\";\n\t\tconst third = thirdCharacter === \"=\" ? 0 : alphabet.indexOf(thirdCharacter);\n\t\tconst fourth = fourthCharacter === \"=\" ? 0 : alphabet.indexOf(fourthCharacter);\n\t\tif (first < 0 || second < 0 || third < 0 || fourth < 0) throw invalidBase64(variant);\n\n\t\tconst isLast = index + 4 === normalized.length;\n\t\t// 填充字符对应的未使用低位必须为零,否则不同文本可以解码为同一字节序列。\n\t\tif (isLast && ((padding === 2 && (second & 15) !== 0) || (padding === 1 && (third & 3) !== 0))) {\n\t\t\tthrow invalidBase64(variant);\n\t\t}\n\n\t\tconst bitmap = (first << 18) | (second << 12) | (third << 6) | fourth;\n\t\tif (outputIndex < result.length) result[outputIndex++] = (bitmap >> 16) & 255;\n\t\tif (outputIndex < result.length) result[outputIndex++] = (bitmap >> 8) & 255;\n\t\tif (outputIndex < result.length) result[outputIndex++] = bitmap & 255;\n\t}\n\treturn result;\n};\n\n/**\n * 以 Fatal 模式解码 UTF-8。\n *\n * @param bytes - 待解码字节。\n * @returns 解码后的 JavaScript 字符串。\n * @throws `TypeError` 当字节不是有效 UTF-8;原始平台异常保存在 `cause` 中。\n */\nconst decodeUtf8 = (bytes: Uint8Array): string => {\n\tconst textDecoder = getTextDecoder();\n\ttry {\n\t\treturn textDecoder.decode(bytes);\n\t} catch (cause) {\n\t\tthrow new TypeError(\"解码后的字节不是有效的 UTF-8。\", { cause });\n\t}\n};\n\n/**\n * 将任意字节编码为标准 Base64。\n *\n * @param bytes - 不会被修改的字节序列。\n * @returns 带标准 `=` 填充的 Base64 文本。\n */\nexport function encodeBase64Bytes(bytes: Uint8Array): string {\n\treturn encodeBytes(bytes);\n}\n\n/**\n * 解码标准 Base64。\n *\n * @remarks 允许省略填充和包含 ASCII 空白,但拒绝非规范尾部位。\n * @param value - Base64 文本。\n * @returns 新建的字节数组。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function decodeBase64Bytes(value: string): Uint8Array {\n\treturn decodeBytes(value, \"standard\");\n}\n\n/**\n * 将 UTF-8 文本编码为标准 Base64。\n *\n * @param value - 任意 Unicode 字符串。\n * @returns 带标准填充的 Base64 文本。\n * @throws 缺少 Encoding API 时抛出 `Error`。\n */\nexport function encodeBase64(value: string): string {\n\treturn encodeBytes(encodeUtf8(value));\n}\n\n/**\n * 将标准 Base64 解码为 UTF-8 文本。\n *\n * @param value - Base64 文本。\n * @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 Unicode 字符串。\n * @throws Base64 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。\n */\nexport function decodeBase64(value: string): DecodedText {\n\treturn createDecodedText(decodeUtf8(decodeBase64Bytes(value)));\n}\n\n/**\n * 将任意字节编码为无填充 Base64URL。\n *\n * @param bytes - 不会被修改的字节序列。\n * @returns 仅使用 URL 安全字母表的文本。\n */\nexport function encodeBase64UrlBytes(bytes: Uint8Array): string {\n\treturn encodeBytes(bytes).replace(/\\+/gu, \"-\").replace(/\\//gu, \"_\").replace(/=+$/u, \"\");\n}\n\n/**\n * 解码 Base64URL 字节。\n *\n * @remarks 接受带填充和无填充形式,不接受空白或标准 Base64 的 `+`、`/`。\n * @param value - Base64URL 文本。\n * @returns 新建的字节数组。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function decodeBase64UrlBytes(value: string): Uint8Array {\n\treturn decodeBytes(value, \"url\");\n}\n\n/**\n * 将 UTF-8 文本编码为无填充 Base64URL。\n *\n * @param value - 任意 Unicode 字符串。\n * @returns 仅使用 URL 安全字母表的文本。\n * @throws 缺少 Encoding API 时抛出 `Error`。\n */\nexport function encodeBase64Url(value: string): string {\n\treturn encodeBase64UrlBytes(encodeUtf8(value));\n}\n\n/**\n * 将 Base64URL 解码为 UTF-8 文本。\n *\n * @param value - 带填充或无填充的 Base64URL 文本。\n * @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 Unicode 字符串。\n * @throws Base64URL 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。\n */\nexport function decodeBase64Url(value: string): DecodedText {\n\treturn createDecodedText(decodeUtf8(decodeBase64UrlBytes(value)));\n}\n\n/**\n * 把 Latin-1 文本编码为标准 Base64。\n *\n * @param value - 每个 UTF-16 码元都必须位于 0–255 的文本。\n * @returns 带标准填充的 Base64 文本。\n * @throws `TypeError` 当文本包含 Latin-1 范围外的码元。\n */\nexport function encodeLatin1Base64(value: string): string {\n\tconst bytes = new Uint8Array(value.length);\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tconst code = value.charCodeAt(index);\n\t\tif (code > 255) throw new TypeError(\"待编码字符串包含超出 Latin-1 范围的字符。\");\n\t\tbytes[index] = code;\n\t}\n\treturn encodeBase64Bytes(bytes);\n}\n\n/**\n * 把标准 Base64 解码为 Latin-1 文本。\n *\n * @param value - 标准 Base64 文本;允许 ASCII 空白和省略尾部填充。\n * @returns 可直接使用或显式调用 `.parseJson<Value>()` 的 Latin-1 原始字符串。\n * @throws `TypeError` 当 Base64 格式或尾部位非法。\n */\nexport function decodeLatin1Base64(value: string): DecodedText {\n\tconst bytes = decodeBase64Bytes(value);\n\tlet result = \"\";\n\tfor (const byte of bytes) result += String.fromCharCode(byte);\n\treturn createDecodedText(result);\n}\n\nconst randomPrefixAlphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\nconst defaultRandomPrefixLength = 6;\n\n/** SecureBase64 兼容格式中一组不可变的字符插入位置。 */\ninterface Base64PasswordDictionaryEntry {\n\t/** 原始载荷中的固定字符索引;超出短载荷长度时忽略当前条目。 */\n\tindex: number;\n\t/** 从原始载荷复制字符的固定索引;数值属于持久化协议,不能重新计算。 */\n\trandomIndex: number;\n}\n\n/** 固定 SecureBase64 字典;索引、顺序与映射属于持久化格式,禁止修改。 */\nconst base64PasswordDictionary: readonly Readonly<Base64PasswordDictionaryEntry>[] = Object.freeze([\n\t{ index: 977, randomIndex: 188 },\n\t{ index: 926, randomIndex: 201 },\n\t{ index: 851, randomIndex: 225 },\n\t{ index: 700, randomIndex: 255 },\n\t{ index: 600, randomIndex: 268 },\n\t{ index: 500, randomIndex: 277 },\n\t{ index: 400, randomIndex: 288 },\n\t{ index: 330, randomIndex: 327 },\n\t{ index: 300, randomIndex: 180 },\n\t{ index: 200, randomIndex: 178 },\n\t{ index: 100, randomIndex: 124 },\n\t{ index: 98, randomIndex: 95 },\n\t{ index: 92, randomIndex: 90 },\n\t{ index: 91, randomIndex: 87 },\n\t{ index: 88, randomIndex: 84 },\n\t{ index: 82, randomIndex: 79 },\n\t{ index: 78, randomIndex: 71 },\n\t{ index: 72, randomIndex: 69 },\n\t{ index: 68, randomIndex: 66 },\n\t{ index: 59, randomIndex: 55 },\n\t{ index: 48, randomIndex: 43 },\n\t{ index: 42, randomIndex: 37 },\n\t{ index: 36, randomIndex: 30 },\n\t{ index: 33, randomIndex: 27 },\n\t{ index: 24, randomIndex: 20 },\n\t{ index: 23, randomIndex: 18 },\n\t{ index: 21, randomIndex: 16 },\n\t{ index: 17, randomIndex: 14 },\n\t{ index: 13, randomIndex: 9 },\n\t{ index: 7, randomIndex: 4 },\n\t{ index: 5, randomIndex: 3 },\n\t{ index: 2, randomIndex: 1 },\n]);\n\n/**\n * 校验 SecureBase64 兼容格式的前缀长度。\n *\n * @param length - 待校验的前缀字符数。\n * @throws `RangeError` 当长度不是非负安全整数。\n */\nconst assertPrefixLength = (length: number): void => {\n\tif (!Number.isSafeInteger(length) || length < 0) throw new RangeError(\"`prefixStrLength` 必须是非负安全整数。\");\n};\n\n/**\n * 生成 SecureBase64 兼容格式使用的随机前缀。\n *\n * @remarks 优先使用 Web Crypto,能力缺失时回退到 `Math.random()`。前缀只用于降低相同明文\n * 产生相同载荷的概率,本身不构成加密,也不得承担安全用途。\n * @param length - 需要生成的字符数,调用前必须完成校验。\n * @returns 由历史字母表组成的文本。\n */\nconst createRandomPrefix = (length: number): string => {\n\tlet result = \"\";\n\tfor (let index = 0; index < length; index += 1) {\n\t\tresult += randomPrefixAlphabet[randomInt(0, randomPrefixAlphabet.length)] ?? \"\";\n\t}\n\treturn result;\n};\n\n/**\n * 按固定字典向 Base64 文本插入冗余字符。\n *\n * @remarks 字典按高索引到低索引排列,确保插入不会改变尚未处理的位置。\n * @param base64Value - 尚未插入兼容字典字符的 Base64 文本。\n * @returns 与旧持久化格式兼容的 SecureBase64 载荷。\n */\nconst insertDictionaryCharacters = (base64Value: string): string => {\n\tlet result = base64Value;\n\tfor (const item of base64PasswordDictionary) {\n\t\tif (item.index >= base64Value.length) continue;\n\t\t// 旧字典的 index=100 项会在 101–124 字符载荷中越界;退回末字符可保持单字符插入协议,旧解码器仍能移除。\n\t\tconst character = base64Value[item.randomIndex] ?? base64Value.at(-1) ?? \"\";\n\t\tresult = result.slice(0, item.index) + character + result.slice(item.index);\n\t}\n\treturn result;\n};\n\n/**\n * 移除历史字典插入的冗余字符。\n *\n * @param base64Value - 已移除随机前缀的 SecureBase64 载荷。\n * @returns 可交给标准 Base64 解码器的文本。\n */\nconst removeDictionaryCharacters = (base64Value: string): string => {\n\tlet result = base64Value;\n\tfor (let index = base64PasswordDictionary.length - 1; index >= 0; index -= 1) {\n\t\tconst item = base64PasswordDictionary[index];\n\t\tif (item !== undefined && item.index < base64Value.length) result = result.slice(0, item.index) + result.slice(item.index + 1);\n\t}\n\treturn result;\n};\n\n/**\n * 使用固定字典和随机前缀编码文本。\n *\n * @remarks 给定相同的 6 字符前缀时,默认输出与旧有效载荷逐字符兼容。旧字典在 101–124 字符载荷中引用越界;这里复制末字符作为单字符回退,\n * 使旧删除字典流程仍能解码。传入 `0` 会同时关闭随机前缀与字典插入。\n * 该格式只是可逆编码,不提供加密、完整性或身份认证。\n * @param value - 任意可由 `encodeURIComponent` 处理的 Unicode 文本。\n * @param prefixLength - 随机字母前缀长度;默认 `6`。\n * @returns 带随机前缀和兼容字典字符的 Base64 文本;空输入返回空字符串。\n * @throws `RangeError` 当前缀长度不是非负安全整数;输入包含孤立代理项时保留平台错误。\n */\nexport function encodeSecureBase64(value: string, prefixLength: number = defaultRandomPrefixLength): string {\n\tif (value.length === 0) return \"\";\n\tassertPrefixLength(prefixLength);\n\tconst prefix = createRandomPrefix(prefixLength);\n\tlet encoded = encodeLatin1Base64(encodeURIComponent(value));\n\tif (prefixLength !== 0) encoded = insertDictionaryCharacters(encoded);\n\treturn prefix + encoded;\n}\n\n/**\n * 解码 {@link encodeSecureBase64} 生成的 SecureBase64 兼容格式。\n *\n * @param value - SecureBase64 文本;必须使用与编码时相同的前缀长度。\n * @param prefixLength - 需要移除的前缀长度;默认 `6`,传入 `0` 时不移除字典字符。\n * @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 Unicode 字符串;空输入返回空字符串。\n * @throws `RangeError` 当前缀长度不是非负安全整数;载荷、Base64 或 URI 编码非法时抛出 `TypeError` 或 `URIError`。\n */\nexport function decodeSecureBase64(value: string, prefixLength: number = defaultRandomPrefixLength): DecodedText {\n\tif (value.length === 0) return createDecodedText(\"\");\n\tassertPrefixLength(prefixLength);\n\tif (prefixLength > value.length) throw new TypeError(\"Base64 值的长度小于配置的前缀长度。\");\n\tlet encoded = value.slice(prefixLength);\n\tif (prefixLength !== 0) encoded = removeDictionaryCharacters(encoded);\n\treturn createDecodedText(decodeURIComponent(decodeLatin1Base64(encoded)));\n}\n"],"mappings":"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"}
|
package/dist/color/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @throws `RangeError` 当值非有限或超出 0 至 255。
|
|
8
8
|
*/
|
|
9
9
|
const assertRgbChannel = (value, channel) => {
|
|
10
|
-
if (!Number.isFinite(value) || value < 0 || value > 255) throw new RangeError(
|
|
10
|
+
if (!Number.isFinite(value) || value < 0 || value > 255) throw new RangeError(`\`${channel}\` 必须是 0 到 255 之间的有限数。`);
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
13
|
* 校验透明度通道。
|
|
@@ -16,7 +16,7 @@ const assertRgbChannel = (value, channel) => {
|
|
|
16
16
|
* @throws `RangeError` 当值非有限或超出闭区间 `[0, 1]`。
|
|
17
17
|
*/
|
|
18
18
|
const assertAlpha = (value) => {
|
|
19
|
-
if (!Number.isFinite(value) || value < 0 || value > 1) throw new RangeError("alpha
|
|
19
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new RangeError("`alpha` 必须是 0 到 1 之间的有限数。");
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* 规范化十六进制颜色文本。
|
|
@@ -32,7 +32,7 @@ const normalizeHexColor = (value) => {
|
|
|
32
32
|
4,
|
|
33
33
|
6,
|
|
34
34
|
8
|
|
35
|
-
].includes(normalized.length) || !/^[\dA-F]+$/iu.test(normalized)) throw new TypeError("
|
|
35
|
+
].includes(normalized.length) || !/^[\dA-F]+$/iu.test(normalized)) throw new TypeError("十六进制颜色必须包含 3、4、6 或 8 个十六进制字符。");
|
|
36
36
|
return normalized.length <= 4 ? Array.from(normalized, (character) => `${character}${character}`).join("") : normalized;
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
@@ -84,7 +84,7 @@ function formatHexColor(color, includeAlpha = "alpha" in color && color.alpha <
|
|
|
84
84
|
* @throws 颜色非法时抛出 `TypeError`;比例非法时抛出 `RangeError`。
|
|
85
85
|
*/
|
|
86
86
|
function mixHexColors(first, second, amount) {
|
|
87
|
-
if (!Number.isFinite(amount) || amount < 0 || amount > 1) throw new RangeError("amount
|
|
87
|
+
if (!Number.isFinite(amount) || amount < 0 || amount > 1) throw new RangeError("`amount` 必须是 0 到 1 之间的有限数。");
|
|
88
88
|
const left = parseHexColor(first);
|
|
89
89
|
const right = parseHexColor(second);
|
|
90
90
|
/**
|
package/dist/color/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/color/index.ts"],"sourcesContent":["/** 0 至 255 范围的 RGB 颜色。 */\nexport interface RgbColor {\n\t/** 蓝色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tblue: number;\n\t/** 绿色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tgreen: number;\n\t/** 红色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tred: number;\n}\n\n/** 带 0 至 1 Alpha 通道的 RGB 颜色。 */\nexport interface RgbaColor extends RgbColor {\n\t/** 不透明度;必须是闭区间 `[0, 1]` 内的有限数,`0` 完全透明,`1` 完全不透明。 */\n\talpha: number;\n}\n\n/**\n * 校验 RGB 颜色通道。\n *\n * @param value - 待校验通道值。\n * @param channel - 用于错误消息的通道名称。\n * @throws `RangeError` 当值非有限或超出 0 至 255。\n */\nconst assertRgbChannel = (value: number, channel: string): void => {\n\tif (!Number.isFinite(value) || value < 0 || value > 255) {\n\t\tthrow new RangeError(
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/color/index.ts"],"sourcesContent":["/** 0 至 255 范围的 RGB 颜色。 */\nexport interface RgbColor {\n\t/** 蓝色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tblue: number;\n\t/** 绿色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tgreen: number;\n\t/** 红色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tred: number;\n}\n\n/** 带 0 至 1 Alpha 通道的 RGB 颜色。 */\nexport interface RgbaColor extends RgbColor {\n\t/** 不透明度;必须是闭区间 `[0, 1]` 内的有限数,`0` 完全透明,`1` 完全不透明。 */\n\talpha: number;\n}\n\n/**\n * 校验 RGB 颜色通道。\n *\n * @param value - 待校验通道值。\n * @param channel - 用于错误消息的通道名称。\n * @throws `RangeError` 当值非有限或超出 0 至 255。\n */\nconst assertRgbChannel = (value: number, channel: string): void => {\n\tif (!Number.isFinite(value) || value < 0 || value > 255) {\n\t\tthrow new RangeError(`\\`${channel}\\` 必须是 0 到 255 之间的有限数。`);\n\t}\n};\n\n/**\n * 校验透明度通道。\n *\n * @param value - 待校验 Alpha 值。\n * @throws `RangeError` 当值非有限或超出闭区间 `[0, 1]`。\n */\nconst assertAlpha = (value: number): void => {\n\tif (!Number.isFinite(value) || value < 0 || value > 1) {\n\t\tthrow new RangeError(\"`alpha` 必须是 0 到 1 之间的有限数。\");\n\t}\n};\n\n/**\n * 规范化十六进制颜色文本。\n *\n * @param value - 可带 `#` 的 3、4、6 或 8 位颜色文本。\n * @returns 不带 `#` 的 6 或 8 位文本。\n * @throws `TypeError` 当长度或字符不符合十六进制颜色格式。\n */\nconst normalizeHexColor = (value: string): string => {\n\tconst normalized = value.startsWith(\"#\") ? value.slice(1) : value;\n\tif (![3, 4, 6, 8].includes(normalized.length) || !/^[\\dA-F]+$/iu.test(normalized)) {\n\t\tthrow new TypeError(\"十六进制颜色必须包含 3、4、6 或 8 个十六进制字符。\");\n\t}\n\treturn normalized.length <= 4 ? Array.from(normalized, (character) => `${character}${character}`).join(\"\") : normalized;\n};\n\n/**\n * 格式化单个颜色通道。\n *\n * @param value - 已校验的 0 至 255 通道值。\n * @returns 舍入后的两位小写十六进制文本。\n */\nconst formatHexChannel = (value: number): string => Math.round(value).toString(16).padStart(2, \"0\");\n\n/**\n * 解析可带可不带 `#` 的 `rgb`、`rgba`、`rrggbb` 或 `rrggbbaa`。\n *\n * @param value - 十六进制颜色文本。\n * @returns 标准化的 RGBA 对象;省略 Alpha 时为 1。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function parseHexColor(value: string): RgbaColor {\n\tconst normalized = normalizeHexColor(value);\n\treturn {\n\t\tred: Number.parseInt(normalized.slice(0, 2), 16),\n\t\tgreen: Number.parseInt(normalized.slice(2, 4), 16),\n\t\tblue: Number.parseInt(normalized.slice(4, 6), 16),\n\t\talpha: normalized.length === 8 ? Number.parseInt(normalized.slice(6, 8), 16) / 255 : 1,\n\t};\n}\n\n/**\n * 把 RGB 或 RGBA 对象格式化为小写十六进制颜色。\n *\n * @param color - 颜色通道;RGB 会四舍五入到最近整数。\n * @param includeAlpha - 是否输出 Alpha;默认只在传入 Alpha 且小于 1 时输出。\n * @returns 小写 `#rrggbb` 或 `#rrggbbaa` 文本。\n * @throws 通道或 Alpha 非法时抛出 `RangeError`。\n */\nexport function formatHexColor(color: RgbColor | RgbaColor, includeAlpha: boolean = \"alpha\" in color && color.alpha < 1): string {\n\tassertRgbChannel(color.red, \"red\");\n\tassertRgbChannel(color.green, \"green\");\n\tassertRgbChannel(color.blue, \"blue\");\n\tconst alpha = \"alpha\" in color ? color.alpha : 1;\n\tassertAlpha(alpha);\n\tconst rgb = `${formatHexChannel(color.red)}${formatHexChannel(color.green)}${formatHexChannel(color.blue)}`;\n\treturn `#${rgb}${includeAlpha ? formatHexChannel(alpha * 255) : \"\"}`;\n}\n\n/**\n * 线性混合两种十六进制颜色,包括 Alpha 通道。\n *\n * @param first - `amount = 0` 时的颜色。\n * @param second - `amount = 1` 时的颜色。\n * @param amount - 0 至 1 的混合比例。\n * @returns 小写十六进制颜色;任一输入含透明度时保留 Alpha。\n * @throws 颜色非法时抛出 `TypeError`;比例非法时抛出 `RangeError`。\n */\nexport function mixHexColors(first: string, second: string, amount: number): string {\n\tif (!Number.isFinite(amount) || amount < 0 || amount > 1) {\n\t\tthrow new RangeError(\"`amount` 必须是 0 到 1 之间的有限数。\");\n\t}\n\tconst left = parseHexColor(first);\n\tconst right = parseHexColor(second);\n\t/**\n\t * 在单个 RGBA 通道上执行与外层相同权重的线性混合。\n\t *\n\t * @param start - 第一个颜色的通道值。\n\t * @param end - 第二个颜色的通道值。\n\t * @returns 按外层 `amount` 线性混合后的通道值。\n\t */\n\tconst mix = (start: number, end: number): number => start + (end - start) * amount;\n\treturn formatHexColor(\n\t\t{\n\t\t\tred: mix(left.red, right.red),\n\t\t\tgreen: mix(left.green, right.green),\n\t\t\tblue: mix(left.blue, right.blue),\n\t\t\talpha: mix(left.alpha, right.alpha),\n\t\t},\n\t\tleft.alpha < 1 || right.alpha < 1\n\t);\n}\n\n/**\n * 按比例向黑色混合。\n *\n * @param color - 合法十六进制颜色。\n * @param amount - 0 至 1 的混合比例。\n * @returns 混入黑色后的十六进制颜色;参数与异常语义见 {@link mixHexColors}。\n */\nexport function mixHexColorWithBlack(color: string, amount: number): string {\n\treturn mixHexColors(color, \"#000000\", amount);\n}\n\n/**\n * 按比例向白色混合。\n *\n * @param color - 合法十六进制颜色。\n * @param amount - 0 至 1 的混合比例。\n * @returns 混入白色后的十六进制颜色;参数与异常语义见 {@link mixHexColors}。\n */\nexport function mixHexColorWithWhite(color: string, amount: number): string {\n\treturn mixHexColors(color, \"#ffffff\", amount);\n}\n\n/**\n * 按 WCAG sRGB 转换曲线线性化颜色通道。\n *\n * @param channel - 已校验的 0 至 255 通道值。\n * @returns 0 至 1 的线性光值。\n */\nconst linearizeSrgbChannel = (channel: number): number => {\n\tconst value = channel / 255;\n\treturn value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n};\n\n/**\n * 计算 WCAG sRGB 相对亮度。\n *\n * @remarks Alpha 通道不会参与计算;半透明颜色应先与实际背景混合。\n * @param color - 合法十六进制颜色。\n * @returns 0 至 1 的相对亮度。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function relativeLuminance(color: string): number {\n\tconst { red, green, blue } = parseHexColor(color);\n\treturn 0.2126 * linearizeSrgbChannel(red) + 0.7152 * linearizeSrgbChannel(green) + 0.0722 * linearizeSrgbChannel(blue);\n}\n\n/**\n * 计算两种不透明颜色的 WCAG 对比度,范围 1 至 21。\n *\n * @remarks 返回比值本身,不代表特定字号或 WCAG 等级必然通过。\n * @param first - 第一种十六进制颜色。\n * @param second - 第二种十六进制颜色。\n * @returns 较亮颜色与较暗颜色的对比度。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function contrastRatio(first: string, second: string): number {\n\tconst firstLuminance = relativeLuminance(first);\n\tconst secondLuminance = relativeLuminance(second);\n\tconst lighter = Math.max(firstLuminance, secondLuminance);\n\tconst darker = Math.min(firstLuminance, secondLuminance);\n\treturn (lighter + 0.05) / (darker + 0.05);\n}\n\n/**\n * 从两个候选颜色中选择与背景对比度更高的一项。\n *\n * @param background - 实际不透明背景色。\n * @param first - 第一候选,默认黑色。\n * @param second - 第二候选,默认白色。\n * @returns 对比度较高的原始候选字符串;相同时返回 `first`。\n * @throws 任一颜色非法时抛出 `TypeError`。\n */\nexport function pickHigherContrastColor(background: string, first = \"#000000\", second = \"#ffffff\"): string {\n\treturn contrastRatio(background, first) >= contrastRatio(background, second) ? first : second;\n}\n"],"mappings":"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"}
|
package/dist/crypto/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DecodedText } from "../internal/text.mjs";
|
|
1
2
|
import "crypto-js/pad-ansix923.js";
|
|
2
3
|
//#region src/crypto/index.d.ts
|
|
3
4
|
/** AES 分组密码模式;与 .NET `CipherMode.CBC` 和 `CipherMode.ECB` 对应。 */
|
|
@@ -177,10 +178,10 @@ declare function AESEncrypt(dataStr: string, key: string, vector: string, cipher
|
|
|
177
178
|
* @param vector - 加密时使用的初始化向量文本。
|
|
178
179
|
* @param cipherMode - AES 分组模式,默认 `CBC`。
|
|
179
180
|
* @param paddingMode - AES 填充模式,默认 `PKCS7`。
|
|
180
|
-
* @returns
|
|
181
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串;输入、密钥或 IV 为空白时返回 `null`。
|
|
181
182
|
* @throws 模式、填充、Base64、密钥或密文无效时抛出错误。
|
|
182
183
|
*/
|
|
183
|
-
declare function AESDecrypt(dataStr: string, key: string, vector: string, cipherMode?: AesCipherMode, paddingMode?: AesPaddingMode):
|
|
184
|
+
declare function AESDecrypt(dataStr: string, key: string, vector: string, cipherMode?: AesCipherMode, paddingMode?: AesPaddingMode): DecodedText | null;
|
|
184
185
|
/**
|
|
185
186
|
* 使用 SHA-256 归一化文本密钥,再以 AES-256-GCM 认证加密 UTF-8 文本。
|
|
186
187
|
*
|
|
@@ -196,10 +197,10 @@ declare function AESEncryptAuthenticated(plaintext: string, key: string): Promis
|
|
|
196
197
|
*
|
|
197
198
|
* @param payload - Base64 编码的 v1 AES-GCM 二进制载荷。
|
|
198
199
|
* @param key - 加密时使用的非空 UTF-8 文本密钥。
|
|
199
|
-
* @returns
|
|
200
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串。
|
|
200
201
|
* @throws 载荷格式无效、密钥错误或认证失败时抛出错误。
|
|
201
202
|
*/
|
|
202
|
-
declare function AESDecryptAuthenticated(payload: string, key: string): Promise<
|
|
203
|
+
declare function AESDecryptAuthenticated(payload: string, key: string): Promise<DecodedText>;
|
|
203
204
|
/**
|
|
204
205
|
* 使用 PBKDF2-HMAC-SHA-256 派生密钥,再以 AES-256-GCM 认证加密 UTF-8 文本。
|
|
205
206
|
*
|
|
@@ -218,11 +219,11 @@ declare function AESEncryptWithPassword(plaintext: string, password: string, ite
|
|
|
218
219
|
*
|
|
219
220
|
* @param payload - 未修改的 v1 载荷,最大约 16 MiB 文本。
|
|
220
221
|
* @param password - 加密时使用的口令。
|
|
221
|
-
* @returns
|
|
222
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串。
|
|
222
223
|
* @throws 格式或字段非法时抛出 `TypeError`,载荷过大时抛出 `RangeError`,认证或密码
|
|
223
224
|
* 失败及缺少平台能力时抛出 `Error`。
|
|
224
225
|
*/
|
|
225
|
-
declare function AESDecryptWithPassword(payload: string, password: string): Promise<
|
|
226
|
+
declare function AESDecryptWithPassword(payload: string, password: string): Promise<DecodedText>;
|
|
226
227
|
/**
|
|
227
228
|
* 生成可供 RSA-OAEP/SHA-256 与 RSA-PSS/SHA-256 共用的 PEM 密钥对。
|
|
228
229
|
*
|
|
@@ -245,10 +246,10 @@ declare function RSAEncryptOAEP(plaintext: string, publicKeyPem: string): Promis
|
|
|
245
246
|
*
|
|
246
247
|
* @param ciphertext - Base64 编码的 RSA 密文。
|
|
247
248
|
* @param privateKeyPem - 未加密的 PKCS#8 PEM 私钥。
|
|
248
|
-
* @returns
|
|
249
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串。
|
|
249
250
|
* @throws 私钥、Base64 或密文无效时抛出错误。
|
|
250
251
|
*/
|
|
251
|
-
declare function RSADecryptOAEP(ciphertext: string, privateKeyPem: string): Promise<
|
|
252
|
+
declare function RSADecryptOAEP(ciphertext: string, privateKeyPem: string): Promise<DecodedText>;
|
|
252
253
|
/**
|
|
253
254
|
* 使用 RSA-PSS/SHA-256 私钥签名文本或字节。
|
|
254
255
|
*
|
package/dist/crypto/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { encodeUtf8, getTextDecoder } from "../internal/text.mjs";
|
|
1
|
+
import { createDecodedText, encodeUtf8, getTextDecoder } from "../internal/text.mjs";
|
|
2
2
|
import { decodeBase64Bytes, decodeBase64UrlBytes, encodeBase64Bytes, encodeBase64UrlBytes } from "../base64/index.mjs";
|
|
3
3
|
import AES from "crypto-js/aes.js";
|
|
4
4
|
import CryptoCore from "crypto-js/core.js";
|
|
@@ -40,7 +40,7 @@ const authenticatedAesPayloadVersion = 1;
|
|
|
40
40
|
const requireWebCrypto = () => {
|
|
41
41
|
const crypto = globalThis.crypto;
|
|
42
42
|
const subtle = crypto?.subtle;
|
|
43
|
-
if (typeof subtle?.decrypt !== "function" || typeof subtle.deriveBits !== "function" || typeof subtle.deriveKey !== "function" || typeof subtle.digest !== "function" || typeof subtle.encrypt !== "function" || typeof subtle.exportKey !== "function" || typeof subtle.generateKey !== "function" || typeof subtle.importKey !== "function" || typeof subtle.sign !== "function" || typeof subtle.verify !== "function") throw new Error("Web Crypto SubtleCrypto
|
|
43
|
+
if (typeof subtle?.decrypt !== "function" || typeof subtle.deriveBits !== "function" || typeof subtle.deriveKey !== "function" || typeof subtle.digest !== "function" || typeof subtle.encrypt !== "function" || typeof subtle.exportKey !== "function" || typeof subtle.generateKey !== "function" || typeof subtle.importKey !== "function" || typeof subtle.sign !== "function" || typeof subtle.verify !== "function") throw new Error("当前运行环境不支持 Web Crypto SubtleCrypto。");
|
|
44
44
|
return crypto;
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
@@ -82,7 +82,7 @@ const fromPem = (value, label) => {
|
|
|
82
82
|
const header = `-----BEGIN ${label}-----`;
|
|
83
83
|
const footer = `-----END ${label}-----`;
|
|
84
84
|
const trimmed = value.trim();
|
|
85
|
-
if (!trimmed.startsWith(header) || !trimmed.endsWith(footer)) throw new TypeError(
|
|
85
|
+
if (!trimmed.startsWith(header) || !trimmed.endsWith(footer)) throw new TypeError(`应提供 ${label} 格式的 PEM 值。`);
|
|
86
86
|
const encoded = trimmed.slice(header.length, -footer.length).replace(/\s+/gu, "");
|
|
87
87
|
return toArrayBuffer(decodeBase64Bytes(encoded));
|
|
88
88
|
};
|
|
@@ -110,7 +110,7 @@ const exportKeyPair = async (keyPair) => {
|
|
|
110
110
|
*/
|
|
111
111
|
const assertKeyPair = (value) => {
|
|
112
112
|
if ("privateKey" in value && "publicKey" in value) return value;
|
|
113
|
-
throw new Error("
|
|
113
|
+
throw new Error("当前运行环境未生成密钥对。");
|
|
114
114
|
};
|
|
115
115
|
/**
|
|
116
116
|
* 校验并编码密码。
|
|
@@ -122,8 +122,8 @@ const assertKeyPair = (value) => {
|
|
|
122
122
|
*/
|
|
123
123
|
const encodeValidatedPassword = (password) => {
|
|
124
124
|
const bytes = encodeUtf8(password);
|
|
125
|
-
if (bytes.length === 0) throw new TypeError("
|
|
126
|
-
if (bytes.length > maximumPasswordBytes) throw new RangeError(`
|
|
125
|
+
if (bytes.length === 0) throw new TypeError("加密密码不能为空。");
|
|
126
|
+
if (bytes.length > maximumPasswordBytes) throw new RangeError(`UTF-8 密码不能超过 ${maximumPasswordBytes} 字节。`);
|
|
127
127
|
return bytes;
|
|
128
128
|
};
|
|
129
129
|
/**
|
|
@@ -134,7 +134,7 @@ const encodeValidatedPassword = (password) => {
|
|
|
134
134
|
* @throws `RangeError` 当值不是 100,000 至 5,000,000 的安全整数。
|
|
135
135
|
*/
|
|
136
136
|
const validateIterations = (iterations) => {
|
|
137
|
-
if (!Number.isSafeInteger(iterations) || iterations < minimumPbkdf2Iterations || iterations > maximumPbkdf2Iterations) throw new RangeError(
|
|
137
|
+
if (!Number.isSafeInteger(iterations) || iterations < minimumPbkdf2Iterations || iterations > maximumPbkdf2Iterations) throw new RangeError(`\`iterations\` 必须是 ${minimumPbkdf2Iterations} 到 ${maximumPbkdf2Iterations} 之间的安全整数。`);
|
|
138
138
|
return iterations;
|
|
139
139
|
};
|
|
140
140
|
/** 把字节格式化为小写十六进制。 */
|
|
@@ -153,7 +153,7 @@ const toUpperHex = (value) => toLowerHex(value).toUpperCase();
|
|
|
153
153
|
*/
|
|
154
154
|
const computeHmacBytes = async (value, key, hash) => {
|
|
155
155
|
const keyBytes = encodeUtf8(key);
|
|
156
|
-
if (keyBytes.length === 0) throw new TypeError("
|
|
156
|
+
if (keyBytes.length === 0) throw new TypeError("HMAC 密钥不能为空。");
|
|
157
157
|
const crypto = requireWebCrypto();
|
|
158
158
|
const cryptoKey = await crypto.subtle.importKey("raw", toArrayBuffer(keyBytes), {
|
|
159
159
|
hash,
|
|
@@ -171,7 +171,7 @@ const computeHmacBytes = async (value, key, hash) => {
|
|
|
171
171
|
* @throws 参数非法时抛出 `RangeError`。
|
|
172
172
|
*/
|
|
173
173
|
function GenerateRandomBytes(length) {
|
|
174
|
-
if (!Number.isSafeInteger(length) || length < 0 || length > 65536) throw new RangeError("length
|
|
174
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 65536) throw new RangeError("`length` 必须是 0 到 65,536 之间的安全整数。");
|
|
175
175
|
const bytes = new Uint8Array(length);
|
|
176
176
|
const crypto = globalThis.crypto;
|
|
177
177
|
if (typeof crypto?.getRandomValues === "function") return crypto.getRandomValues(bytes);
|
|
@@ -315,8 +315,8 @@ async function HMACSHA512Encrypt(value, key) {
|
|
|
315
315
|
*/
|
|
316
316
|
async function PBKDF2SHA256(password, salt, iterations = defaultPbkdf2Iterations, outputLength = 32) {
|
|
317
317
|
const passwordBytes = encodeValidatedPassword(password);
|
|
318
|
-
if (salt.length < 8) throw new RangeError("
|
|
319
|
-
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 1024) throw new RangeError("outputLength
|
|
318
|
+
if (salt.length < 8) throw new RangeError("PBKDF2 盐值必须至少包含 8 字节,建议不少于 16 字节。");
|
|
319
|
+
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 1024) throw new RangeError("`outputLength` 必须是 1 到 1,024 之间的安全整数。");
|
|
320
320
|
const crypto = requireWebCrypto();
|
|
321
321
|
const material = await crypto.subtle.importKey("raw", toArrayBuffer(passwordBytes), "PBKDF2", false, ["deriveBits"]);
|
|
322
322
|
const derived = await crypto.subtle.deriveBits({
|
|
@@ -374,7 +374,7 @@ async function VerifyPasswordPBKDF2SHA256(password, passwordHash) {
|
|
|
374
374
|
* @returns 与 `salt` 和 `info` 绑定的派生密钥。
|
|
375
375
|
*/
|
|
376
376
|
async function HKDFSHA256(inputKeyMaterial, salt = /* @__PURE__ */ new Uint8Array(), info = /* @__PURE__ */ new Uint8Array(), outputLength = 32) {
|
|
377
|
-
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 8160) throw new RangeError("outputLength
|
|
377
|
+
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 8160) throw new RangeError("`outputLength` 必须是 1 到 8,160 之间的安全整数。");
|
|
378
378
|
const crypto = requireWebCrypto();
|
|
379
379
|
const material = await crypto.subtle.importKey("raw", toArrayBuffer(inputKeyMaterial), "HKDF", false, ["deriveBits"]);
|
|
380
380
|
const derived = await crypto.subtle.deriveBits({
|
|
@@ -400,7 +400,7 @@ async function HKDFSHA256(inputKeyMaterial, salt = /* @__PURE__ */ new Uint8Arra
|
|
|
400
400
|
*/
|
|
401
401
|
function AESEncrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKCS7") {
|
|
402
402
|
if (dataStr.trim().length === 0 || key.trim().length === 0 || vector.trim().length === 0) return null;
|
|
403
|
-
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("cipherMode
|
|
403
|
+
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("`cipherMode` 必须是 `CBC` 或 `ECB`。");
|
|
404
404
|
let padding = Pkcs7;
|
|
405
405
|
switch (paddingMode) {
|
|
406
406
|
case "None":
|
|
@@ -416,9 +416,9 @@ function AESEncrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
416
416
|
case "ISO10126":
|
|
417
417
|
padding = Iso10126;
|
|
418
418
|
break;
|
|
419
|
-
default: throw new RangeError("paddingMode
|
|
419
|
+
default: throw new RangeError("不支持当前 `paddingMode`。");
|
|
420
420
|
}
|
|
421
|
-
if (paddingMode === "None" && encodeUtf8(dataStr).length % 16 !== 0) throw new RangeError("
|
|
421
|
+
if (paddingMode === "None" && encodeUtf8(dataStr).length % 16 !== 0) throw new RangeError("当 `paddingMode` 为 `None` 时,AES 明文长度必须是 16 字节的整数倍。");
|
|
422
422
|
const keyBytes = Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
423
423
|
const vectorBytes = Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
424
424
|
return AES.encrypt(dataStr, keyBytes, cipherMode === "CBC" ? {
|
|
@@ -439,12 +439,12 @@ function AESEncrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
439
439
|
* @param vector - 加密时使用的初始化向量文本。
|
|
440
440
|
* @param cipherMode - AES 分组模式,默认 `CBC`。
|
|
441
441
|
* @param paddingMode - AES 填充模式,默认 `PKCS7`。
|
|
442
|
-
* @returns
|
|
442
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串;输入、密钥或 IV 为空白时返回 `null`。
|
|
443
443
|
* @throws 模式、填充、Base64、密钥或密文无效时抛出错误。
|
|
444
444
|
*/
|
|
445
445
|
function AESDecrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKCS7") {
|
|
446
446
|
if (dataStr.trim().length === 0 || key.trim().length === 0 || vector.trim().length === 0) return null;
|
|
447
|
-
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("cipherMode
|
|
447
|
+
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("`cipherMode` 必须是 `CBC` 或 `ECB`。");
|
|
448
448
|
let padding = Pkcs7;
|
|
449
449
|
switch (paddingMode) {
|
|
450
450
|
case "None":
|
|
@@ -460,20 +460,20 @@ function AESDecrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
460
460
|
case "ISO10126":
|
|
461
461
|
padding = Iso10126;
|
|
462
462
|
break;
|
|
463
|
-
default: throw new RangeError("paddingMode
|
|
463
|
+
default: throw new RangeError("不支持当前 `paddingMode`。");
|
|
464
464
|
}
|
|
465
465
|
const ciphertextBytes = decodeBase64Bytes(dataStr);
|
|
466
|
-
if (ciphertextBytes.length === 0 || ciphertextBytes.length % 16 !== 0) throw new RangeError("AES
|
|
466
|
+
if (ciphertextBytes.length === 0 || ciphertextBytes.length % 16 !== 0) throw new RangeError("AES 密文必须至少包含一个完整的 16 字节块。");
|
|
467
467
|
const keyBytes = Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
468
468
|
const vectorBytes = Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
469
|
-
return AES.decrypt(dataStr, keyBytes, cipherMode === "CBC" ? {
|
|
469
|
+
return createDecodedText(AES.decrypt(dataStr, keyBytes, cipherMode === "CBC" ? {
|
|
470
470
|
iv: vectorBytes,
|
|
471
471
|
padding
|
|
472
472
|
} : {
|
|
473
473
|
iv: vectorBytes,
|
|
474
474
|
mode: ECB,
|
|
475
475
|
padding
|
|
476
|
-
}).toString(Utf8);
|
|
476
|
+
}).toString(Utf8));
|
|
477
477
|
}
|
|
478
478
|
/**
|
|
479
479
|
* 使用 SHA-256 归一化文本密钥,再以 AES-256-GCM 认证加密 UTF-8 文本。
|
|
@@ -485,7 +485,7 @@ function AESDecrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
485
485
|
* @throws 密钥为空或运行时缺少 Web Crypto 时抛出错误。
|
|
486
486
|
*/
|
|
487
487
|
async function AESEncryptAuthenticated(plaintext, key) {
|
|
488
|
-
if (key.trim().length === 0) throw new TypeError("
|
|
488
|
+
if (key.trim().length === 0) throw new TypeError("加密密钥不能为空。");
|
|
489
489
|
const crypto = requireWebCrypto();
|
|
490
490
|
const keyBytes = await SHA256Bytes(key);
|
|
491
491
|
const cryptoKey = await crypto.subtle.importKey("raw", toArrayBuffer(keyBytes), { name: "AES-GCM" }, false, ["encrypt"]);
|
|
@@ -508,13 +508,13 @@ async function AESEncryptAuthenticated(plaintext, key) {
|
|
|
508
508
|
*
|
|
509
509
|
* @param payload - Base64 编码的 v1 AES-GCM 二进制载荷。
|
|
510
510
|
* @param key - 加密时使用的非空 UTF-8 文本密钥。
|
|
511
|
-
* @returns
|
|
511
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串。
|
|
512
512
|
* @throws 载荷格式无效、密钥错误或认证失败时抛出错误。
|
|
513
513
|
*/
|
|
514
514
|
async function AESDecryptAuthenticated(payload, key) {
|
|
515
|
-
if (key.trim().length === 0) throw new TypeError("
|
|
515
|
+
if (key.trim().length === 0) throw new TypeError("加密密钥不能为空。");
|
|
516
516
|
const decoded = decodeBase64Bytes(payload);
|
|
517
|
-
if (decoded.length < 29 || decoded[0] !== authenticatedAesPayloadVersion) throw new TypeError("
|
|
517
|
+
if (decoded.length < 29 || decoded[0] !== authenticatedAesPayloadVersion) throw new TypeError("不支持该 AES-GCM 载荷格式或版本。");
|
|
518
518
|
const nonce = decoded.subarray(1, 13);
|
|
519
519
|
const tag = decoded.subarray(13, 29);
|
|
520
520
|
const ciphertext = decoded.subarray(29);
|
|
@@ -529,7 +529,7 @@ async function AESDecryptAuthenticated(payload, key) {
|
|
|
529
529
|
name: "AES-GCM",
|
|
530
530
|
tagLength: 128
|
|
531
531
|
}, cryptoKey, toArrayBuffer(ciphertextAndTag));
|
|
532
|
-
return getTextDecoder().decode(plaintext);
|
|
532
|
+
return createDecodedText(getTextDecoder().decode(plaintext));
|
|
533
533
|
}
|
|
534
534
|
/**
|
|
535
535
|
* 使用 PBKDF2-HMAC-SHA-256 派生密钥,再以 AES-256-GCM 认证加密 UTF-8 文本。
|
|
@@ -546,7 +546,7 @@ async function AESDecryptAuthenticated(payload, key) {
|
|
|
546
546
|
async function AESEncryptWithPassword(plaintext, password, iterations = defaultPbkdf2Iterations) {
|
|
547
547
|
const passwordBytes = encodeValidatedPassword(password);
|
|
548
548
|
const plaintextBytes = encodeUtf8(plaintext);
|
|
549
|
-
if (plaintextBytes.length > maximumPlaintextBytes) throw new RangeError(`
|
|
549
|
+
if (plaintextBytes.length > maximumPlaintextBytes) throw new RangeError(`UTF-8 明文不能超过 ${maximumPlaintextBytes} 字节。`);
|
|
550
550
|
const validatedIterations = validateIterations(iterations);
|
|
551
551
|
const crypto = requireWebCrypto();
|
|
552
552
|
const salt = GenerateRandomBytes(16);
|
|
@@ -580,15 +580,15 @@ async function AESEncryptWithPassword(plaintext, password, iterations = defaultP
|
|
|
580
580
|
*
|
|
581
581
|
* @param payload - 未修改的 v1 载荷,最大约 16 MiB 文本。
|
|
582
582
|
* @param password - 加密时使用的口令。
|
|
583
|
-
* @returns
|
|
583
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串。
|
|
584
584
|
* @throws 格式或字段非法时抛出 `TypeError`,载荷过大时抛出 `RangeError`,认证或密码
|
|
585
585
|
* 失败及缺少平台能力时抛出 `Error`。
|
|
586
586
|
*/
|
|
587
587
|
async function AESDecryptWithPassword(payload, password) {
|
|
588
588
|
const passwordBytes = encodeValidatedPassword(password);
|
|
589
|
-
if (payload.length > maximumPayloadLength) throw new RangeError("
|
|
589
|
+
if (payload.length > maximumPayloadLength) throw new RangeError("加密载荷超出支持的大小。");
|
|
590
590
|
const parts = payload.split(":");
|
|
591
|
-
if (parts.length !== 5 || parts[0] !== encryptedPayloadPrefix) throw new TypeError("
|
|
591
|
+
if (parts.length !== 5 || parts[0] !== encryptedPayloadPrefix) throw new TypeError("不支持该加密载荷格式。");
|
|
592
592
|
let iterations;
|
|
593
593
|
let salt;
|
|
594
594
|
let iv;
|
|
@@ -598,9 +598,9 @@ async function AESDecryptWithPassword(payload, password) {
|
|
|
598
598
|
salt = decodeBase64UrlBytes(parts[2] ?? "");
|
|
599
599
|
iv = decodeBase64UrlBytes(parts[3] ?? "");
|
|
600
600
|
ciphertext = decodeBase64UrlBytes(parts[4] ?? "");
|
|
601
|
-
if (salt.length !== 16 || iv.length !== 12 || ciphertext.length < 16) throw new TypeError("
|
|
601
|
+
if (salt.length !== 16 || iv.length !== 12 || ciphertext.length < 16) throw new TypeError("加密载荷的字段长度无效。");
|
|
602
602
|
} catch (cause) {
|
|
603
|
-
throw new TypeError("
|
|
603
|
+
throw new TypeError("加密载荷包含无效字段。", { cause });
|
|
604
604
|
}
|
|
605
605
|
const crypto = requireWebCrypto();
|
|
606
606
|
const textDecoder = getTextDecoder();
|
|
@@ -621,9 +621,9 @@ async function AESDecryptWithPassword(payload, password) {
|
|
|
621
621
|
name: "AES-GCM",
|
|
622
622
|
tagLength: 128
|
|
623
623
|
}, key, toArrayBuffer(ciphertext));
|
|
624
|
-
return textDecoder.decode(plaintext);
|
|
624
|
+
return createDecodedText(textDecoder.decode(plaintext));
|
|
625
625
|
} catch (cause) {
|
|
626
|
-
throw new Error("
|
|
626
|
+
throw new Error("无法认证或解密载荷。", { cause });
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
629
|
/**
|
|
@@ -634,7 +634,7 @@ async function AESDecryptWithPassword(payload, password) {
|
|
|
634
634
|
* @throws 模数小于 2,048 或不是 256 的倍数时抛出 `RangeError`。
|
|
635
635
|
*/
|
|
636
636
|
async function GenerateRSAKeyPair(modulusLength = 2048) {
|
|
637
|
-
if (!Number.isSafeInteger(modulusLength) || modulusLength < 2048 || modulusLength % 256 !== 0) throw new RangeError("modulusLength
|
|
637
|
+
if (!Number.isSafeInteger(modulusLength) || modulusLength < 2048 || modulusLength % 256 !== 0) throw new RangeError("`modulusLength` 必须是大于或等于 2048 且能被 256 整除的安全整数。");
|
|
638
638
|
const crypto = requireWebCrypto();
|
|
639
639
|
const keyPair = assertKeyPair(await crypto.subtle.generateKey({
|
|
640
640
|
hash: "SHA-256",
|
|
@@ -666,7 +666,7 @@ async function RSAEncryptOAEP(plaintext, publicKeyPem) {
|
|
|
666
666
|
*
|
|
667
667
|
* @param ciphertext - Base64 编码的 RSA 密文。
|
|
668
668
|
* @param privateKeyPem - 未加密的 PKCS#8 PEM 私钥。
|
|
669
|
-
* @returns
|
|
669
|
+
* @returns 可直接使用或显式调用 `.parseJson<Value>()` 的原始 UTF-8 字符串。
|
|
670
670
|
* @throws 私钥、Base64 或密文无效时抛出错误。
|
|
671
671
|
*/
|
|
672
672
|
async function RSADecryptOAEP(ciphertext, privateKeyPem) {
|
|
@@ -676,7 +676,7 @@ async function RSADecryptOAEP(ciphertext, privateKeyPem) {
|
|
|
676
676
|
name: "RSA-OAEP"
|
|
677
677
|
}, false, ["decrypt"]);
|
|
678
678
|
const plaintext = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, key, toArrayBuffer(decodeBase64Bytes(ciphertext)));
|
|
679
|
-
return getTextDecoder().decode(plaintext);
|
|
679
|
+
return createDecodedText(getTextDecoder().decode(plaintext));
|
|
680
680
|
}
|
|
681
681
|
/**
|
|
682
682
|
* 使用 RSA-PSS/SHA-256 私钥签名文本或字节。
|