@blocklet/sdk 1.16.51-beta-20250904-031834-c611f059 → 1.16.51-beta-20250905-051437-fe05adb2

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.
@@ -8,7 +8,15 @@ export interface CSRFOptions {
8
8
  generateToken?: (req: Request, res: CSRFOptionsResponse) => void | Promise<void>;
9
9
  verifyToken?: (req: Request, res: CSRFOptionsResponse) => Promise<void> | void;
10
10
  }
11
- declare function defaultGenerateToken(req: Request, res: Response): Promise<void>;
11
+ /**
12
+ *
13
+ * @param req
14
+ * @param res
15
+ * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-hmac-csrf-tokens
16
+ * @note: 我们需要意识到 1. csrf token 不会被攻击者从 cookie 中得到 2. csrf token 的校验是需要当前用户的 login token 的,3. csrf token 不应该有过期时间
17
+ * @returns
18
+ */
19
+ declare function defaultGenerateToken(req: Request, res: Response): void;
12
20
  declare function defaultVerifyToken(req: Request): void;
13
21
  export declare function csrf(options?: CSRFOptions): RequestHandler;
14
22
  export {};
@@ -14,28 +14,39 @@ const wallet = (0, wallet_2.default)();
14
14
  function printCookieParserNotInstalledWarning() {
15
15
  config_1.default.logger.warn('cookie-parser middleware is required for the csrf middleware to work properly.');
16
16
  }
17
- async function defaultGenerateToken(req, res) {
17
+ /**
18
+ *
19
+ * @param req
20
+ * @param res
21
+ * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-hmac-csrf-tokens
22
+ * @note: 我们需要意识到 1. csrf token 不会被攻击者从 cookie 中得到 2. csrf token 的校验是需要当前用户的 login token 的,3. csrf token 不应该有过期时间
23
+ * @returns
24
+ */
25
+ function defaultGenerateToken(req, res) {
18
26
  if (!req.cookies) {
19
27
  printCookieParserNotInstalledWarning();
20
28
  return;
21
29
  }
22
30
  if (req.cookies.login_token) {
23
- const csrfToken = await (0, csrf_1.signWithCache)(wallet.secretKey, req.cookies.login_token);
24
- res.cookie('x-csrf-token', csrfToken, {
25
- sameSite: 'none',
26
- secure: true,
27
- });
31
+ const newCsrfToken = (0, csrf_1.sign)(wallet.secretKey, req.cookies.login_token);
32
+ const oldCsrfToken = req.cookies['x-csrf-token'];
33
+ if (newCsrfToken !== oldCsrfToken) {
34
+ res.cookie('x-csrf-token', newCsrfToken, {
35
+ sameSite: 'strict',
36
+ secure: true,
37
+ });
38
+ }
28
39
  }
29
40
  }
30
41
  function defaultVerifyToken(req) {
31
42
  if (!req.cookies) {
32
43
  printCookieParserNotInstalledWarning();
33
44
  }
34
- if (req.cookies &&
45
+ if (req.cookies?.login_token &&
35
46
  !(0, isEmpty_1.default)(req.cookies['x-csrf-token']) &&
36
47
  req.cookies['x-csrf-token'] === req.headers['x-csrf-token']) {
37
- const csrfToken = req.cookies['x-csrf-token'];
38
- if ((0, csrf_1.verify)(wallet.secretKey, csrfToken)) {
48
+ const csrfTokenFromRequest = req.cookies['x-csrf-token'];
49
+ if ((0, csrf_1.verify)(wallet.secretKey, csrfTokenFromRequest, req.cookies.login_token)) {
39
50
  return;
40
51
  }
41
52
  config_1.default.logger.warn('Invalid request: csrf token mismatch', {
@@ -49,7 +60,7 @@ function defaultVerifyToken(req) {
49
60
  csrfTokenFromHeader: req.headers['x-csrf-token'],
50
61
  });
51
62
  }
52
- throw new Error('Invalid request: csrf token mismatch');
63
+ throw new Error('Invalid request: csrf token mismatch, please refresh the page try again');
53
64
  }
54
65
  function shouldGenerateToken(req) {
55
66
  return ['GET'].includes(req.method);
@@ -77,9 +77,9 @@ interface BlockletService {
77
77
  verifyAccessKey(params: OmitTeamDid<Client.RequestVerifyAccessKeyInput>): Promise<Client.ResponseAccessKey>;
78
78
  getAccessKeys(params: OmitTeamDid<Client.RequestAccessKeysInput>): Promise<Client.ResponseAccessKeys>;
79
79
  getAccessKey(params: OmitTeamDid<Client.RequestAccessKeyInput>): Promise<Client.ResponseAccessKey>;
80
- getUserFollowers(args?: OmitTeamDid<Client.RequestUserFollowsInput>): Promise<Client.ResponseUserFollows>;
81
- getUserFollowing(args?: OmitTeamDid<Client.RequestUserFollowsInput>): Promise<Client.ResponseUserFollows>;
82
- getUserFollowStats(args?: OmitTeamDid<Client.RequestUserFollowsStatsInput>): Promise<Client.ResponseFollowStats>;
80
+ getUserFollowers(args: OmitTeamDid<Client.RequestUserFollowsInput>, options: RequestHeaders): Promise<Client.ResponseUserFollows>;
81
+ getUserFollowing(args: OmitTeamDid<Client.RequestUserFollowsInput>, options: RequestHeaders): Promise<Client.ResponseUserFollows>;
82
+ getUserFollowStats(args: OmitTeamDid<Client.RequestUserFollowsStatsInput>, options: RequestHeaders): Promise<Client.ResponseFollowStats>;
83
83
  checkFollowing(args: OmitTeamDid<Client.RequestCheckFollowingInput>): Promise<Client.ResponseCheckFollowing>;
84
84
  followUser(args: OmitTeamDid<Client.RequestFollowUserActionInput>): Promise<Client.GeneralResponse>;
85
85
  unfollowUser(args: OmitTeamDid<Client.RequestFollowUserActionInput>): Promise<Client.GeneralResponse>;
@@ -81,6 +81,9 @@ class BlockletClient extends server_js_1.default {
81
81
  return headers;
82
82
  }
83
83
  }
84
+ // 通用的客户端调用方法,用于减少代码重复
85
+ // 需要 cookie 验证的方法列表
86
+ const AUTH_REQUIRED_METHODS = Object.freeze(['getUserFollowers', 'getUserFollowing', 'getUserFollowStats']);
84
87
  // Blocklet 相关的功能 API 都在这里
85
88
  // core/state/lib/api/team.js L42
86
89
  // 所有可配置调用的函数在这里,如果需要额外增加,则需要在这里新增对应的函数
@@ -302,20 +305,25 @@ class BlockletService {
302
305
  throw new Error((0, error_1.formatError)(err));
303
306
  }
304
307
  };
305
- // 通用的客户端调用方法,用于减少代码重复
306
- const callClientMethod = async (methodName, args) => {
308
+ const callClientMethod = async (methodName, args, options) => {
307
309
  try {
310
+ // 检查是否为需要认证的方法
311
+ if (AUTH_REQUIRED_METHODS.includes(methodName)) {
312
+ if (!options?.headers?.cookie) {
313
+ throw new Error('Missing required authentication cookie in request headers');
314
+ }
315
+ }
308
316
  const clientMethod = client[methodName];
309
- const res = await clientMethod({ input: { teamDid, ...args } });
317
+ const res = await clientMethod({ input: { teamDid, ...args } }, options);
310
318
  return res;
311
319
  }
312
320
  catch (err) {
313
321
  throw new Error((0, error_1.formatError)(err)); // 这几个错误消息需要 format
314
322
  }
315
323
  };
316
- this.getUserFollowers = (args) => callClientMethod('getUserFollowers', args);
317
- this.getUserFollowing = (args) => callClientMethod('getUserFollowing', args);
318
- this.getUserFollowStats = (args) => callClientMethod('getUserFollowStats', args);
324
+ this.getUserFollowers = (args, options) => callClientMethod('getUserFollowers', args, options);
325
+ this.getUserFollowing = (args, options) => callClientMethod('getUserFollowing', args, options);
326
+ this.getUserFollowStats = (args, options) => callClientMethod('getUserFollowStats', args, options);
319
327
  this.checkFollowing = (args) => callClientMethod('checkFollowing', args);
320
328
  this.followUser = (args) => callClientMethod('followUser', args);
321
329
  this.unfollowUser = (args) => callClientMethod('unfollowUser', args);
@@ -1,3 +1,5 @@
1
+ import type { LiteralUnion } from 'type-fest';
2
+ export declare function hmac(secretKey: string, message: string, algorithm?: LiteralUnion<'md5' | 'sha256', string>): string;
1
3
  /**
2
4
  * 生成 CSRF Token
3
5
  * @param secretKey 服务器密钥(必填,需保密)
@@ -5,25 +7,11 @@
5
7
  * @returns 签名后的Token字符串
6
8
  */
7
9
  export declare function sign(secretKey: string, loginToken: string): string;
8
- /**
9
- * 带缓存的 CSRF Token 生成函数
10
- * 半小时内,相同的 secretKey 和 loginToken 会返回缓存的结果
11
- * @param secretKey 服务器密钥(必填,需保密)
12
- * @param loginToken 登录token
13
- * @returns 签名后的Token字符串
14
- */
15
- export declare function signWithCache(secretKey: string, loginToken: string): Promise<string>;
16
- /**
17
- * 清除缓存(主要用于测试)
18
- * @param secretKey 可选,指定清除特定密钥的缓存
19
- * @param loginToken 可选,指定清除特定登录token的缓存
20
- */
21
- export declare function clearSignCache(secretKey?: string, loginToken?: string): Promise<void>;
22
10
  /**
23
11
  * 验证CSRF Token有效性
24
- * @param secret 服务器密钥(需与生成时一致)
25
- * @param token 待验证的Token
26
- * @param expiresIn 有效期(需与生成时一致,默认3600
12
+ * @param secretKey 服务器密钥(需与生成时一致)
13
+ * @param csrfTokenFromRequest 待验证的Token
14
+ * @param expiresIn 有效期(需与生成时一致,默认5400
27
15
  * @returns 是否有效(布尔值)
28
16
  */
29
- export declare function verify(secret: string, token: string, expiresIn?: number): boolean;
17
+ export declare function verify(secretKey: string, csrfTokenFromRequest: string, loginToken: string): boolean;
package/lib/util/csrf.js CHANGED
@@ -1,32 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hmac = hmac;
3
4
  exports.sign = sign;
4
- exports.signWithCache = signWithCache;
5
- exports.clearSignCache = clearSignCache;
6
5
  exports.verify = verify;
7
6
  const crypto_1 = require("crypto");
8
- const lru_cache_1 = require("lru-cache");
9
- const config_1 = require("../config");
10
- // 常量定义:避免魔法数字,增强可读性
11
- const TIMESTAMP_LENGTH = 10; // 秒级时间戳长度(10位)
12
- const RANDOM_LENGTH = 4; // 随机数长度(4位)
13
- const PREFIX_LENGTH = TIMESTAMP_LENGTH + RANDOM_LENGTH; // 前缀总长度(14位)
14
- const MIN_TOKEN_LENGTH = PREFIX_LENGTH + 1 + 40; // 最小Token长度(14+1+40)
15
- const HASH_ALGORITHM = 'sha256'; // 哈希算法
16
- const CACHE_DURATION = 30 * 60 * 1000; // 缓存时长:30分钟(毫秒)
17
- // 创建缓存实例
18
- const cache = new lru_cache_1.LRUCache({ max: 100 * 1000, ttl: CACHE_DURATION });
19
- /**
20
- * Base64URL编码去除补位符
21
- */
22
- const trimBase64UrlPadding = (str) => str.replace(/=+$/, '');
23
- /**
24
- * Base64URL编码补全补位符
25
- */
26
- const padBase64Url = (str) => {
27
- const padLength = (4 - (str.length % 4)) % 4;
28
- return str.padEnd(str.length + padLength, '=');
29
- };
7
+ function hmac(secretKey, message, algorithm = 'md5') {
8
+ const hmacFunc = (0, crypto_1.createHmac)(algorithm, secretKey);
9
+ return hmacFunc.update(message).digest('base64url');
10
+ }
30
11
  /**
31
12
  * 生成 CSRF Token
32
13
  * @param secretKey 服务器密钥(必填,需保密)
@@ -34,116 +15,18 @@ const padBase64Url = (str) => {
34
15
  * @returns 签名后的Token字符串
35
16
  */
36
17
  function sign(secretKey, loginToken) {
37
- // 生成10位秒级时间戳
38
- const timestamp = Math.floor(Date.now() / 1000);
39
- const timestampStr = timestamp.toString().padStart(TIMESTAMP_LENGTH, '0');
40
- const salt = loginToken.slice(-4).padStart(4, '0');
41
- // 组合前缀:时间戳 + 随机数
42
- const prefix = `${timestampStr}${salt}`;
43
- // 生成HMAC签名并处理编码
44
- const signature = (0, crypto_1.createHmac)(HASH_ALGORITHM, secretKey).update(prefix).digest('base64url');
45
- // 拼接前缀和签名,使用.分隔(移除补位符减少长度)
46
- return `${prefix}.${trimBase64UrlPadding(signature)}`;
47
- }
48
- /**
49
- * 生成缓存键
50
- * @param secretKey 服务器密钥
51
- * @param loginToken 登录token
52
- * @returns 缓存键
53
- */
54
- function generateCacheKey(secretKey, loginToken) {
55
- // 使用哈希值作为键的一部分,避免敏感信息直接存储在键中
56
- const keyHash = (0, crypto_1.createHmac)('md5', 'cache-key-salt').update(`${secretKey}:${loginToken}`).digest('hex');
57
- return `csrf:${keyHash}`;
58
- }
59
- /**
60
- * 带缓存的 CSRF Token 生成函数
61
- * 半小时内,相同的 secretKey 和 loginToken 会返回缓存的结果
62
- * @param secretKey 服务器密钥(必填,需保密)
63
- * @param loginToken 登录token
64
- * @returns 签名后的Token字符串
65
- */
66
- async function signWithCache(secretKey, loginToken) {
67
- const cacheKey = generateCacheKey(secretKey, loginToken);
68
- // 尝试从缓存获取
69
- const cachedToken = await cache.get(cacheKey);
70
- if (cachedToken && typeof cachedToken === 'string') {
71
- return cachedToken;
72
- }
73
- // 缓存未命中,生成新的token
74
- const newToken = sign(secretKey, loginToken);
75
- // 存储到缓存
76
- await cache.set(cacheKey, newToken);
77
- return newToken;
78
- }
79
- /**
80
- * 清除缓存(主要用于测试)
81
- * @param secretKey 可选,指定清除特定密钥的缓存
82
- * @param loginToken 可选,指定清除特定登录token的缓存
83
- */
84
- async function clearSignCache(secretKey, loginToken) {
85
- if (secretKey && loginToken) {
86
- const cacheKey = generateCacheKey(secretKey, loginToken);
87
- await cache.delete(cacheKey);
88
- }
89
- else {
90
- await cache.clear();
91
- }
18
+ const xCsrfTokenMd5 = hmac(secretKey, loginToken);
19
+ const xCsrfTokenSigned = hmac(secretKey, xCsrfTokenMd5, 'sha256');
20
+ return [xCsrfTokenMd5, xCsrfTokenSigned].join('.');
92
21
  }
93
22
  /**
94
23
  * 验证CSRF Token有效性
95
- * @param secret 服务器密钥(需与生成时一致)
96
- * @param token 待验证的Token
97
- * @param expiresIn 有效期(需与生成时一致,默认3600
24
+ * @param secretKey 服务器密钥(需与生成时一致)
25
+ * @param csrfTokenFromRequest 待验证的Token
26
+ * @param expiresIn 有效期(需与生成时一致,默认5400
98
27
  * @returns 是否有效(布尔值)
99
28
  */
100
- function verify(secret, token, expiresIn = 3600) {
101
- // 基础格式校验
102
- if (typeof token !== 'string' || token.length < MIN_TOKEN_LENGTH) {
103
- config_1.logger.warn('CSRF token invalid: format error', {
104
- token: typeof token === 'string' ? `${token.slice(0, 20)}...` : token,
105
- });
106
- return false;
107
- }
108
- // 拆分前缀和签名部分(以.分隔)
109
- const dotIndex = token.indexOf('.', PREFIX_LENGTH);
110
- if (dotIndex === -1 || dotIndex !== PREFIX_LENGTH) {
111
- config_1.logger.warn('CSRF token invalid: separator error', { token: `${token.slice(0, 20)}...` });
112
- return false;
113
- }
114
- const prefix = token.slice(0, PREFIX_LENGTH);
115
- const signatureB64Url = token.slice(PREFIX_LENGTH + 1);
116
- // 提取时间戳部分(前10位)
117
- const timestampStr = prefix.slice(0, TIMESTAMP_LENGTH);
118
- // 时间戳格式校验
119
- const timestamp = parseInt(timestampStr, 10);
120
- if (Number.isNaN(timestamp)) {
121
- config_1.logger.warn('CSRF token invalid: timestamp error', { token: `${token.slice(0, 20)}...` });
122
- return false;
123
- }
124
- // 有效期校验
125
- const currentTime = Math.floor(Date.now() / 1000);
126
- if (currentTime - timestamp > expiresIn) {
127
- config_1.logger.warn('CSRF token invalid: expired', { token: `${token.slice(0, 20)}...` });
128
- return false;
129
- }
130
- // 签名校验
131
- try {
132
- // 补全Base64URL补位符并解码
133
- const paddedSignature = padBase64Url(signatureB64Url);
134
- const receivedSignature = Buffer.from(paddedSignature, 'base64url');
135
- // 重新计算预期签名(使用完整的前缀)
136
- const expectedSignature = (0, crypto_1.createHmac)(HASH_ALGORITHM, secret).update(prefix).digest();
137
- // 时序安全的对比(防止时序攻击)
138
- const isValid = (0, crypto_1.timingSafeEqual)(receivedSignature, expectedSignature);
139
- if (!isValid) {
140
- config_1.logger.warn('CSRF token invalid: signature error', { token: `${token.slice(0, 20)}...` });
141
- }
142
- return isValid;
143
- }
144
- catch (error) {
145
- // 任何解码或计算异常都视为无效
146
- config_1.logger.warn('CSRF token invalid: decode error', { token: `${token.slice(0, 20)}...` });
147
- return false;
148
- }
29
+ function verify(secretKey, csrfTokenFromRequest, loginToken) {
30
+ const expectCsrfToken = sign(secretKey, loginToken);
31
+ return expectCsrfToken === csrfTokenFromRequest;
149
32
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.51-beta-20250904-031834-c611f059",
6
+ "version": "1.16.51-beta-20250905-051437-fe05adb2",
7
7
  "description": "graphql client to read/write data on abt node",
8
8
  "main": "lib/index.js",
9
9
  "typings": "lib/index.d.ts",
@@ -27,19 +27,19 @@
27
27
  "author": "linchen1987 <linchen.1987@foxmail.com> (http://github.com/linchen1987)",
28
28
  "license": "Apache-2.0",
29
29
  "dependencies": {
30
- "@abtnode/constant": "1.16.51-beta-20250904-031834-c611f059",
31
- "@abtnode/db-cache": "1.16.51-beta-20250904-031834-c611f059",
32
- "@abtnode/util": "1.16.51-beta-20250904-031834-c611f059",
30
+ "@abtnode/constant": "1.16.51-beta-20250905-051437-fe05adb2",
31
+ "@abtnode/db-cache": "1.16.51-beta-20250905-051437-fe05adb2",
32
+ "@abtnode/util": "1.16.51-beta-20250905-051437-fe05adb2",
33
33
  "@arcblock/did": "1.24.0",
34
34
  "@arcblock/did-connect-js": "1.24.0",
35
35
  "@arcblock/jwt": "1.24.0",
36
36
  "@arcblock/ws": "1.24.0",
37
- "@blocklet/constant": "1.16.51-beta-20250904-031834-c611f059",
38
- "@blocklet/env": "1.16.51-beta-20250904-031834-c611f059",
37
+ "@blocklet/constant": "1.16.51-beta-20250905-051437-fe05adb2",
38
+ "@blocklet/env": "1.16.51-beta-20250905-051437-fe05adb2",
39
39
  "@blocklet/error": "^0.2.5",
40
- "@blocklet/meta": "1.16.51-beta-20250904-031834-c611f059",
41
- "@blocklet/server-js": "1.16.51-beta-20250904-031834-c611f059",
42
- "@blocklet/theme": "^3.1.33",
40
+ "@blocklet/meta": "1.16.51-beta-20250905-051437-fe05adb2",
41
+ "@blocklet/server-js": "1.16.51-beta-20250905-051437-fe05adb2",
42
+ "@blocklet/theme": "^3.1.34",
43
43
  "@did-connect/authenticator": "^2.2.8",
44
44
  "@did-connect/handler": "^2.2.8",
45
45
  "@nedb/core": "^2.1.5",
@@ -85,5 +85,5 @@
85
85
  "ts-node": "^10.9.1",
86
86
  "typescript": "^5.6.3"
87
87
  },
88
- "gitHead": "c5cedc16ca7239ec7dcadd8e046b2f8684df335c"
88
+ "gitHead": "cadff492e3858895a3e0565b0b1778f166fb34f7"
89
89
  }