@yongdall/user-cookie 0.1.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.
@@ -0,0 +1,118 @@
1
+ import { existUser, useTenant } from "@yongdall/core";
2
+ import { k99Context } from "@yongdall/http";
3
+
4
+ //#region plugins/user-cookie/hooks.yongdall.mjs
5
+ /** @import { Hooks } from '@yongdall/core' */
6
+ const userLoggedCookie = "user-logged-token";
7
+ const cookieTime = 1440 * 60 * 1e3;
8
+ /**
9
+ * 使用 HMAC-SHA256 对一个或多个值进行哈希,使用 salt 作为密钥。
10
+ * 输出为 base64url 格式。
11
+ *
12
+ * @param {string | ArrayBuffer | ArrayBufferView<ArrayBuffer>} salt
13
+ * @param {string | number} value
14
+ * @param {...(string | number)} values
15
+ * @returns {Promise<string>}
16
+ */
17
+ async function hash(salt, value, ...values) {
18
+ /** @type {ArrayBuffer | ArrayBufferView<ArrayBuffer>} */
19
+ let keyBuffer;
20
+ if (salt instanceof ArrayBuffer || ArrayBuffer.isView(salt)) keyBuffer = salt;
21
+ else if (typeof salt === "string") keyBuffer = new TextEncoder().encode(salt);
22
+ else throw new Error("`salt` 参数必须为字符串或 Uint8Array");
23
+ const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, {
24
+ name: "HMAC",
25
+ hash: "SHA-256"
26
+ }, false, ["sign"]);
27
+ /** @type {string[]} */
28
+ let data = [];
29
+ if (typeof value === "number") data.push(String(value));
30
+ else if (typeof value === "string") data.push(JSON.stringify(value).slice(1, -1));
31
+ else throw new Error("`value` 参数必须为字符串或数字");
32
+ for (const v of values) if (typeof v === "number") data.push(String(v));
33
+ else if (typeof v === "string") data.push(JSON.stringify(v).slice(1, -1));
34
+ const messageBuffer = new TextEncoder().encode(data.join("\n"));
35
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageBuffer);
36
+ return btoa(String.fromCharCode(...new Uint8Array(signature))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
37
+ }
38
+ /**
39
+ *
40
+ * @param {string} cookie
41
+ * @returns {Promise<[string, boolean]?>}
42
+ */
43
+ async function parseUserCookie(cookie) {
44
+ if (typeof cookie !== "string") return null;
45
+ const list = cookie.split(".");
46
+ if (list.length !== 3) return null;
47
+ const timestamp = Number(list[0]);
48
+ if (!Number.isSafeInteger(timestamp)) return null;
49
+ const time = Date.now() - timestamp;
50
+ if (time > cookieTime) return null;
51
+ if (await hash((await useTenant()).salt, list[1], list[0]) !== list[2]) return null;
52
+ return [list[1], time > cookieTime * 2 / 3];
53
+ }
54
+ /**
55
+ *
56
+ * @param {string} user
57
+ * @returns {Promise<string>}
58
+ */
59
+ async function toUserCookie(user) {
60
+ const timestamp = Date.now();
61
+ return [
62
+ timestamp,
63
+ user,
64
+ await hash((await useTenant()).salt, user, timestamp)
65
+ ].join(".");
66
+ }
67
+ /** @type {Hooks.Define['userManager']} */
68
+ const userManager = {
69
+ async get() {
70
+ const ctx = k99Context();
71
+ if (!ctx) return null;
72
+ const cookie = ctx.cookies[userLoggedCookie];
73
+ if (!cookie) return null;
74
+ const userInfo = await parseUserCookie(cookie);
75
+ if (!userInfo) {
76
+ ctx.clearCookie(userLoggedCookie, {
77
+ httpOnly: true,
78
+ path: "/"
79
+ });
80
+ return null;
81
+ }
82
+ const [userId, reset] = userInfo;
83
+ if (!existUser(userId)) return null;
84
+ if (reset) ctx.setCookie(userLoggedCookie, await toUserCookie(userId), {
85
+ httpOnly: true,
86
+ path: "/"
87
+ });
88
+ return userId;
89
+ },
90
+ async set(userId) {
91
+ const ctx = k99Context();
92
+ if (!ctx) return null;
93
+ ctx.setCookie(userLoggedCookie, await toUserCookie(userId), {
94
+ httpOnly: true,
95
+ path: "/"
96
+ });
97
+ },
98
+ async login(userId) {
99
+ const ctx = k99Context();
100
+ if (!ctx) return null;
101
+ ctx.setCookie(userLoggedCookie, await toUserCookie(userId), {
102
+ httpOnly: true,
103
+ path: "/"
104
+ });
105
+ },
106
+ exit() {
107
+ const ctx = k99Context();
108
+ if (!ctx) return null;
109
+ ctx.clearCookie(userLoggedCookie, {
110
+ httpOnly: true,
111
+ path: "/"
112
+ });
113
+ }
114
+ };
115
+
116
+ //#endregion
117
+ export { userManager };
118
+ //# sourceMappingURL=hooks.yongdall.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.yongdall.mjs","names":[],"sources":["../../plugins/user-cookie/hooks.yongdall.mjs"],"sourcesContent":["/** @import { Hooks } from '@yongdall/core' */\nimport { existUser } from '@yongdall/core';\nimport { useTenant } from '@yongdall/core';\n\nimport { k99Context } from '@yongdall/http';\n\nconst userLoggedCookie = 'user-logged-token';\nconst cookieTime = 24 * 60 * 60 * 1000;\n\n\n\n/**\n * 使用 HMAC-SHA256 对一个或多个值进行哈希,使用 salt 作为密钥。\n * 输出为 base64url 格式。\n *\n * @param {string | ArrayBuffer | ArrayBufferView<ArrayBuffer>} salt\n * @param {string | number} value\n * @param {...(string | number)} values\n * @returns {Promise<string>}\n */\nasync function hash(salt, value, ...values) {\n\t// 1. 准备密钥(salt)\n\t/** @type {ArrayBuffer | ArrayBufferView<ArrayBuffer>} */\n\tlet keyBuffer;\n\tif (salt instanceof ArrayBuffer || ArrayBuffer.isView(salt)) {\n\t\tkeyBuffer = salt;\n\t} else if (typeof salt === 'string') {\n\t\tkeyBuffer = new TextEncoder().encode(salt);\n\t} else {\n\t\tthrow new Error('`salt` 参数必须为字符串或 Uint8Array');\n\t}\n\n\t// 2. 导入 HMAC 密钥\n\tconst cryptoKey = await crypto.subtle.importKey(\n\t\t'raw',\n\t\tkeyBuffer,\n\t\t{ name: 'HMAC', hash: 'SHA-256' },\n\t\tfalse,\n\t\t['sign']\n\t);\n\n\t// 3. 构造输入数据\n\t/** @type {string[]} */\n\tlet data = [];\n\n\t// 处理第一个 value\n\tif (typeof value === 'number') {\n\t\tdata.push(String(value));\n\t} else if (typeof value === 'string') {\n\t\t// JSON.stringify(value).slice(1, -1) 相当于去掉引号,等价于直接编码字符串内容\n\t\tdata.push(JSON.stringify(value).slice(1, -1));\n\t} else {\n\t\tthrow new Error('`value` 参数必须为字符串或数字');\n\t}\n\n\t// 处理后续 values\n\tfor (const v of values) {\n\t\tif (typeof v === 'number') {\n\t\t\tdata.push(String(v));\n\t\t} else if (typeof v === 'string') {\n\t\t\tdata.push(JSON.stringify(v).slice(1, -1));\n\t\t}\n\t}\n\n\t// 合并所有数据\n\tconst messageBuffer = new TextEncoder().encode(data.join('\\n'));\n\n\t// 4. 生成 HMAC\n\tconst signature = await crypto.subtle.sign('HMAC', cryptoKey, messageBuffer);\n\n\t// 5. 转为 base64url 格式\n\tconst base64 = btoa(String.fromCharCode(...new Uint8Array(signature)));\n\treturn base64\n\t\t.replace(/\\+/g, '-')\n\t\t.replace(/\\//g, '_')\n\t\t.replace(/=+$/, ''); // 移除填充\n}\n/**\n * \n * @param {string} cookie \n * @returns {Promise<[string, boolean]?>}\n */\nasync function parseUserCookie(cookie) {\n\tif (typeof cookie !== 'string') { return null; }\n\tconst list = cookie.split('.');\n\tif (list.length !== 3) { return null; }\n\tconst timestamp = Number(list[0]);\n\tif (!Number.isSafeInteger(timestamp)) { return null; }\n\tconst time = Date.now() - timestamp;\n\tif (time > cookieTime) { return null; }\n\tconst tenant = await useTenant();\n\tif (await hash(tenant.salt, list[1], list[0]) !== list[2]) { return null; }\n\treturn [list[1], time > cookieTime * 2 / 3];\n}\n\n/**\n * \n * @param {string} user \n * @returns {Promise<string>}\n */\nasync function toUserCookie(user) {\n\tconst timestamp = Date.now();\n\tconst tenant = await useTenant();\n\treturn [timestamp, user, await hash(tenant.salt, user, timestamp)].join('.');\n}\n/** @type {Hooks.Define['userManager']} */\nexport const userManager = {\n\tasync get() {\n\t\tconst ctx = k99Context();\n\t\tif (!ctx) { return null; }\n\t\tconst cookie = ctx.cookies[userLoggedCookie];\n\t\tif (!cookie) { return null; }\n\t\tconst userInfo = await parseUserCookie(cookie);\n\t\tif (!userInfo) { ctx.clearCookie(userLoggedCookie, { httpOnly: true, path: '/' }); return null; }\n\t\tconst [userId, reset] = userInfo;\n\t\tif (!existUser(userId)) { return null; }\n\t\tif (reset) { ctx.setCookie(userLoggedCookie, await toUserCookie(userId), { httpOnly: true, path: '/' }); }\n\t\treturn userId;\n\t},\n\t/**\n\t * \n\t * @param {string} userId \n\t * @returns \n\t */\n\tasync set(userId) {\n\t\tconst ctx = k99Context();\n\t\tif (!ctx) { return null; }\n\t\tctx.setCookie(userLoggedCookie, await toUserCookie(userId), { httpOnly: true, path: '/' });\n\t},\n\t/**\n\t * \n\t * @param {string} userId \n\t * @returns \n\t */\n\tasync login(userId) {\n\t\tconst ctx = k99Context();\n\t\tif (!ctx) { return null; }\n\t\tctx.setCookie(userLoggedCookie, await toUserCookie(userId), { httpOnly: true, path: '/' });\n\t},\n\texit() {\n\t\tconst ctx = k99Context();\n\t\tif (!ctx) { return null; }\n\t\tctx.clearCookie(userLoggedCookie, { httpOnly: true, path: '/' });\n\t},\n};\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@yongdall/user-cookie",
3
+ "version": "0.1.0",
4
+ "dependencies": {
5
+ "@yongdall/http": "^0.1.0",
6
+ "@yongdall/core": "^0.1.0"
7
+ }
8
+ }