@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.
- package/lib/api/team.js +71 -3
- package/lib/blocklet/manager/disk.js +51 -40
- package/lib/blocklet/manager/helper/blue-green-get-componentids.js +11 -16
- package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +118 -82
- package/lib/blocklet/migration-dist/migration.cjs +1 -1
- package/lib/migrations/index.js +4 -4
- package/lib/monitor/blocklet-runtime-monitor.js +3 -5
- package/lib/states/audit-log.js +34 -9
- package/lib/states/blocklet-child.js +193 -0
- package/lib/states/blocklet-extras.js +63 -1
- package/lib/states/blocklet.js +292 -11
- package/lib/states/index.js +4 -1
- package/lib/states/notification.js +4 -2
- package/lib/util/blocklet.js +112 -42
- package/lib/util/migration-sqlite-to-postgres.js +240 -6
- package/package.json +39 -39
- package/lib/blocklet/manager/helper/blue-green-update-blocklet-status.js +0 -18
package/lib/states/audit-log.js
CHANGED
|
@@ -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
|
-
|
|
238
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
|