@abtnode/core 1.17.5-beta-20251209-090953-3a59e7ac → 1.17.5-beta-20251214-122206-29056e8c

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.
@@ -11,6 +11,7 @@ const isString = require('lodash/isString');
11
11
  const mapValues = require('lodash/mapValues');
12
12
  const map = require('lodash/map');
13
13
  const omit = require('lodash/omit');
14
+ const { Joi } = require('@arcblock/validator');
14
15
  const { joinURL } = require('ufo');
15
16
  const { Op } = require('sequelize');
16
17
  const { getDisplayName } = require('@blocklet/meta/lib/util');
@@ -22,6 +23,8 @@ const BaseState = require('./base');
22
23
  const { parse } = require('../util/ua');
23
24
  const { getScope } = require('../util/audit-log');
24
25
 
26
+ const emailSchema = Joi.string().email().required();
27
+
25
28
  const getServerInfo = (info) =>
26
29
  `[${info.name}](${joinURL(process.env.NODE_ENV === 'production' ? info.routing.adminPath : '', '/settings/about')})`;
27
30
  /**
@@ -232,15 +235,26 @@ const getTaggingInfo = async (args, node, info) => {
232
235
  /**
233
236
  * 隐藏私密信息,主要字段有
234
237
  * 1. email
238
+ * 2. password
235
239
  */
236
- const hidePrivateInfo = (result) => {
237
- const emailRegex =
238
- /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
240
+ const hidePrivateInfo = (result, fields = []) => {
241
+ // 默认需要隐藏的字段(不区分大小写匹配)
242
+ const defaultFields = ['email', 'password'];
243
+ // 合并默认字段和自定义字段
244
+ const sensitiveFields = [...defaultFields, ...fields].map((f) => f.toLowerCase());
245
+
246
+ // 检查字段名是否需要隐藏
247
+ const isSensitiveField = (key) => {
248
+ if (!key) return false;
249
+ const lowerKey = key.toLowerCase();
250
+ return sensitiveFields.some((field) => lowerKey.includes(field));
251
+ };
239
252
 
240
253
  const processValue = (value, key) => {
241
- // 如果字段名包含 email 或值是邮箱格式,则隐藏
242
- if ((key && /email/i.test(key)) || (isString(value) && emailRegex.test(value))) {
243
- return '***';
254
+ // 如果字段名匹配敏感字段,则隐藏
255
+ // 使用反引号包裹,以代码样式显示星号,避免被 Markdown 渲染为加粗/斜体
256
+ if (isSensitiveField(key) || (isString(value) && !emailSchema.validate(value).error)) {
257
+ return '`***`';
244
258
  }
245
259
 
246
260
  // 递归处理对象
@@ -351,8 +365,19 @@ const getLogContent = async (action, args, context, result, info, node) => {
351
365
  return `blocklet ${getBlockletInfo(result, info)} audit federated login member ${args.memberPid} with status: ${
352
366
  args.status
353
367
  }`;
354
- case 'configNotification':
355
- return `updated following notification setting: ${args.notification}`;
368
+ case 'configNotification': {
369
+ const { notification } = args;
370
+ const notificationObject = JSON.parse(notification || '{}');
371
+ const resultObj = {};
372
+ if (notificationObject.email) {
373
+ resultObj.email = hidePrivateInfo(notificationObject.email, ['from', 'host', 'password']);
374
+ }
375
+ if (notificationObject.push) {
376
+ resultObj.push = hidePrivateInfo(notificationObject.push);
377
+ }
378
+
379
+ return `updated following notification setting: ${JSON.stringify(resultObj)}`;
380
+ }
356
381
  case 'updateComponentTitle':
357
382
  return `update component title to **${args.title}**`;
358
383
  case 'updateComponentMountPoint':
@@ -1031,7 +1056,7 @@ class AuditLogState extends BaseState {
1031
1056
  componentDid: context?.user?.componentDid || null,
1032
1057
  });
1033
1058
 
1034
- logger.info('create', data);
1059
+ logger.info('create', { action, userDid: actor.did, componentDid: context?.user?.componentDid || null });
1035
1060
  return resolve(data);
1036
1061
  } catch (err) {
1037
1062
  logger.error('create error', { error: err, action, args, context });
@@ -0,0 +1,193 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ const { BlockletStatus } = require('@blocklet/constant');
3
+ const BaseState = require('./base');
4
+
5
+ /**
6
+ * @extends BaseState<import('@abtnode/models').BlockletChildState>
7
+ */
8
+ class BlockletChildState extends BaseState {
9
+ constructor(model, config = {}) {
10
+ super(model, config);
11
+ }
12
+
13
+ /**
14
+ * Get children by parent blocklet ID
15
+ * @param {string} parentBlockletId - The parent blocklet ID
16
+ * @returns {Promise<Array>} - Array of children
17
+ */
18
+ async getChildrenByParentId(parentBlockletId) {
19
+ if (!parentBlockletId) {
20
+ return [];
21
+ }
22
+ const children = await this.find({ parentBlockletId }, {}, { createdAt: 1 });
23
+ return children || [];
24
+ }
25
+
26
+ /**
27
+ * Delete children by parent blocklet ID
28
+ * @param {string} parentBlockletId - The parent blocklet ID
29
+ * @returns {Promise<number>} - Number of deleted children
30
+ */
31
+ deleteByParentId(parentBlockletId) {
32
+ if (!parentBlockletId) {
33
+ return 0;
34
+ }
35
+ return this.remove({ parentBlockletId });
36
+ }
37
+
38
+ async updateChildStatusRunning(parentBlockletId, childDid, isGreen, additionalUpdates = {}) {
39
+ const now = new Date();
40
+ const baseUpdates = {
41
+ ...additionalUpdates,
42
+ updatedAt: now,
43
+ startedAt: now,
44
+ stoppedAt: null,
45
+ };
46
+
47
+ if (isGreen) {
48
+ // 绿环境启动成功 -> 绿 running,蓝 stopped
49
+ await this.update(
50
+ { parentBlockletId, childDid },
51
+ {
52
+ $set: {
53
+ ...baseUpdates,
54
+ greenStatus: BlockletStatus.running,
55
+ status: BlockletStatus.stopped,
56
+ },
57
+ }
58
+ );
59
+ } else {
60
+ // 蓝环境启动成功 -> 蓝 running,绿 stopped
61
+ await this.update(
62
+ { parentBlockletId, childDid },
63
+ {
64
+ $set: {
65
+ ...baseUpdates,
66
+ greenStatus: BlockletStatus.stopped,
67
+ status: BlockletStatus.running,
68
+ },
69
+ }
70
+ );
71
+ }
72
+ }
73
+
74
+ async updateChildStatusError(parentBlockletId, childDid, isGreen, additionalUpdates = {}) {
75
+ const now = new Date();
76
+ const baseUpdates = {
77
+ ...additionalUpdates,
78
+ updatedAt: now,
79
+ };
80
+
81
+ if (isGreen) {
82
+ // 绿环境启动失败
83
+ await this.update(
84
+ { parentBlockletId, childDid },
85
+ {
86
+ $set: {
87
+ ...baseUpdates,
88
+ greenStatus: BlockletStatus.error,
89
+ },
90
+ }
91
+ );
92
+ } else {
93
+ // 蓝环境启动失败
94
+ await this.update(
95
+ { parentBlockletId, childDid },
96
+ {
97
+ $set: {
98
+ ...baseUpdates,
99
+ status: BlockletStatus.error,
100
+ },
101
+ }
102
+ );
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Update child status only (without overwriting other fields)
108
+ * Used by setBlockletStatus to avoid race conditions in multi-process environments
109
+ * @param {string} parentBlockletId - The parent blocklet ID
110
+ * @param {string} childDid - The child DID
111
+ * @param {Object} options - Update options
112
+ * @param {number} options.status - The blue status to set
113
+ * @param {number} options.greenStatus - The green status to set
114
+ * @param {boolean} options.isGreen - Whether to update green status
115
+ * @param {boolean} options.isGreenAndBlue - Whether to update both statuses
116
+ * @param {string} options.operator - The operator
117
+ * @returns {Promise<Object>} - Updated child
118
+ */
119
+ async updateChildStatus(
120
+ parentBlockletId,
121
+ childDid,
122
+ { status, isGreen = false, isGreenAndBlue = false, operator } = {}
123
+ ) {
124
+ if (!parentBlockletId || !childDid) {
125
+ return null;
126
+ }
127
+
128
+ const updates = {
129
+ updatedAt: new Date(),
130
+ inProgressStart: Date.now(),
131
+ };
132
+
133
+ if (operator) {
134
+ updates.operator = operator;
135
+ }
136
+
137
+ if (isGreenAndBlue) {
138
+ updates.status = status;
139
+ updates.greenStatus = status;
140
+ } else if (isGreen) {
141
+ updates.greenStatus = status;
142
+ } else {
143
+ updates.status = status;
144
+ }
145
+
146
+ if (status === BlockletStatus.running) {
147
+ updates.startedAt = new Date();
148
+ updates.stoppedAt = null;
149
+ } else if (status === BlockletStatus.stopped) {
150
+ updates.startedAt = null;
151
+ updates.stoppedAt = new Date();
152
+ }
153
+
154
+ const [, [updated]] = await this.update({ parentBlockletId, childDid }, { $set: updates });
155
+ return updated;
156
+ }
157
+
158
+ /**
159
+ * Update child ports only (without affecting status fields)
160
+ * Used by refreshBlockletPorts to avoid overwriting status during concurrent operations
161
+ * @param {string} parentBlockletId - The parent blocklet ID
162
+ * @param {string} childDid - The child DID
163
+ * @param {Object} ports - The ports to set (for blue environment)
164
+ * @param {Object} greenPorts - The green ports to set (for green environment)
165
+ * @returns {Promise<Object>} - Updated child
166
+ */
167
+ async updateChildPorts(parentBlockletId, childDid, { ports, greenPorts } = {}) {
168
+ if (!parentBlockletId || !childDid) {
169
+ return null;
170
+ }
171
+
172
+ const updates = {
173
+ updatedAt: new Date(),
174
+ };
175
+
176
+ if (ports !== undefined) {
177
+ updates.ports = ports;
178
+ }
179
+ if (greenPorts !== undefined) {
180
+ updates.greenPorts = greenPorts;
181
+ }
182
+
183
+ // Only update if there's something to update
184
+ if (Object.keys(updates).length <= 1) {
185
+ return null;
186
+ }
187
+
188
+ const [, [updated]] = await this.update({ parentBlockletId, childDid }, { $set: updates });
189
+ return updated;
190
+ }
191
+ }
192
+
193
+ module.exports = BlockletChildState;
@@ -5,12 +5,20 @@ const logger = require('@abtnode/logger')('@abtnode/core:states:blocklet-extras'
5
5
  const camelCase = require('lodash/camelCase');
6
6
  const get = require('lodash/get');
7
7
  const { CustomError } = require('@blocklet/error');
8
+ const security = require('@abtnode/util/lib/security');
9
+ const cloneDeep = require('@abtnode/util/lib/deep-clone');
8
10
 
9
11
  const BaseState = require('./base');
10
12
 
11
13
  const { mergeConfigs, parseConfigs, encryptConfigs } = require('../blocklet/extras');
12
14
  const { validateAddMeta } = require('../validators/blocklet-extra');
13
15
 
16
+ // settings 中需要加密的字段路径
17
+ const SETTINGS_SECURE_FIELDS = ['notification.email.password'];
18
+
19
+ // 加密数据的前缀标记,用于识别数据是否已加密
20
+ const ENCRYPTED_PREFIX = 'ENC:';
21
+
14
22
  const noop = (k) => (v) => v[k];
15
23
 
16
24
  /**
@@ -31,15 +39,69 @@ class BlockletExtrasState extends BaseState {
31
39
  // setting
32
40
  {
33
41
  name: 'settings',
34
- beforeSet: ({ old, cur }) => {
42
+ beforeSet: ({ old, cur, did, dek }) => {
35
43
  const merged = { ...old, ...cur };
36
44
  Object.keys(merged).forEach((key) => {
37
45
  if (merged[key] === undefined || merged[key] === null) {
38
46
  delete merged[key];
39
47
  }
40
48
  });
49
+
50
+ // 对敏感字段进行加密
51
+ const enableSecurity = dek && did;
52
+ if (enableSecurity) {
53
+ SETTINGS_SECURE_FIELDS.forEach((fieldPath) => {
54
+ const value = get(merged, fieldPath);
55
+ // 只加密 cur 中传入的新值,避免重复加密已存储的旧值
56
+ const newValue = get(cur, fieldPath);
57
+ if (newValue !== undefined && value) {
58
+ const keys = fieldPath.split('.');
59
+ let target = merged;
60
+ for (let i = 0; i < keys.length - 1; i++) {
61
+ target = target[keys[i]];
62
+ }
63
+ // 添加前缀标记,用于识别已加密的数据
64
+ const encrypted = ENCRYPTED_PREFIX + security.encrypt(String(value), did, dek);
65
+ target[keys[keys.length - 1]] = encrypted;
66
+ }
67
+ });
68
+ }
69
+
41
70
  return merged;
42
71
  },
72
+ afterGet: ({ data, did, dek }) => {
73
+ if (!data) {
74
+ return data;
75
+ }
76
+
77
+ // 对敏感字段进行解密
78
+ const enableSecurity = dek && did;
79
+ if (enableSecurity) {
80
+ const result = cloneDeep(data);
81
+ SETTINGS_SECURE_FIELDS.forEach((fieldPath) => {
82
+ const value = get(result, fieldPath);
83
+ // 只有带有加密前缀的数据才需要解密,未加密的历史数据保持原值
84
+ if (value && typeof value === 'string' && value.startsWith(ENCRYPTED_PREFIX)) {
85
+ try {
86
+ const keys = fieldPath.split('.');
87
+ let target = result;
88
+ for (let i = 0; i < keys.length - 1; i++) {
89
+ target = target[keys[i]];
90
+ }
91
+ // 去掉前缀后解密
92
+ const encryptedValue = value.slice(ENCRYPTED_PREFIX.length);
93
+ target[keys[keys.length - 1]] = security.decrypt(encryptedValue, did, dek);
94
+ } catch {
95
+ // 解密失败,保持原值(去掉前缀)
96
+ logger.warn('Failed to decrypt settings field', { fieldPath });
97
+ }
98
+ }
99
+ });
100
+ return result;
101
+ }
102
+
103
+ return data;
104
+ },
43
105
  },
44
106
  ];
45
107