evolcore 0.0.2 → 0.0.4
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/CHANGELOG.md +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
|
|
2
|
+
import { getContactSnapshot, resolveContactView } from '../../config/contact-book.js';
|
|
3
|
+
import { listConfiguredChannelKeys, mutateAccessPolicy, readAccessPolicySnapshot, } from '../../config/access-policy.js';
|
|
4
|
+
import { expirePendingContactRequests, mutateContactWithOperation, reviewContactRequest, } from '../../config/contact-request-service.js';
|
|
5
|
+
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
6
|
+
/**
|
|
7
|
+
* Connect Menu — 联系人与关系管理协议
|
|
8
|
+
*
|
|
9
|
+
* 定位:role menu 之上的**薄委派层**。
|
|
10
|
+
* - 角色分配 / admin(assignments、resource=assignment/admin)→ 委派给 role-menu,
|
|
11
|
+
* 复用其 agent 级互斥锁、CAS revision、owner/admin 安全护栏与审计链,绝不自行写 relation 文件。
|
|
12
|
+
* - 群组列表(view=groups)→ 委派给 role-menu 的 view=targets(数据源是 AUN live 目录),
|
|
13
|
+
* 群组不落 contact.json(contact book 只接受个人 AID,group 会被拒绝)。
|
|
14
|
+
* - 联系人与申请(view=contacts / contact,add / block / unblock / review)→ 直接读写
|
|
15
|
+
* Contact Book v3;contact.json 是当前状态和 pending 版本的唯一事实来源。
|
|
16
|
+
* - 访问设置(view=access / option=allowedGroupChannels / resource=<access field>)→
|
|
17
|
+
* 查询策略、列举群聊渠道候选项并逐字段写入 Agent config.json 的 access 字段。
|
|
18
|
+
*/
|
|
19
|
+
const MENU_NAME = 'connect';
|
|
20
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
21
|
+
const MAX_PAGE_SIZE = 200;
|
|
22
|
+
/**
|
|
23
|
+
* Map a connect menu request to its authorization operation id.
|
|
24
|
+
* Read views/options → connect.read; contact writes → connect.write;
|
|
25
|
+
* owner-only access fields → connect.access.write;
|
|
26
|
+
* role assignment/admin delegate to the role.* operations role-menu enforces.
|
|
27
|
+
*/
|
|
28
|
+
export function connectMenuOperation(kind, args, value) {
|
|
29
|
+
if (kind === 'options' || kind === 'query')
|
|
30
|
+
return 'connect.read';
|
|
31
|
+
if (kind === 'action')
|
|
32
|
+
return 'connect.write';
|
|
33
|
+
const resource = String(args?.resource ?? '');
|
|
34
|
+
if (resource === 'allowedGroupChannels')
|
|
35
|
+
return 'connect.write';
|
|
36
|
+
if (resource === 'policyMode' || resource === 'requestLimit') {
|
|
37
|
+
return 'connect.access.write';
|
|
38
|
+
}
|
|
39
|
+
if (resource === 'admin')
|
|
40
|
+
return 'connect.admin';
|
|
41
|
+
const decoded = decodeUpdateValue(value);
|
|
42
|
+
if (decoded === 'admin')
|
|
43
|
+
return 'connect.admin';
|
|
44
|
+
return decoded === null || decoded === false || decoded === 'revoke'
|
|
45
|
+
? 'connect.revoke'
|
|
46
|
+
: 'connect.assign';
|
|
47
|
+
}
|
|
48
|
+
// ── Menu Entry ──
|
|
49
|
+
export async function handleConnectMenu(req, context) {
|
|
50
|
+
const { self } = context;
|
|
51
|
+
if (!self)
|
|
52
|
+
return fail('INVALID_ARGUMENT', 'selfAid is required for connect menu');
|
|
53
|
+
if (req.args?.self !== undefined && req.args.self !== self) {
|
|
54
|
+
return fail('PERMISSION_DENIED', 'args.self does not match the receiving Agent');
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
if (req.subtype === 'options')
|
|
58
|
+
return await handleOptions(self, req.args);
|
|
59
|
+
if (req.subtype === 'query')
|
|
60
|
+
return await handleQuery(req, self, context);
|
|
61
|
+
if (req.subtype === 'update')
|
|
62
|
+
return await handleUpdate(req, context);
|
|
63
|
+
if (req.subtype === 'action')
|
|
64
|
+
return await handleAction(req, self, context);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return fail(errorCode(error), errorMessage(error), errorData(error));
|
|
68
|
+
}
|
|
69
|
+
return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
|
|
70
|
+
}
|
|
71
|
+
// ── Options ──
|
|
72
|
+
async function handleOptions(self, args) {
|
|
73
|
+
const option = String(args?.option ?? '').trim();
|
|
74
|
+
if (option) {
|
|
75
|
+
if (option !== 'allowedGroupChannels') {
|
|
76
|
+
return fail('NOT_SUPPORTED', `Unknown connect option: ${option}`);
|
|
77
|
+
}
|
|
78
|
+
const selected = new Set(readAccessPolicySnapshot(self).policy.allowedGroupChannels);
|
|
79
|
+
return ok({
|
|
80
|
+
options: listConfiguredChannelKeys(self).map(channelKey => ({
|
|
81
|
+
option: channelKey,
|
|
82
|
+
label: channelKey,
|
|
83
|
+
selected: selected.has(channelKey),
|
|
84
|
+
})),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await expirePendingContactRequests(self);
|
|
88
|
+
const snapshot = getContactSnapshot(self);
|
|
89
|
+
const contactCount = snapshot.contacts.size;
|
|
90
|
+
const blockedCount = snapshot.blockedIndex.size;
|
|
91
|
+
const pendingCount = [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length;
|
|
92
|
+
return ok({
|
|
93
|
+
options: [
|
|
94
|
+
{
|
|
95
|
+
option: 'contacts',
|
|
96
|
+
label: '联系人列表',
|
|
97
|
+
description: `查看所有状态(${contactCount} 人,${pendingCount} 个待审核,${blockedCount} 人已拉黑)`,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
option: 'groups',
|
|
101
|
+
label: '群组列表',
|
|
102
|
+
description: '查看所在群组',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
option: 'access',
|
|
106
|
+
label: '访问设置',
|
|
107
|
+
description: '设置私聊准入模式与允许群聊的渠道',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// ── Query ──
|
|
113
|
+
async function handleQuery(req, self, context) {
|
|
114
|
+
const args = req.args ?? {};
|
|
115
|
+
const view = String(args.view ?? '');
|
|
116
|
+
if (view === 'contacts')
|
|
117
|
+
return queryContacts(args, self);
|
|
118
|
+
if (view === 'contact')
|
|
119
|
+
return queryContact(req, self);
|
|
120
|
+
if (view === 'access')
|
|
121
|
+
return queryAccess(self);
|
|
122
|
+
// Groups and role assignments are delegated to role-menu (authoritative source).
|
|
123
|
+
// Role *definitions* are out of scope here — use `menu name=role` for those.
|
|
124
|
+
if (view === 'groups') {
|
|
125
|
+
const data = await roleMenuQuery(context, { ...args, view: 'targets', targetType: 'group' });
|
|
126
|
+
return ok({
|
|
127
|
+
self,
|
|
128
|
+
view: 'groups',
|
|
129
|
+
items: (data.items ?? []).map((item) => ({
|
|
130
|
+
groupId: item.groupId,
|
|
131
|
+
peerKey: item.peerKey,
|
|
132
|
+
displayName: item.displayName,
|
|
133
|
+
status: item.status,
|
|
134
|
+
memberCount: item.memberCount,
|
|
135
|
+
roleId: item.explicitDefaultRole ?? null,
|
|
136
|
+
effectiveRoleId: item.baselineRole ?? null,
|
|
137
|
+
effectiveRoleSource: item.baselineRoleSource ?? null,
|
|
138
|
+
relationRevision: item.relationRevision,
|
|
139
|
+
})),
|
|
140
|
+
nextCursor: data.nextCursor ?? null,
|
|
141
|
+
total: data.total,
|
|
142
|
+
dataSource: data.dataSource,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (view === 'assignments') {
|
|
146
|
+
const data = await roleMenuQuery(context, { ...args, view: 'assignments', includeDefaults: false });
|
|
147
|
+
return ok({
|
|
148
|
+
self,
|
|
149
|
+
view: 'assignments',
|
|
150
|
+
...(args.roleId === undefined ? {} : { roleId: args.roleId }),
|
|
151
|
+
policyRevision: data.policyRevision,
|
|
152
|
+
items: (data.items ?? []).map(normalizeAssignmentItem),
|
|
153
|
+
nextCursor: data.nextCursor ?? null,
|
|
154
|
+
dataSource: { ...data.dataSource, kind: 'relation-config' },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return fail('NOT_SUPPORTED', `Unknown view: ${String(view)}`);
|
|
158
|
+
}
|
|
159
|
+
function queryAccess(self) {
|
|
160
|
+
const { policy, accessRevision } = readAccessPolicySnapshot(self);
|
|
161
|
+
return ok({
|
|
162
|
+
self,
|
|
163
|
+
view: 'access',
|
|
164
|
+
policy,
|
|
165
|
+
accessRevision,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function queryContacts(args, self) {
|
|
169
|
+
await expirePendingContactRequests(self);
|
|
170
|
+
const snapshot = getContactSnapshot(self);
|
|
171
|
+
const page = paginate([...snapshot.contacts.keys()].sort(), args, self, primaryId => primaryId);
|
|
172
|
+
const items = page.items.map(primaryId => contactItem(self, primaryId));
|
|
173
|
+
return ok({
|
|
174
|
+
self,
|
|
175
|
+
view: 'contacts',
|
|
176
|
+
items,
|
|
177
|
+
contactRevision: snapshot.contactRevision,
|
|
178
|
+
pendingCount: [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length,
|
|
179
|
+
nextCursor: page.nextCursor,
|
|
180
|
+
dataSource: { kind: 'contact-book', authoritative: true, complete: true },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function queryContact(req, self) {
|
|
184
|
+
await expirePendingContactRequests(self);
|
|
185
|
+
const primaryId = String(req.args?.primaryId ?? '').trim();
|
|
186
|
+
if (!primaryId)
|
|
187
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
188
|
+
const snapshot = getContactSnapshot(self);
|
|
189
|
+
if (!snapshot.contacts.has(primaryId))
|
|
190
|
+
return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
|
|
191
|
+
return ok({ self, view: 'contact', ...contactItem(self, primaryId), contactRevision: snapshot.contactRevision });
|
|
192
|
+
}
|
|
193
|
+
function contactItem(self, primaryId) {
|
|
194
|
+
const view = resolveContactView(self, primaryId);
|
|
195
|
+
const pendingRequest = getContactSnapshot(self).contacts.get(primaryId)?.pendingRequest;
|
|
196
|
+
return {
|
|
197
|
+
primaryId,
|
|
198
|
+
displayName: view.displayName ?? null,
|
|
199
|
+
status: view.status,
|
|
200
|
+
blocked: view.blocked,
|
|
201
|
+
isOwner: view.isOwner,
|
|
202
|
+
aliases: view.aliases,
|
|
203
|
+
...(pendingRequest ? {
|
|
204
|
+
pendingRequest: {
|
|
205
|
+
id: pendingRequest.id,
|
|
206
|
+
submittedAt: pendingRequest.submittedAt,
|
|
207
|
+
expiresAt: pendingRequest.expiresAt,
|
|
208
|
+
},
|
|
209
|
+
} : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function normalizeAssignmentItem(item) {
|
|
213
|
+
const peerKey = item?.target?.peerKey;
|
|
214
|
+
if (typeof peerKey !== 'string')
|
|
215
|
+
return item;
|
|
216
|
+
try {
|
|
217
|
+
const { channelType, channelId } = parsePeerKey(peerKey);
|
|
218
|
+
if (channelType === 'aun')
|
|
219
|
+
return item;
|
|
220
|
+
return {
|
|
221
|
+
...item,
|
|
222
|
+
target: {
|
|
223
|
+
kind: item.target.kind,
|
|
224
|
+
channelType,
|
|
225
|
+
channelId,
|
|
226
|
+
peerKey,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return item;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ── Update ──
|
|
235
|
+
async function handleUpdate(req, context) {
|
|
236
|
+
const args = req.args ?? {};
|
|
237
|
+
const resource = String(args.resource ?? '');
|
|
238
|
+
if (isAccessUpdateResource(resource)) {
|
|
239
|
+
return updateAccessResource(req, context, resource);
|
|
240
|
+
}
|
|
241
|
+
if (resource !== 'assignment' && resource !== 'admin') {
|
|
242
|
+
return fail('NOT_SUPPORTED', `Unknown resource: ${String(resource)}`);
|
|
243
|
+
}
|
|
244
|
+
if (req.value === undefined || !req.value.trim()) {
|
|
245
|
+
return fail('INVALID_ARGUMENT', 'value is required');
|
|
246
|
+
}
|
|
247
|
+
validateWritableTarget(args.target);
|
|
248
|
+
const value = roleMenuUpdateValue(resource, req.value);
|
|
249
|
+
const data = await roleMenuUpdate(context, args, value);
|
|
250
|
+
return ok(resource === 'assignment' ? normalizeAssignmentUpdate(data) : data);
|
|
251
|
+
}
|
|
252
|
+
// ── Action (Contact Book v3) ──
|
|
253
|
+
async function handleAction(req, self, context) {
|
|
254
|
+
const action = req.action;
|
|
255
|
+
if (action === 'add')
|
|
256
|
+
return actionAdd(req, self, context);
|
|
257
|
+
if (action === 'block')
|
|
258
|
+
return actionSetStatus(req, self, context, true);
|
|
259
|
+
if (action === 'unblock')
|
|
260
|
+
return actionSetStatus(req, self, context, false);
|
|
261
|
+
if (action === 'approve-request')
|
|
262
|
+
return actionReviewRequest(req, self, context, 'approve');
|
|
263
|
+
if (action === 'reject-request')
|
|
264
|
+
return actionReviewRequest(req, self, context, 'reject');
|
|
265
|
+
if (action === 'block-request')
|
|
266
|
+
return actionReviewRequest(req, self, context, 'block');
|
|
267
|
+
return fail('NOT_SUPPORTED', `Unknown action: ${String(action)}`);
|
|
268
|
+
}
|
|
269
|
+
function isAccessUpdateResource(resource) {
|
|
270
|
+
return resource === 'policyMode'
|
|
271
|
+
|| resource === 'allowedGroupChannels'
|
|
272
|
+
|| resource === 'requestLimit';
|
|
273
|
+
}
|
|
274
|
+
function updateAccessResource(req, context, resource) {
|
|
275
|
+
const args = req.args ?? {};
|
|
276
|
+
if (resource === 'allowedGroupChannels')
|
|
277
|
+
requireAdminContext(context);
|
|
278
|
+
else
|
|
279
|
+
requireOwnerContext(context);
|
|
280
|
+
if (req.value === undefined || !req.value.trim()) {
|
|
281
|
+
return fail('INVALID_ARGUMENT', 'value is required');
|
|
282
|
+
}
|
|
283
|
+
const self = context.self;
|
|
284
|
+
const expectedAccessRevision = requireAccessRevision(args);
|
|
285
|
+
const decoded = decodeAccessUpdateValue(req.value);
|
|
286
|
+
if (resource === 'policyMode') {
|
|
287
|
+
if (typeof decoded !== 'string' || !['open', 'contacts', 'owners-only'].includes(decoded)) {
|
|
288
|
+
return fail('INVALID_ARGUMENT', 'policyMode must be open, contacts, or owners-only');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (resource === 'allowedGroupChannels') {
|
|
292
|
+
if (!Array.isArray(decoded)
|
|
293
|
+
|| !decoded.every(value => typeof value === 'string' && value.length > 0)
|
|
294
|
+
|| new Set(decoded).size !== decoded.length) {
|
|
295
|
+
return fail('INVALID_ARGUMENT', 'allowedGroupChannels must be a JSON string array');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const requestLimit = decoded;
|
|
300
|
+
if (!requestLimit || typeof requestLimit !== 'object' || Array.isArray(requestLimit)
|
|
301
|
+
|| Object.keys(requestLimit).some(key => key !== 'maxTimes' && key !== 'coolDown')
|
|
302
|
+
|| !Number.isInteger(requestLimit.maxTimes)
|
|
303
|
+
|| Number(requestLimit.maxTimes) < 1
|
|
304
|
+
|| Number(requestLimit.maxTimes) > 100_000
|
|
305
|
+
|| !Number.isInteger(requestLimit.coolDown)
|
|
306
|
+
|| Number(requestLimit.coolDown) < 1
|
|
307
|
+
|| Number(requestLimit.coolDown) > 86_400) {
|
|
308
|
+
return fail('INVALID_ARGUMENT', 'requestLimit must be a JSON object with integer maxTimes and coolDown');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const result = mutateAccessPolicy({
|
|
312
|
+
selfAid: self,
|
|
313
|
+
actorAid: String(context.actorAid || ''),
|
|
314
|
+
actorRole: context.actorRole,
|
|
315
|
+
resource: `access.${resource}`,
|
|
316
|
+
expectedAccessRevision,
|
|
317
|
+
update: current => accessPolicyWithUpdate(current, resource, decoded),
|
|
318
|
+
});
|
|
319
|
+
return ok({
|
|
320
|
+
self,
|
|
321
|
+
resource,
|
|
322
|
+
policy: result.policy,
|
|
323
|
+
accessRevision: result.accessRevision,
|
|
324
|
+
changed: result.changed,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function actionReviewRequest(req, self, context, decision) {
|
|
328
|
+
const args = req.args ?? {};
|
|
329
|
+
const primaryId = String(args.primaryId ?? '').trim();
|
|
330
|
+
const requestId = String(args.requestId ?? '').trim();
|
|
331
|
+
if (!primaryId)
|
|
332
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
333
|
+
if (!requestId)
|
|
334
|
+
return fail('INVALID_ARGUMENT', 'requestId is required');
|
|
335
|
+
requireAdminContext(context);
|
|
336
|
+
const expectedContactRevision = requireContactRevision(args);
|
|
337
|
+
const actorId = String(context.actorAid || '').trim();
|
|
338
|
+
const reviewed = await reviewContactRequest({
|
|
339
|
+
selfAid: self,
|
|
340
|
+
primaryId,
|
|
341
|
+
requestId,
|
|
342
|
+
expectedContactRevision,
|
|
343
|
+
approverId: actorId,
|
|
344
|
+
actorId,
|
|
345
|
+
reviewerRole: context.actorRole,
|
|
346
|
+
decision,
|
|
347
|
+
});
|
|
348
|
+
if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
|
|
349
|
+
return ok({
|
|
350
|
+
self,
|
|
351
|
+
action: `${decision}-request`,
|
|
352
|
+
primaryId,
|
|
353
|
+
requestId,
|
|
354
|
+
result: reviewed.code,
|
|
355
|
+
contactRevision: reviewed.contactRevision,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const code = reviewed.code === 'forbidden' ? 'PERMISSION_DENIED'
|
|
359
|
+
: reviewed.code === 'revision-conflict' ? 'CONFLICT'
|
|
360
|
+
: reviewed.code === 'request-expired' ? 'EXPIRED'
|
|
361
|
+
: 'NOT_FOUND';
|
|
362
|
+
return fail(code, reviewed.code.replace(/-/g, ' '), { contactRevision: reviewed.contactRevision });
|
|
363
|
+
}
|
|
364
|
+
async function actionAdd(req, self, context) {
|
|
365
|
+
requireAdminContext(context);
|
|
366
|
+
const args = req.args ?? {};
|
|
367
|
+
const primaryId = String(args.primaryId ?? '').trim();
|
|
368
|
+
if (!primaryId)
|
|
369
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
370
|
+
const expectedContactRevision = requireContactRevision(args);
|
|
371
|
+
const hasDisplayName = Object.prototype.hasOwnProperty.call(args, 'displayName');
|
|
372
|
+
if (hasDisplayName && args.displayName !== null && typeof args.displayName !== 'string') {
|
|
373
|
+
return fail('INVALID_ARGUMENT', 'displayName must be a string or null');
|
|
374
|
+
}
|
|
375
|
+
const current = getContactSnapshot(self).contacts.get(primaryId);
|
|
376
|
+
const displayName = hasDisplayName ? args.displayName : current?.displayName ?? null;
|
|
377
|
+
const result = await mutateContactWithOperation({
|
|
378
|
+
selfAid: self,
|
|
379
|
+
actorId: context.actorAid ?? 'connect-menu',
|
|
380
|
+
operation: 'manual-add',
|
|
381
|
+
expectedContactRevision,
|
|
382
|
+
mutation: { type: 'set-display-name', primaryId, displayName },
|
|
383
|
+
primaryId,
|
|
384
|
+
});
|
|
385
|
+
const storedDisplayName = result.contact.contacts[primaryId]?.displayName ?? null;
|
|
386
|
+
return ok({
|
|
387
|
+
self,
|
|
388
|
+
action: 'add',
|
|
389
|
+
primaryId,
|
|
390
|
+
displayName: storedDisplayName,
|
|
391
|
+
changed: result.changed,
|
|
392
|
+
contactRevision: result.contactRevision,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async function actionSetStatus(req, self, context, blocked) {
|
|
396
|
+
requireAdminContext(context);
|
|
397
|
+
const args = req.args ?? {};
|
|
398
|
+
const primaryId = String(args.primaryId ?? '').trim();
|
|
399
|
+
if (!primaryId)
|
|
400
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
401
|
+
const expectedContactRevision = requireContactRevision(args);
|
|
402
|
+
if (!getContactSnapshot(self).contacts.has(primaryId)) {
|
|
403
|
+
return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
|
|
404
|
+
}
|
|
405
|
+
const result = await mutateContactWithOperation({
|
|
406
|
+
selfAid: self,
|
|
407
|
+
actorId: context.actorAid ?? 'connect-menu',
|
|
408
|
+
operation: blocked ? 'block' : 'unblock',
|
|
409
|
+
expectedContactRevision,
|
|
410
|
+
mutation: { type: 'set-status', primaryId, status: blocked ? 'blocked' : 'active' },
|
|
411
|
+
primaryId,
|
|
412
|
+
});
|
|
413
|
+
return ok({
|
|
414
|
+
self,
|
|
415
|
+
action: blocked ? 'block' : 'unblock',
|
|
416
|
+
primaryId,
|
|
417
|
+
changed: result.changed,
|
|
418
|
+
contactRevision: result.contactRevision,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
// ── Helpers ──
|
|
422
|
+
function ok(data) {
|
|
423
|
+
return { type: 'menu.response', id: '', name: MENU_NAME, data };
|
|
424
|
+
}
|
|
425
|
+
function requireOwnerContext(context) {
|
|
426
|
+
if (context.actorRole !== 'owner') {
|
|
427
|
+
throw connectError('PERMISSION_DENIED', 'Only an Agent Owner can modify this access setting');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function requireAdminContext(context) {
|
|
431
|
+
if (context.actorRole !== 'owner' && context.actorRole !== 'admin') {
|
|
432
|
+
throw connectError('PERMISSION_DENIED', 'Only an Agent Owner or Admin can perform this operation');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function fail(code, message, data) {
|
|
436
|
+
return {
|
|
437
|
+
type: 'menu.response',
|
|
438
|
+
id: '',
|
|
439
|
+
name: MENU_NAME,
|
|
440
|
+
error: { code, message, ...(data === undefined ? {} : { data }) },
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function errorCode(error) {
|
|
444
|
+
const code = error?.code;
|
|
445
|
+
if (code === 'INVALID_AID')
|
|
446
|
+
return 'INVALID_ARGUMENT';
|
|
447
|
+
if (code === 'OWNER_PROTECTED')
|
|
448
|
+
return 'NOT_ALLOWED';
|
|
449
|
+
if (code === 'ALIAS_CONFLICT')
|
|
450
|
+
return 'CONFLICT';
|
|
451
|
+
if (code === 'INVALID_CONTACT_BOOK')
|
|
452
|
+
return 'INTERNAL_ERROR';
|
|
453
|
+
if (code === 'access_policy_owner_required')
|
|
454
|
+
return 'PERMISSION_DENIED';
|
|
455
|
+
if (code === 'access_policy_manager_required')
|
|
456
|
+
return 'PERMISSION_DENIED';
|
|
457
|
+
if (code === 'access_policy_revision_conflict')
|
|
458
|
+
return 'CONFLICT';
|
|
459
|
+
if (code === 'access_policy_temporarily_unavailable')
|
|
460
|
+
return 'TEMPORARILY_UNAVAILABLE';
|
|
461
|
+
if (code === 'restricted_mode_without_owner')
|
|
462
|
+
return 'NOT_ALLOWED';
|
|
463
|
+
if (code === 'unknown_group_access_channel')
|
|
464
|
+
return 'INVALID_ARGUMENT';
|
|
465
|
+
if (code === 'invalid_access_policy')
|
|
466
|
+
return 'INTERNAL_ERROR';
|
|
467
|
+
return typeof code === 'string' ? code : 'INTERNAL_ERROR';
|
|
468
|
+
}
|
|
469
|
+
function errorMessage(error) {
|
|
470
|
+
if (error instanceof Error)
|
|
471
|
+
return error.message;
|
|
472
|
+
const message = error?.message;
|
|
473
|
+
return typeof message === 'string' ? message : String(error);
|
|
474
|
+
}
|
|
475
|
+
function errorData(error) {
|
|
476
|
+
return error && typeof error === 'object' ? error.data : undefined;
|
|
477
|
+
}
|
|
478
|
+
function requireContactRevision(args) {
|
|
479
|
+
if (!Object.prototype.hasOwnProperty.call(args, 'expectedContactRevision')) {
|
|
480
|
+
throw connectError('INVALID_ARGUMENT', 'expectedContactRevision is required');
|
|
481
|
+
}
|
|
482
|
+
if (typeof args.expectedContactRevision !== 'string' || !args.expectedContactRevision) {
|
|
483
|
+
throw connectError('INVALID_ARGUMENT', 'expectedContactRevision must be a non-empty string');
|
|
484
|
+
}
|
|
485
|
+
return args.expectedContactRevision;
|
|
486
|
+
}
|
|
487
|
+
function requireAccessRevision(args) {
|
|
488
|
+
if (!Object.prototype.hasOwnProperty.call(args, 'expectedAccessRevision')) {
|
|
489
|
+
throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision is required');
|
|
490
|
+
}
|
|
491
|
+
if (typeof args.expectedAccessRevision !== 'string' || !args.expectedAccessRevision) {
|
|
492
|
+
throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision must be a non-empty string');
|
|
493
|
+
}
|
|
494
|
+
return args.expectedAccessRevision;
|
|
495
|
+
}
|
|
496
|
+
function decodeUpdateValue(value) {
|
|
497
|
+
const text = String(value ?? '').trim();
|
|
498
|
+
if (!text)
|
|
499
|
+
return undefined;
|
|
500
|
+
try {
|
|
501
|
+
const decoded = JSON.parse(text);
|
|
502
|
+
if (decoded === null || typeof decoded === 'string' || typeof decoded === 'boolean')
|
|
503
|
+
return decoded;
|
|
504
|
+
}
|
|
505
|
+
catch { }
|
|
506
|
+
return text;
|
|
507
|
+
}
|
|
508
|
+
function decodeAccessUpdateValue(value) {
|
|
509
|
+
const text = value.trim();
|
|
510
|
+
try {
|
|
511
|
+
return JSON.parse(text);
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
return text;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function roleMenuUpdateValue(resource, value) {
|
|
518
|
+
const decoded = decodeUpdateValue(value);
|
|
519
|
+
if (resource === 'assignment') {
|
|
520
|
+
if (decoded !== null && typeof decoded !== 'string') {
|
|
521
|
+
throw connectError('INVALID_ARGUMENT', 'assignment value must be a role ID or null');
|
|
522
|
+
}
|
|
523
|
+
return JSON.stringify(decoded);
|
|
524
|
+
}
|
|
525
|
+
if (decoded !== 'grant' && decoded !== 'revoke') {
|
|
526
|
+
throw connectError('INVALID_ARGUMENT', 'admin value must be grant or revoke');
|
|
527
|
+
}
|
|
528
|
+
return JSON.stringify(decoded);
|
|
529
|
+
}
|
|
530
|
+
function normalizeAssignmentUpdate(data) {
|
|
531
|
+
const explicitRole = data?.assignment?.explicitRole;
|
|
532
|
+
return {
|
|
533
|
+
...data,
|
|
534
|
+
roleId: typeof explicitRole === 'string' ? explicitRole : null,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function accessPolicyWithUpdate(current, resource, decoded) {
|
|
538
|
+
if (resource === 'policyMode') {
|
|
539
|
+
return { ...current, policyMode: decoded };
|
|
540
|
+
}
|
|
541
|
+
if (resource === 'allowedGroupChannels') {
|
|
542
|
+
return { ...current, allowedGroupChannels: [...decoded] };
|
|
543
|
+
}
|
|
544
|
+
const requestLimit = decoded;
|
|
545
|
+
return {
|
|
546
|
+
...current,
|
|
547
|
+
requestLimit: { maxTimes: requestLimit.maxTimes, coolDown: requestLimit.coolDown },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function validateWritableTarget(target) {
|
|
551
|
+
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
552
|
+
return;
|
|
553
|
+
const value = target;
|
|
554
|
+
if (typeof value.channelType === 'string' && value.channelType !== 'aun') {
|
|
555
|
+
throw connectError('NOT_SUPPORTED', 'Native non-AUN assignment writes are not supported in Phase 1');
|
|
556
|
+
}
|
|
557
|
+
const kind = value.kind;
|
|
558
|
+
const logicalId = kind === 'private' ? value.peerAid : kind === 'group' ? value.groupId : undefined;
|
|
559
|
+
if (value.peerKey === undefined || typeof logicalId !== 'string')
|
|
560
|
+
return;
|
|
561
|
+
const expected = formatPeerKey('aun', logicalId);
|
|
562
|
+
if (value.peerKey !== expected) {
|
|
563
|
+
throw connectError('INVALID_ARGUMENT', 'target.peerKey does not match the logical target', {
|
|
564
|
+
expectedPeerKey: expected,
|
|
565
|
+
peerKey: value.peerKey,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function paginate(items, args, self, key) {
|
|
570
|
+
const requestedLimit = Number(args.limit ?? DEFAULT_PAGE_SIZE);
|
|
571
|
+
const limit = Number.isInteger(requestedLimit) && requestedLimit > 0
|
|
572
|
+
? Math.min(requestedLimit, MAX_PAGE_SIZE)
|
|
573
|
+
: DEFAULT_PAGE_SIZE;
|
|
574
|
+
let start = 0;
|
|
575
|
+
if (args.cursor !== undefined && args.cursor !== null) {
|
|
576
|
+
try {
|
|
577
|
+
const decoded = JSON.parse(Buffer.from(String(args.cursor), 'base64url').toString('utf8'));
|
|
578
|
+
if (!decoded || typeof decoded.after !== 'string')
|
|
579
|
+
throw new Error();
|
|
580
|
+
start = items.findIndex(item => key(item) > decoded.after);
|
|
581
|
+
if (start < 0)
|
|
582
|
+
start = items.length;
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
throw connectError('INVALID_ARGUMENT', 'cursor is invalid', { self, field: 'cursor' });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const page = items.slice(start, start + limit);
|
|
589
|
+
const hasMore = start + page.length < items.length;
|
|
590
|
+
return {
|
|
591
|
+
items: page,
|
|
592
|
+
nextCursor: hasMore && page.length
|
|
593
|
+
? Buffer.from(JSON.stringify({ after: key(page[page.length - 1]) }), 'utf8').toString('base64url')
|
|
594
|
+
: null,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function connectError(code, message, data) {
|
|
598
|
+
return Object.assign(new Error(message), { code, ...(data === undefined ? {} : { data }) });
|
|
599
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { loadDaemonConfig } from '../../config-store.js';
|
|
2
|
+
import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
3
|
+
import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
|
|
4
|
+
export function evaluateEvolMenuVersionGate(input) {
|
|
5
|
+
const minimum = loadDaemonConfig().aun?.minEvolVersion;
|
|
6
|
+
if (!minimum)
|
|
7
|
+
return null;
|
|
8
|
+
const minimumVersion = parseStableSemver(minimum);
|
|
9
|
+
if (!minimumVersion) {
|
|
10
|
+
throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
|
|
11
|
+
}
|
|
12
|
+
const received = input.protectedHeaders?.evol_version;
|
|
13
|
+
const receivedVersion = input.encrypted ? parseStableSemver(received) : null;
|
|
14
|
+
if (!receivedVersion || compareStableSemver(receivedVersion, minimumVersion) < 0) {
|
|
15
|
+
return {
|
|
16
|
+
code: 'UPGRADE_REQUIRED',
|
|
17
|
+
message: `Evol ${minimum} or later is required`,
|
|
18
|
+
data: {
|
|
19
|
+
minimum,
|
|
20
|
+
received: receivedVersion ? received : null,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
export function isEvolMenuVersionGateEnabled() {
|
|
27
|
+
return loadDaemonConfig().aun?.minEvolVersion !== undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Token may be issued while disabled, but only this flag makes it an access requirement. */
|
|
30
|
+
export function isAunMenuTokenRequired() {
|
|
31
|
+
return loadDaemonConfig().aun?.menuTokenRequired === true;
|
|
32
|
+
}
|
|
33
|
+
export function evolMenuResponseTransportMetadata(_requestEncrypted) {
|
|
34
|
+
return {
|
|
35
|
+
encrypted: true,
|
|
36
|
+
protectedHeaders: { ec_version: readInstalledEvolcoreVersion() },
|
|
37
|
+
};
|
|
38
|
+
}
|