@jayfong/x-utils 3.3.0 → 3.4.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/lib/fs.d.mts CHANGED
@@ -1,8 +1,10 @@
1
1
  import fs from "fs-extra";
2
+ import { globby, globbyStream, globbySync } from "globby";
2
3
  /**
3
- * 额外的文件类型辅助方法:
4
+ * 额外的文件系统辅助方法:
4
5
  * - `getMimeType`:将扩展名 / 文件名 / MIME 字符串统一转换为 MIME 类型;
5
- * - `getExtension`:将 MIME 字符串 / 文件名统一转换为扩展名。
6
+ * - `getExtension`:将 MIME 字符串 / 文件名统一转换为扩展名;
7
+ * - `globby` / `globbySync` / `globbyStream`:按 glob 模式查找文件与目录。
6
8
  *
7
9
  * 这些方法会被合并到 `XFs` 中,与 `fs-extra` 的 API 一起导出。
8
10
  */
@@ -38,6 +40,66 @@ declare const XFsExtra: {
38
40
  * 4. 否则按扩展名/MIME 简写尝试 `mime.getExtension`,失败返回空字符串。
39
41
  */
40
42
  getExtension(typeOrName: string): string;
43
+ /**
44
+ * 按 glob 模式异步查找文件与目录。
45
+ *
46
+ * 转发自 `globby` 的异步版本,返回 `Promise`,适合 `await` 使用。
47
+ * 模式只支持正斜杠 `/`;拼接路径片段时请用 `path.posix.join`,
48
+ * Windows 上含反斜杠的模式会静默匹配失败。支持以 `!` 开头的取反
49
+ * 模式排除文件;当只传入取反模式(如 `['!*.json']`)时,会自动
50
+ * 前置一个匹配全部文件的兜底模式,再应用取反(可通过
51
+ * `options.expandNegationOnlyPatterns` 关闭)。
52
+ *
53
+ * @param patterns 一个 glob 模式或模式数组,支持取反排除。
54
+ * @param options 可选配置;除 `globby` 自身选项(如 `gitignore`、
55
+ * `cwd`)外,其余选项透传给 `fast-glob`,默认在
56
+ * `process.cwd()` 下查找。
57
+ * @returns 匹配到的路径数组;传入 `objectMode: true` 或
58
+ * `stats: true` 时返回 `GlobEntry` 对象数组。
59
+ *
60
+ * @example
61
+ * await XFs.globby(['*.ts', '!*.test.ts']);
62
+ * // ['index.ts', 'utils.ts']
63
+ */
64
+ globby: typeof globby;
65
+ /**
66
+ * 按 glob 模式同步查找文件与目录。
67
+ *
68
+ * 转发自 `globby` 的同步版本,阻塞执行并直接返回结果,行为与
69
+ * `globby` 一致:模式只支持正斜杠 `/`,支持 `!` 取反排除,只传入
70
+ * 取反模式时自动前置匹配全部文件的兜底模式。
71
+ *
72
+ * @param patterns 一个 glob 模式或模式数组,支持取反排除。
73
+ * @param options 可选配置;除 `globby` 自身选项(如 `gitignore`、
74
+ * `cwd`)外,其余选项透传给 `fast-glob`,默认在
75
+ * `process.cwd()` 下查找。
76
+ * @returns 匹配到的路径数组;传入 `objectMode: true` 或
77
+ * `stats: true` 时返回 `GlobEntry` 对象数组。
78
+ *
79
+ * @example
80
+ * XFs.globbySync(['*.ts', '!*.test.ts']);
81
+ * // ['index.ts', 'utils.ts']
82
+ */
83
+ globbySync: typeof globbySync;
84
+ /**
85
+ * 按 glob 模式流式查找文件与目录。
86
+ *
87
+ * 转发自 `globby` 的流式版本,返回可异步迭代的 `Readable` 流,
88
+ * 适合边匹配边处理,避免一次性在内存中持有全部结果。模式与选项
89
+ * 行为同 `globby`:只支持正斜杠 `/`,支持 `!` 取反排除。
90
+ *
91
+ * @param patterns 一个 glob 模式或模式数组,支持取反排除。
92
+ * @param options 可选配置;除 `globby` 自身选项外,其余选项
93
+ * 透传给 `fast-glob`,默认在 `process.cwd()` 下查找。
94
+ * @returns 逐个产出匹配路径的可读流;传入 `objectMode: true` 或
95
+ * `stats: true` 时产出 `GlobEntry` 对象。
96
+ *
97
+ * @example
98
+ * for await (const path of XFs.globbyStream('*.ts')) {
99
+ * console.log(path);
100
+ * }
101
+ */
102
+ globbyStream: typeof globbyStream;
41
103
  };
42
104
  /**
43
105
  * `XFs` = `fs-extra` + 轻量文件类型工具。
package/lib/fs.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  import fs from "fs-extra";
2
2
  import mime from "mime";
3
+ import { globby, globbyStream, globbySync } from "globby";
3
4
  // src/fs.ts
4
5
  /**
5
- * 额外的文件类型辅助方法:
6
+ * 额外的文件系统辅助方法:
6
7
  * - `getMimeType`:将扩展名 / 文件名 / MIME 字符串统一转换为 MIME 类型;
7
- * - `getExtension`:将 MIME 字符串 / 文件名统一转换为扩展名。
8
+ * - `getExtension`:将 MIME 字符串 / 文件名统一转换为扩展名;
9
+ * - `globby` / `globbySync` / `globbyStream`:按 glob 模式查找文件与目录。
8
10
  *
9
11
  * 这些方法会被合并到 `XFs` 中,与 `fs-extra` 的 API 一起导出。
10
12
  */
@@ -50,7 +52,67 @@ const XFsExtra = {
50
52
  if (value.includes("/")) return mime.getExtension(value) || value.split("/").pop() || "";
51
53
  if (value.includes(".")) return value.split(".").pop();
52
54
  return mime.getExtension(value) || "";
53
- }
55
+ },
56
+ /**
57
+ * 按 glob 模式异步查找文件与目录。
58
+ *
59
+ * 转发自 `globby` 的异步版本,返回 `Promise`,适合 `await` 使用。
60
+ * 模式只支持正斜杠 `/`;拼接路径片段时请用 `path.posix.join`,
61
+ * Windows 上含反斜杠的模式会静默匹配失败。支持以 `!` 开头的取反
62
+ * 模式排除文件;当只传入取反模式(如 `['!*.json']`)时,会自动
63
+ * 前置一个匹配全部文件的兜底模式,再应用取反(可通过
64
+ * `options.expandNegationOnlyPatterns` 关闭)。
65
+ *
66
+ * @param patterns 一个 glob 模式或模式数组,支持取反排除。
67
+ * @param options 可选配置;除 `globby` 自身选项(如 `gitignore`、
68
+ * `cwd`)外,其余选项透传给 `fast-glob`,默认在
69
+ * `process.cwd()` 下查找。
70
+ * @returns 匹配到的路径数组;传入 `objectMode: true` 或
71
+ * `stats: true` 时返回 `GlobEntry` 对象数组。
72
+ *
73
+ * @example
74
+ * await XFs.globby(['*.ts', '!*.test.ts']);
75
+ * // ['index.ts', 'utils.ts']
76
+ */
77
+ globby,
78
+ /**
79
+ * 按 glob 模式同步查找文件与目录。
80
+ *
81
+ * 转发自 `globby` 的同步版本,阻塞执行并直接返回结果,行为与
82
+ * `globby` 一致:模式只支持正斜杠 `/`,支持 `!` 取反排除,只传入
83
+ * 取反模式时自动前置匹配全部文件的兜底模式。
84
+ *
85
+ * @param patterns 一个 glob 模式或模式数组,支持取反排除。
86
+ * @param options 可选配置;除 `globby` 自身选项(如 `gitignore`、
87
+ * `cwd`)外,其余选项透传给 `fast-glob`,默认在
88
+ * `process.cwd()` 下查找。
89
+ * @returns 匹配到的路径数组;传入 `objectMode: true` 或
90
+ * `stats: true` 时返回 `GlobEntry` 对象数组。
91
+ *
92
+ * @example
93
+ * XFs.globbySync(['*.ts', '!*.test.ts']);
94
+ * // ['index.ts', 'utils.ts']
95
+ */
96
+ globbySync,
97
+ /**
98
+ * 按 glob 模式流式查找文件与目录。
99
+ *
100
+ * 转发自 `globby` 的流式版本,返回可异步迭代的 `Readable` 流,
101
+ * 适合边匹配边处理,避免一次性在内存中持有全部结果。模式与选项
102
+ * 行为同 `globby`:只支持正斜杠 `/`,支持 `!` 取反排除。
103
+ *
104
+ * @param patterns 一个 glob 模式或模式数组,支持取反排除。
105
+ * @param options 可选配置;除 `globby` 自身选项外,其余选项
106
+ * 透传给 `fast-glob`,默认在 `process.cwd()` 下查找。
107
+ * @returns 逐个产出匹配路径的可读流;传入 `objectMode: true` 或
108
+ * `stats: true` 时产出 `GlobEntry` 对象。
109
+ *
110
+ * @example
111
+ * for await (const path of XFs.globbyStream('*.ts')) {
112
+ * console.log(path);
113
+ * }
114
+ */
115
+ globbyStream
54
116
  };
55
117
  /**
56
118
  * `XFs` = `fs-extra` + 轻量文件类型工具。
package/lib/html.d.mts ADDED
@@ -0,0 +1,103 @@
1
+ import { HTMLElement } from "node-html-parser";
2
+ /**
3
+ * HTML 转义与反转义工具。
4
+ *
5
+ * 基于 `fast-escape-html` 实现,用于在把不可信文本嵌入 HTML 前进行转义,
6
+ * 或将已转义的 HTML 实体还原为原始字符,避免标签错乱与 XSS 注入。
7
+ *
8
+ * 转义字符对照:
9
+ * | 原字符 | 转义结果 |
10
+ * | ------ | -------- |
11
+ * | `&` | `&` |
12
+ * | `<` | `&lt;` |
13
+ * | `>` | `&gt;` |
14
+ * | `"` | `&quot;` |
15
+ * | `'` | `&#39;` |
16
+ *
17
+ * @example
18
+ * const html = new XHtml();
19
+ * html.escape('<a href="x">\'</a>') // '&lt;a href=&quot;x&quot;&gt;&#39;&lt;/a&gt;'
20
+ * html.unescape('&lt;a&amp;b&gt;') // '<a&b>'
21
+ *
22
+ * @remarks
23
+ * - `escape` 是无状态转义:已含实体的文本会再次被转义(如 `&lt;` →
24
+ * `&amp;lt;`),因此不要对同一段文本重复调用;
25
+ * - `unescape` 只还原本工具转义出的实体(`&amp;`、`&lt;`、`&gt;`、
26
+ * `&quot;`、`&#39;`、`&apos;`),不处理 `&nbsp;`、`&copy;` 等其它命名
27
+ * 实体,也不处理 `&#65;`、`&#x41;` 这类十进制/十六进制数字实体;
28
+ * - 本工具面向 HTML 文本节点/属性值的转义场景,不负责 URL 编码,也不保证
29
+ * 脚本、样式等非 HTML 上下文的安全。
30
+ */
31
+ export declare class XHtml {
32
+ /**
33
+ * 将字符串转义为可安全嵌入 HTML 的实体文本。
34
+ *
35
+ * 处理规则:
36
+ * - `&`、`<`、`>`、`"`、`'` 会被替换为对应实体;
37
+ * - 其余字符(中文、字母、数字、换行、空白等)原样保留;
38
+ * - 空字符串返回空字符串。
39
+ *
40
+ * 边界与意外场景:
41
+ * - 已转义文本会被二次转义(如 `&lt;` → `&amp;lt;`),调用方需自行保证
42
+ * 同一段文本只转义一次。
43
+ *
44
+ * @param html 待转义的原始字符串。
45
+ * @returns 转义后的字符串。
46
+ *
47
+ * @example
48
+ * new XHtml().escape('<a href="x">') // '&lt;a href=&quot;x&quot;&gt;'
49
+ * new XHtml().escape('') // ''
50
+ */
51
+ static escape(html: string): string;
52
+ /**
53
+ * 将 HTML 实体还原为原始字符。
54
+ *
55
+ * 处理规则:
56
+ * - 还原 `&amp;`、`&lt;`、`&gt;`、`&quot;`、`&#39;`、`&apos;`;
57
+ * - 未匹配到上述实体的文本原样保留;
58
+ * - 空字符串返回空字符串。
59
+ *
60
+ * 边界与意外场景:
61
+ * - `&nbsp;`、`&copy;` 等其它命名实体不会被还原;
62
+ * - `&#65;`、`&#x41;` 等数字实体不会被还原;
63
+ * - 反转义是一次性替换而非循环展开,例如 `&amp;amp;` 还原后得到 `&amp;`
64
+ * 而不会继续还原成 `&`。
65
+ *
66
+ * @param html 含 HTML 实体的字符串。
67
+ * @returns 还原后的字符串。
68
+ *
69
+ * @example
70
+ * new XHtml().unescape('&lt;a&amp;b&gt;') // '<a&b>'
71
+ * new XHtml().unescape('&apos;ok&apos;') // "'ok'"
72
+ * new XHtml().unescape('') // ''
73
+ */
74
+ static unescape(html: string): string;
75
+ /**
76
+ * 将 HTML 字符串解析为可查询的节点树。
77
+ *
78
+ * 基于 `node-html-parser` 实现,返回的根节点是一个“文档容器”:
79
+ * - 根节点自身不携带标签名(`tagName` / `rawTagName` 为 `null`),
80
+ * 真正解析出的元素位于 `childNodes` 中;
81
+ * - 同时支持完整文档与 HTML 片段(多个顶层节点);
82
+ * - 元素节点与文本节点会被保留,注释节点默认被丢弃;
83
+ * - 属性可通过 `getAttribute` / `rawAttributes` 读取,元素可通过
84
+ * `querySelector` / `querySelectorAll` / `getElementById` /
85
+ * `getElementsByTagName` 等方法查询。
86
+ *
87
+ * 边界与意外场景:
88
+ * - 空字符串解析为空树(`childNodes` 为空、`textContent` 为空字符串);
89
+ * - 查询不到匹配元素时,选择器方法返回 `null` / 空数组,而不会抛出异常;
90
+ * - 本方法不负责 HTML 清理与消毒,不可信输入需先经过 `escape` 或其它
91
+ * 消毒流程后再使用;
92
+ * - 底层解析失败时会直接抛出异常,本方法不做兜底转换。
93
+ *
94
+ * @param html 待解析的 HTML 字符串。
95
+ * @returns 解析得到的根节点(HTMLElement),其子节点为实际解析出的元素。
96
+ *
97
+ * @example
98
+ * const root = new XHtml().parse('<div class="box">hi<span>!</span></div>');
99
+ * root.querySelector('.box')?.textContent // 'hi!'
100
+ * root.querySelectorAll('span').length // 1
101
+ */
102
+ static parse(html: string): HTMLElement;
103
+ }
package/lib/html.mjs ADDED
@@ -0,0 +1,112 @@
1
+ import { parse } from "node-html-parser";
2
+ import { escapeHTML, unescapeHTML } from "fast-escape-html";
3
+ // src/html.ts
4
+ /**
5
+ * HTML 转义与反转义工具。
6
+ *
7
+ * 基于 `fast-escape-html` 实现,用于在把不可信文本嵌入 HTML 前进行转义,
8
+ * 或将已转义的 HTML 实体还原为原始字符,避免标签错乱与 XSS 注入。
9
+ *
10
+ * 转义字符对照:
11
+ * | 原字符 | 转义结果 |
12
+ * | ------ | -------- |
13
+ * | `&` | `&amp;` |
14
+ * | `<` | `&lt;` |
15
+ * | `>` | `&gt;` |
16
+ * | `"` | `&quot;` |
17
+ * | `'` | `&#39;` |
18
+ *
19
+ * @example
20
+ * const html = new XHtml();
21
+ * html.escape('<a href="x">\'</a>') // '&lt;a href=&quot;x&quot;&gt;&#39;&lt;/a&gt;'
22
+ * html.unescape('&lt;a&amp;b&gt;') // '<a&b>'
23
+ *
24
+ * @remarks
25
+ * - `escape` 是无状态转义:已含实体的文本会再次被转义(如 `&lt;` →
26
+ * `&amp;lt;`),因此不要对同一段文本重复调用;
27
+ * - `unescape` 只还原本工具转义出的实体(`&amp;`、`&lt;`、`&gt;`、
28
+ * `&quot;`、`&#39;`、`&apos;`),不处理 `&nbsp;`、`&copy;` 等其它命名
29
+ * 实体,也不处理 `&#65;`、`&#x41;` 这类十进制/十六进制数字实体;
30
+ * - 本工具面向 HTML 文本节点/属性值的转义场景,不负责 URL 编码,也不保证
31
+ * 脚本、样式等非 HTML 上下文的安全。
32
+ */
33
+ var XHtml = class {
34
+ /**
35
+ * 将字符串转义为可安全嵌入 HTML 的实体文本。
36
+ *
37
+ * 处理规则:
38
+ * - `&`、`<`、`>`、`"`、`'` 会被替换为对应实体;
39
+ * - 其余字符(中文、字母、数字、换行、空白等)原样保留;
40
+ * - 空字符串返回空字符串。
41
+ *
42
+ * 边界与意外场景:
43
+ * - 已转义文本会被二次转义(如 `&lt;` → `&amp;lt;`),调用方需自行保证
44
+ * 同一段文本只转义一次。
45
+ *
46
+ * @param html 待转义的原始字符串。
47
+ * @returns 转义后的字符串。
48
+ *
49
+ * @example
50
+ * new XHtml().escape('<a href="x">') // '&lt;a href=&quot;x&quot;&gt;'
51
+ * new XHtml().escape('') // ''
52
+ */
53
+ static escape(html) {
54
+ return escapeHTML(html);
55
+ }
56
+ /**
57
+ * 将 HTML 实体还原为原始字符。
58
+ *
59
+ * 处理规则:
60
+ * - 还原 `&amp;`、`&lt;`、`&gt;`、`&quot;`、`&#39;`、`&apos;`;
61
+ * - 未匹配到上述实体的文本原样保留;
62
+ * - 空字符串返回空字符串。
63
+ *
64
+ * 边界与意外场景:
65
+ * - `&nbsp;`、`&copy;` 等其它命名实体不会被还原;
66
+ * - `&#65;`、`&#x41;` 等数字实体不会被还原;
67
+ * - 反转义是一次性替换而非循环展开,例如 `&amp;amp;` 还原后得到 `&amp;`
68
+ * 而不会继续还原成 `&`。
69
+ *
70
+ * @param html 含 HTML 实体的字符串。
71
+ * @returns 还原后的字符串。
72
+ *
73
+ * @example
74
+ * new XHtml().unescape('&lt;a&amp;b&gt;') // '<a&b>'
75
+ * new XHtml().unescape('&apos;ok&apos;') // "'ok'"
76
+ * new XHtml().unescape('') // ''
77
+ */
78
+ static unescape(html) {
79
+ return unescapeHTML(html);
80
+ }
81
+ /**
82
+ * 将 HTML 字符串解析为可查询的节点树。
83
+ *
84
+ * 基于 `node-html-parser` 实现,返回的根节点是一个“文档容器”:
85
+ * - 根节点自身不携带标签名(`tagName` / `rawTagName` 为 `null`),
86
+ * 真正解析出的元素位于 `childNodes` 中;
87
+ * - 同时支持完整文档与 HTML 片段(多个顶层节点);
88
+ * - 元素节点与文本节点会被保留,注释节点默认被丢弃;
89
+ * - 属性可通过 `getAttribute` / `rawAttributes` 读取,元素可通过
90
+ * `querySelector` / `querySelectorAll` / `getElementById` /
91
+ * `getElementsByTagName` 等方法查询。
92
+ *
93
+ * 边界与意外场景:
94
+ * - 空字符串解析为空树(`childNodes` 为空、`textContent` 为空字符串);
95
+ * - 查询不到匹配元素时,选择器方法返回 `null` / 空数组,而不会抛出异常;
96
+ * - 本方法不负责 HTML 清理与消毒,不可信输入需先经过 `escape` 或其它
97
+ * 消毒流程后再使用;
98
+ * - 底层解析失败时会直接抛出异常,本方法不做兜底转换。
99
+ *
100
+ * @param html 待解析的 HTML 字符串。
101
+ * @returns 解析得到的根节点(HTMLElement),其子节点为实际解析出的元素。
102
+ *
103
+ * @example
104
+ * const root = new XHtml().parse('<div class="box">hi<span>!</span></div>');
105
+ * root.querySelector('.box')?.textContent // 'hi!'
106
+ * root.querySelectorAll('span').length // 1
107
+ */
108
+ static parse(html) {
109
+ return parse(html);
110
+ }
111
+ };
112
+ export { XHtml };
package/lib/jwt.d.mts ADDED
@@ -0,0 +1,128 @@
1
+ import { MsValue } from "vtils";
2
+ import jwt from "jsonwebtoken";
3
+ import { LRUCache } from "lru-cache";
4
+ import { RequiredBy } from "vtils/types";
5
+ import { IncomingHttpHeaders } from "node:http";
6
+ /**
7
+ * JWT 载荷。
8
+ *
9
+ * 即签入 token 的自定义数据;`jsonwebtoken` 签发时会额外写入
10
+ * `iat`(签发时间,Unix 秒)与 `exp`(过期时间,Unix 秒)两个标准字段。
11
+ *
12
+ * 该接口只是宽松约束,实际载荷可包含任意字段,
13
+ * 调用方通过 `sign` / `verify` 的泛型参数指定具体形状。
14
+ */
15
+ export interface XJwtPayload extends Record<string, any> {}
16
+ /**
17
+ * `XJwt` 的构造选项。
18
+ */
19
+ export interface XJwtOptions {
20
+ /**
21
+ * 签名密钥。
22
+ *
23
+ * 用于签发与校验 token,必须保密;一旦泄露,
24
+ * 任何人都可以伪造出能被校验通过的 token。
25
+ */
26
+ secret: string;
27
+ /**
28
+ * 默认有效期。
29
+ *
30
+ * 支持字符串(如 `'1s'`、`'30m'`、`'7d'`)或数字(毫秒)。
31
+ * 当 `sign` 未显式传入 `ttl` 时使用该值。
32
+ *
33
+ * 说明:token 的 `exp` 字段精度为秒(`jsonwebtoken` 的限制),
34
+ * 换算时不足 1 秒的部分会四舍五入;缓存的有效期则使用毫秒精度。
35
+ */
36
+ ttl: MsValue;
37
+ /**
38
+ * 可选的内存缓存配置。
39
+ *
40
+ * 传入后启用“先查缓存、命中即返回”的快速校验路径,
41
+ * 可避免对同一 token 重复执行 `jsonwebtoken` 验签。
42
+ */
43
+ cache?: LRUCache.Options<string, XJwtPayload, any>;
44
+ /**
45
+ * 校验通过后的钩子。
46
+ *
47
+ * 在 token 校验成功(或命中缓存)后、`verify` 返回前调用,
48
+ * 可用于记录日志、更新活跃状态等;若钩子抛出异常,
49
+ * `verify` 会以该异常拒绝。
50
+ */
51
+ onVerify?: (payload: XJwtPayload & RequiredBy<jwt.JwtPayload, 'iat' | 'exp'> & {
52
+ token: string;
53
+ }) => any;
54
+ }
55
+ /**
56
+ * `XJwt.verify` 的返回结果。
57
+ *
58
+ * 在自定义载荷的基础上,合并 `jsonwebtoken` 校验后的标准字段
59
+ * (`iat` / `exp` 等)以及本次校验所用的 `token` 字段。
60
+ */
61
+ export type XJwtVerifyResult<T extends XJwtPayload> = T & RequiredBy<jwt.JwtPayload, 'iat' | 'exp'> & {
62
+ token: string;
63
+ };
64
+ /**
65
+ * JWT 签发与校验工具。
66
+ *
67
+ * 封装 `jsonwebtoken`,提供统一的签发(`sign`)与校验(`verify`),
68
+ * 并额外支持:
69
+ * - 可选的内存缓存(`LRUCache`),命中缓存时跳过重复验签;
70
+ * - 从 HTTP 或 WebSocket 升级请求头中提取 token(`verifyFromHttpHeaders`)。
71
+ *
72
+ * @example
73
+ * const xJwt = new XJwt({ secret: 'xx', ttl: '1h' });
74
+ *
75
+ * const token = xJwt.sign({ userId: 1 });
76
+ * // 'header.payload.signature'
77
+ *
78
+ * await xJwt.verify(token);
79
+ * // { userId: 1, iat: 1730000000, exp: 1730003600, token: 'header.payload.signature' }
80
+ */
81
+ export declare class XJwt {
82
+ private options;
83
+ private cache;
84
+ constructor(options: XJwtOptions);
85
+ /**
86
+ * 签发 JWT。
87
+ *
88
+ * 使用 `secret` 对 `payload` 签名,并写入 `iat` 与 `exp`。
89
+ * 若启用了缓存,会将原始 `payload` 以 token 为 key 写入缓存,
90
+ * 有效期与 token 一致,供 `verify` 快速命中。
91
+ *
92
+ * @param payload 要签入 token 的自定义载荷。
93
+ * @param ttl 有效期,默认使用构造时传入的 `options.ttl`;
94
+ * 支持字符串(如 `'1s'`)或数字(毫秒)。
95
+ * @returns 签名后的 JWT 字符串。
96
+ */
97
+ sign<T extends XJwtPayload>(payload: T, ttl?: MsValue): string;
98
+ /**
99
+ * 校验 JWT。
100
+ *
101
+ * 主要分支:
102
+ * - 启用了缓存且 token 命中缓存:直接返回缓存中的载荷(补上 `token` 字段),
103
+ * 不再执行验签;
104
+ * - 否则调用 `jsonwebtoken.verify` 验签,要求载荷中存在 `iat` 与 `exp`
105
+ * 且未过期;验签通过后写入缓存(有效期 = token 剩余时长)并返回。
106
+ *
107
+ * 校验通过后,会先执行 `options.onVerify`(若已配置),再返回结果。
108
+ *
109
+ * @param token 待校验的 JWT 字符串。
110
+ * @returns 载荷与 `token` 字段合并后的对象(含 `iat` / `exp` 等标准字段)。
111
+ * @throws 当 token 无效、格式错误、已过期,或 `onVerify` 钩子抛出异常时拒绝。
112
+ */
113
+ verify<T extends XJwtPayload>(token: string): Promise<XJwtVerifyResult<T>>;
114
+ /**
115
+ * 从请求头中提取 token 并校验。
116
+ *
117
+ * 依次尝试以下来源:
118
+ * 1. `authorization` 头:支持 `Bearer <token>` 形式,也兼容不带 scheme 的
119
+ * 原始 token(按空格拆分后,若只有一段则直接作为 token);
120
+ * 2. `sec-websocket-protocol` 头(WebSocket 升级请求):其值为原始 token,
121
+ * 会被包装成 `Bearer <token>` 后按相同逻辑解析。
122
+ *
123
+ * @param headers Node HTTP 请求头对象。
124
+ * @returns `verify` 的校验结果。
125
+ * @throws 当两个头都缺失或为空时抛出异常。
126
+ */
127
+ verifyFromHttpHeaders<T extends XJwtPayload>(headers: IncomingHttpHeaders): Promise<XJwtVerifyResult<T>>;
128
+ }
package/lib/jwt.mjs ADDED
@@ -0,0 +1,104 @@
1
+ import { ms } from "vtils";
2
+ import { differenceInMilliseconds, fromUnixTime } from "vtils/date";
3
+ import jwt from "jsonwebtoken";
4
+ import { LRUCache } from "lru-cache";
5
+ // src/jwt.ts
6
+ /**
7
+ * JWT 签发与校验工具。
8
+ *
9
+ * 封装 `jsonwebtoken`,提供统一的签发(`sign`)与校验(`verify`),
10
+ * 并额外支持:
11
+ * - 可选的内存缓存(`LRUCache`),命中缓存时跳过重复验签;
12
+ * - 从 HTTP 或 WebSocket 升级请求头中提取 token(`verifyFromHttpHeaders`)。
13
+ *
14
+ * @example
15
+ * const xJwt = new XJwt({ secret: 'xx', ttl: '1h' });
16
+ *
17
+ * const token = xJwt.sign({ userId: 1 });
18
+ * // 'header.payload.signature'
19
+ *
20
+ * await xJwt.verify(token);
21
+ * // { userId: 1, iat: 1730000000, exp: 1730003600, token: 'header.payload.signature' }
22
+ */
23
+ var XJwt = class {
24
+ options;
25
+ cache;
26
+ constructor(options) {
27
+ this.options = options;
28
+ if (options.cache) this.cache = new LRUCache(options.cache);
29
+ }
30
+ /**
31
+ * 签发 JWT。
32
+ *
33
+ * 使用 `secret` 对 `payload` 签名,并写入 `iat` 与 `exp`。
34
+ * 若启用了缓存,会将原始 `payload` 以 token 为 key 写入缓存,
35
+ * 有效期与 token 一致,供 `verify` 快速命中。
36
+ *
37
+ * @param payload 要签入 token 的自定义载荷。
38
+ * @param ttl 有效期,默认使用构造时传入的 `options.ttl`;
39
+ * 支持字符串(如 `'1s'`)或数字(毫秒)。
40
+ * @returns 签名后的 JWT 字符串。
41
+ */
42
+ sign(payload, ttl = this.options.ttl) {
43
+ const token = jwt.sign(payload, this.options.secret, { expiresIn: ms(ttl, true) });
44
+ if (this.cache) this.cache.set(token, payload, { ttl: ms(ttl) });
45
+ return token;
46
+ }
47
+ /**
48
+ * 校验 JWT。
49
+ *
50
+ * 主要分支:
51
+ * - 启用了缓存且 token 命中缓存:直接返回缓存中的载荷(补上 `token` 字段),
52
+ * 不再执行验签;
53
+ * - 否则调用 `jsonwebtoken.verify` 验签,要求载荷中存在 `iat` 与 `exp`
54
+ * 且未过期;验签通过后写入缓存(有效期 = token 剩余时长)并返回。
55
+ *
56
+ * 校验通过后,会先执行 `options.onVerify`(若已配置),再返回结果。
57
+ *
58
+ * @param token 待校验的 JWT 字符串。
59
+ * @returns 载荷与 `token` 字段合并后的对象(含 `iat` / `exp` 等标准字段)。
60
+ * @throws 当 token 无效、格式错误、已过期,或 `onVerify` 钩子抛出异常时拒绝。
61
+ */
62
+ async verify(token) {
63
+ const data = await new Promise((resolve, reject) => {
64
+ if (this.cache && this.cache.has(token)) {
65
+ resolve({
66
+ ...this.cache.get(token),
67
+ token
68
+ });
69
+ return;
70
+ }
71
+ jwt.verify(token, this.options.secret, { ignoreExpiration: false }, (err, decoded) => {
72
+ if (err || !decoded || typeof decoded === "string" || !decoded.iat || !decoded.exp) {
73
+ reject(/* @__PURE__ */ new Error("JWT 校验失败:token 无效、格式错误或已过期"));
74
+ return;
75
+ }
76
+ decoded.token = token;
77
+ if (this.cache) this.cache.set(token, decoded, { ttl: differenceInMilliseconds(fromUnixTime(decoded.exp), /* @__PURE__ */ new Date()) });
78
+ resolve(decoded);
79
+ });
80
+ });
81
+ await this.options.onVerify?.(data);
82
+ return data;
83
+ }
84
+ /**
85
+ * 从请求头中提取 token 并校验。
86
+ *
87
+ * 依次尝试以下来源:
88
+ * 1. `authorization` 头:支持 `Bearer <token>` 形式,也兼容不带 scheme 的
89
+ * 原始 token(按空格拆分后,若只有一段则直接作为 token);
90
+ * 2. `sec-websocket-protocol` 头(WebSocket 升级请求):其值为原始 token,
91
+ * 会被包装成 `Bearer <token>` 后按相同逻辑解析。
92
+ *
93
+ * @param headers Node HTTP 请求头对象。
94
+ * @returns `verify` 的校验结果。
95
+ * @throws 当两个头都缺失或为空时抛出异常。
96
+ */
97
+ async verifyFromHttpHeaders(headers) {
98
+ const authorization = headers.authorization || headers["sec-websocket-protocol"] && `Bearer ${headers["sec-websocket-protocol"]}` || "";
99
+ if (!authorization) throw new Error("缺少认证信息:请提供 Authorization 或 Sec-WebSocket-Protocol 请求头");
100
+ const [scheme, token = scheme] = authorization.split(" ");
101
+ return this.verify(token);
102
+ }
103
+ };
104
+ export { XJwt };