evolcore 0.0.12 → 0.0.13
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 +22 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +53 -7
- package/dist/agents/codex-app-server-client.js +53 -5
- package/dist/agents/codex-runner.js +104 -59
- package/dist/agents/ecagent-runner.js +4 -1
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +116 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +371 -136
- package/dist/cli/agent.js +9 -96
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +19 -10
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +1 -0
- package/dist/core/command/command-handler.js +198 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/message-bridge.js +63 -11
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +107 -41
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +93 -14
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +249 -27
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +161 -86
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +7 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -62,7 +62,8 @@ async function candidateExists(store, candidate) {
|
|
|
62
62
|
throw new Error(`Gateway 不可达,无法查重控制 AID:${r.error?.message ?? 'unknown'}`);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* 生成控制 AID:循环候选 → candidateExists 查重(权威 PKI 判据)→ 不冲突则 aidCreate
|
|
65
|
+
* 生成控制 AID:循环候选 → candidateExists 查重(权威 PKI 判据)→ 不冲突则 aidCreate;
|
|
66
|
+
* 查重通过但注册失败时也换候选重试,覆盖查重与注册之间的竞态。
|
|
66
67
|
* - 查重走 GET 证书(见 candidateExists;不拉 agent.md,控制 AID 本就不传 agent.md)
|
|
67
68
|
* - fail-fast:查重探测失败(网关不可达)立即抛错,不掩盖成"均冲突"
|
|
68
69
|
* - agent.md 不上传:aidCreate 仅注册身份 + 写私钥,不调 agentmdPut
|
|
@@ -72,6 +73,7 @@ export async function generateControlAid(aidDomain) {
|
|
|
72
73
|
? resolveControlAidDomain()
|
|
73
74
|
: normalizeAidDomain(aidDomain, 'aidDomain');
|
|
74
75
|
const store = await getAidStore({ slotId: SLOT.cli });
|
|
76
|
+
let lastRegistrationError;
|
|
75
77
|
try {
|
|
76
78
|
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
77
79
|
const candidate = candidateAid(finalAidDomain);
|
|
@@ -79,22 +81,43 @@ export async function generateControlAid(aidDomain) {
|
|
|
79
81
|
logger.info(`[control-aid] ${candidate} 已注册,重试 (${i + 1}/${MAX_ATTEMPTS})`);
|
|
80
82
|
continue;
|
|
81
83
|
}
|
|
82
|
-
const created = await aidCreate(candidate);
|
|
83
|
-
// 清理 aidCreate 内部另开的 client/store——关闭失败不可丢弃已注册的 AID(否则下次 init
|
|
84
|
-
// 会把它当冲突,白白消耗一次重试)。close 异常降级为 warn。
|
|
85
84
|
try {
|
|
86
|
-
await
|
|
85
|
+
const created = await aidCreate(candidate);
|
|
86
|
+
// 清理 aidCreate 内部另开的 client/store——关闭失败不可丢弃已注册的 AID(否则下次 init
|
|
87
|
+
// 会把它当冲突,白白消耗一次重试)。close 异常降级为 warn。
|
|
88
|
+
try {
|
|
89
|
+
await created.client?.close?.();
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
logger.warn(`[control-aid] client.close() 失败(非致命): ${e}`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await created.store?.close?.();
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
logger.warn(`[control-aid] store.close() 失败(非致命): ${e}`);
|
|
99
|
+
}
|
|
100
|
+
return { aid: created.aid, gateway: created.gateway };
|
|
87
101
|
}
|
|
88
102
|
catch (e) {
|
|
89
|
-
|
|
103
|
+
// 查重与实际注册之间存在 TOCTOU 竞态;注册接口也可能因服务端暂时失败而拒绝,
|
|
104
|
+
// 这些情况都换一个候选继续尝试。查重阶段的网关错误仍在 candidateExists 中 fail-fast。
|
|
105
|
+
lastRegistrationError = e;
|
|
106
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
107
|
+
logger.warn(`[control-aid] ${candidate} 注册失败: ${reason},重试 (${i + 1}/${MAX_ATTEMPTS})`);
|
|
90
108
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
109
|
+
}
|
|
110
|
+
if (lastRegistrationError !== undefined) {
|
|
111
|
+
const reason = lastRegistrationError instanceof Error
|
|
112
|
+
? lastRegistrationError.message
|
|
113
|
+
: String(lastRegistrationError);
|
|
114
|
+
const error = new Error(`无法生成控制 AID:连续 ${MAX_ATTEMPTS} 次注册尝试失败;最后错误:${reason}`, {
|
|
115
|
+
cause: lastRegistrationError,
|
|
116
|
+
});
|
|
117
|
+
const code = lastRegistrationError?.code;
|
|
118
|
+
if (typeof code === 'string')
|
|
119
|
+
error.code = code;
|
|
120
|
+
throw error;
|
|
98
121
|
}
|
|
99
122
|
throw new Error(`无法生成控制 AID:连续 ${MAX_ATTEMPTS} 次候选均冲突`);
|
|
100
123
|
}
|
package/dist/aun/aid/store.js
CHANGED
|
@@ -7,21 +7,27 @@ import { AUN_KEYSTORE_ENCRYPTION_SEED } from './encryption-seed-policy.js';
|
|
|
7
7
|
* slotIsolationKey 取第一个分隔符(空格/'/'/':')之前的部分作为隔离键,
|
|
8
8
|
* 隔离键相同 = 共享消费通道(token / seq 游标 / 消息过滤)。
|
|
9
9
|
*
|
|
10
|
-
* 'evolcore daemon' → 隔离键 'evolcore'
|
|
11
|
-
* 'evolcore
|
|
12
|
-
* 'evolcore
|
|
10
|
+
* 'evolcore daemon' → 隔离键 'evolcore'
|
|
11
|
+
* 'evolcore-cli' → 隔离键 'evolcore-cli'
|
|
12
|
+
* 'evolcore-netcheck' → 隔离键 'evolcore-netcheck'
|
|
13
|
+
* 'evolcore-group-rules' → 隔离键 'evolcore-group-rules'
|
|
13
14
|
* 'evolcore-bench' → 隔离键 'evolcore-bench'(连字符非分隔符)→ 独立通道
|
|
14
15
|
*
|
|
15
16
|
* 设计意图:
|
|
16
|
-
* - daemon
|
|
17
|
-
* -
|
|
17
|
+
* - daemon 保留历史 slot 值;slot_id 是网关上的消费游标身份,不可改名。
|
|
18
|
+
* - CLI / ecweb 查询使用新的独立 slot,不能共享 daemon 的 token 或 seq 游标。
|
|
19
|
+
* - netcheck 使用独立消费者,不得因诊断连接影响 daemon 的入站顺序。
|
|
20
|
+
* - 群规则同步是短连接 RPC,也必须使用独立 slot,避免触碰 daemon 消费游标。
|
|
18
21
|
* - bench 各并发单元用 evolcore-bench-<N>,各自独立隔离键,互不干扰
|
|
19
22
|
*/
|
|
20
23
|
export const SLOT = {
|
|
24
|
+
// Wire-compatibility contract: changing this creates a new gateway cursor
|
|
25
|
+
// and can replay retained history into the daemon.
|
|
21
26
|
daemon: 'evolcore daemon',
|
|
22
|
-
cli: 'evolcore
|
|
27
|
+
cli: 'evolcore-cli',
|
|
28
|
+
groupRules: 'evolcore-group-rules',
|
|
23
29
|
bench: 'evolcore-bench',
|
|
24
|
-
netcheck: 'evolcore
|
|
30
|
+
netcheck: 'evolcore-netcheck',
|
|
25
31
|
};
|
|
26
32
|
/** 加载身份失败时抛出,携带 SDK 的错误码与消息。 */
|
|
27
33
|
export class AidLoadError extends Error {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export class GroupFsDownloadError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'GroupFsDownloadError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Download a group file into memory through the ticket/data plane. */
|
|
11
|
+
export async function downloadGroupFsBytes(client, path) {
|
|
12
|
+
const ticketValue = await client.call('group.fs.create_download_ticket', { path });
|
|
13
|
+
if (!isRecord(ticketValue)) {
|
|
14
|
+
throw new GroupFsDownloadError('INVALID_TICKET', 'group.fs.create_download_ticket returned invalid response');
|
|
15
|
+
}
|
|
16
|
+
const downloadUrl = stringField(ticketValue, ['download_url', 'url']);
|
|
17
|
+
if (!downloadUrl) {
|
|
18
|
+
throw new GroupFsDownloadError('MISSING_DOWNLOAD_URL', 'group.fs.create_download_ticket did not return download_url');
|
|
19
|
+
}
|
|
20
|
+
const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
|
|
21
|
+
const expectedSha = stringField(ticketValue, ['sha256']);
|
|
22
|
+
if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
|
|
23
|
+
throw new GroupFsDownloadError('HASH_MISMATCH', 'download hash verification failed');
|
|
24
|
+
}
|
|
25
|
+
return { bytes, ticket: ticketValue };
|
|
26
|
+
}
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function stringField(value, keys) {
|
|
31
|
+
for (const key of keys) {
|
|
32
|
+
const field = value[key];
|
|
33
|
+
if (typeof field === 'string' && field.trim())
|
|
34
|
+
return field;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
function sha256Hex(bytes) {
|
|
39
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
function bearerHeaders(client) {
|
|
42
|
+
const anyClient = client;
|
|
43
|
+
if (typeof anyClient.getAccessToken === 'function') {
|
|
44
|
+
const token = stringValue(anyClient.getAccessToken());
|
|
45
|
+
if (token)
|
|
46
|
+
return { Authorization: `Bearer ${token}` };
|
|
47
|
+
}
|
|
48
|
+
const token = stringValue(anyClient.accessToken)
|
|
49
|
+
|| stringValue(anyClient.access_token)
|
|
50
|
+
|| stringValue(anyClient._identity?.access_token)
|
|
51
|
+
|| stringValue(anyClient._sessionParams?.access_token);
|
|
52
|
+
return token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
53
|
+
}
|
|
54
|
+
function stringValue(value) {
|
|
55
|
+
return value === undefined || value === null ? '' : String(value).trim();
|
|
56
|
+
}
|
|
@@ -5,6 +5,5 @@ export function isExplicitGroupId(value) {
|
|
|
5
5
|
return (id.startsWith('group.') && id.includes('/'))
|
|
6
6
|
|| /^\d+\.[a-z0-9.-]+$/i.test(id)
|
|
7
7
|
|| /^grp_[a-z0-9_-]+$/i.test(id)
|
|
8
|
-
|| /^g-[a-z0-9_-]+(?:[.@][a-z0-9.-]+)?$/i.test(id)
|
|
9
|
-
|| /(?:^|\.)groupid\.pub$/i.test(id);
|
|
8
|
+
|| /^g-[a-z0-9_-]+(?:[.@][a-z0-9.-]+)?$/i.test(id);
|
|
10
9
|
}
|
package/dist/aun/msg/group.js
CHANGED
|
@@ -14,6 +14,7 @@ import { ipcQuery } from '../../ipc.js';
|
|
|
14
14
|
import { resolvePaths } from '../../paths.js';
|
|
15
15
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../../core/auth/agent-delegation.js';
|
|
16
16
|
import { normalizeAunMentionEntries } from './mention-schema.js';
|
|
17
|
+
import { downloadGroupFsBytes } from '../group-fs-download.js';
|
|
17
18
|
function buildGroupPayload(body) {
|
|
18
19
|
return body.mode === 'text' ? { type: 'text', text: body.text } : { ...body.payload };
|
|
19
20
|
}
|
|
@@ -130,17 +131,18 @@ async function daemonGroupSendResult(args, payload, runtimeContext, result) {
|
|
|
130
131
|
status: result.status,
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
+
const messageId = result.message_id
|
|
135
|
+
?? (typeof result.message?.message_id === 'string' ? result.message.message_id : undefined);
|
|
136
|
+
if (!result.group_id || !messageId) {
|
|
134
137
|
return { ok: false, error: 'daemon AUN group send returned no group message' };
|
|
135
138
|
}
|
|
136
|
-
const messageId = result.message_id
|
|
137
|
-
?? (typeof result.message.message_id === 'string' ? result.message.message_id : undefined);
|
|
138
139
|
if (messageId && !result.log_written) {
|
|
139
140
|
appendGroupSendOutboundLog(args, messageId, payload, runtimeContext, result.encrypt ?? resolveAunMessageEncrypt(args.encrypt));
|
|
140
141
|
}
|
|
141
142
|
return {
|
|
142
143
|
ok: true,
|
|
143
144
|
group_id: result.group_id || args.groupId,
|
|
145
|
+
message_id: messageId,
|
|
144
146
|
message: result.message,
|
|
145
147
|
event: result.event,
|
|
146
148
|
};
|
|
@@ -1055,20 +1057,7 @@ async function uploadGroupRulesBytes(client, groupId, bytes) {
|
|
|
1055
1057
|
}
|
|
1056
1058
|
async function readGroupRulesBytes(client, groupId) {
|
|
1057
1059
|
const remoteRef = `${groupId}:${GROUP_RULES_FILE_PATH}`;
|
|
1058
|
-
|
|
1059
|
-
if (!isRecord(ticket)) {
|
|
1060
|
-
throw new Error('group.fs.create_download_ticket returned invalid response');
|
|
1061
|
-
}
|
|
1062
|
-
const downloadUrl = stringField(ticket, ['download_url', 'url']);
|
|
1063
|
-
if (!downloadUrl) {
|
|
1064
|
-
throw new Error('group.fs.create_download_ticket did not return download_url');
|
|
1065
|
-
}
|
|
1066
|
-
const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
|
|
1067
|
-
const expectedSha = stringField(ticket, ['sha256']);
|
|
1068
|
-
if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
|
|
1069
|
-
throw new Error('download hash verification failed');
|
|
1070
|
-
}
|
|
1071
|
-
return bytes;
|
|
1060
|
+
return (await downloadGroupFsBytes(client, remoteRef)).bytes;
|
|
1072
1061
|
}
|
|
1073
1062
|
function decodeRulesText(bytes) {
|
|
1074
1063
|
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
@@ -1126,22 +1115,6 @@ function groupIndexEtagFromUpdate(result, groupId) {
|
|
|
1126
1115
|
function sha256Hex(bytes) {
|
|
1127
1116
|
return createHash('sha256').update(bytes).digest('hex');
|
|
1128
1117
|
}
|
|
1129
|
-
function bearerHeaders(client) {
|
|
1130
|
-
const token = accessTokenFromClient(client);
|
|
1131
|
-
return token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
1132
|
-
}
|
|
1133
|
-
function accessTokenFromClient(client) {
|
|
1134
|
-
const anyClient = client;
|
|
1135
|
-
if (typeof anyClient.getAccessToken === 'function') {
|
|
1136
|
-
const token = stringValue(anyClient.getAccessToken());
|
|
1137
|
-
if (token)
|
|
1138
|
-
return token;
|
|
1139
|
-
}
|
|
1140
|
-
return stringValue(anyClient.accessToken)
|
|
1141
|
-
|| stringValue(anyClient.access_token)
|
|
1142
|
-
|| stringValue(anyClient._identity?.access_token)
|
|
1143
|
-
|| stringValue(anyClient._sessionParams?.access_token);
|
|
1144
|
-
}
|
|
1145
1118
|
function stringField(obj, keys) {
|
|
1146
1119
|
for (const key of keys) {
|
|
1147
1120
|
const value = obj[key];
|
|
@@ -1189,6 +1162,11 @@ function rulesFileStatusFromError(error) {
|
|
|
1189
1162
|
if (tagged)
|
|
1190
1163
|
return tagged;
|
|
1191
1164
|
const haystack = errorHaystack(error);
|
|
1165
|
+
// A peer cache miss means signer discovery failed; it does not establish
|
|
1166
|
+
// that the group's rules file is absent.
|
|
1167
|
+
if (/peer\s+not\s+found\s+in\s+cache/i.test(haystack)) {
|
|
1168
|
+
return 'unreadable';
|
|
1169
|
+
}
|
|
1192
1170
|
if (/(forbidden|unauthorized|permission|denied|no_permission|permission_denied|无权限|权限|拒绝|\b401\b|\b403\b)/i.test(haystack)) {
|
|
1193
1171
|
return 'forbidden';
|
|
1194
1172
|
}
|
package/dist/aun/msg/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { msgSend, msgPull, msgAck, msgRecall, msgOnline, } from './p2p.js';
|
|
2
2
|
export { msgHistory, MAX_MSG_HISTORY_LIMIT } from './history.js';
|
|
3
3
|
export { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } from './group.js';
|
|
4
|
-
export { uploadFileAndBuildPayload } from './upload.js';
|
|
5
|
-
export { inferPayloadType, isValidPayloadType } from './payload-type.js';
|
|
4
|
+
export { uploadFileAndBuildPayload, inferPayloadType, isValidPayloadType } from './upload.js';
|
|
@@ -5,6 +5,7 @@ import { msgOnline } from './p2p.js';
|
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
6
|
import { isExplicitGroupId } from '../group-identity.js';
|
|
7
7
|
import { isValidAid } from '../aid/validation.js';
|
|
8
|
+
import { downloadGroupFsBytes } from '../group-fs-download.js';
|
|
8
9
|
import { groupBan, groupBanlist, groupCreate, groupDissolve, groupInfo, groupInvite, groupJoin, groupKick, groupLeave, groupList, groupMembers, groupOnline, groupPull, groupAck, groupResume, groupRules, groupRulesFileGet, groupRulesFilePublish, groupRulesFileSet, groupSetRole, groupSuspend, groupTransferOwner, groupUnban, groupUpdate, groupUpdateRules, } from './group.js';
|
|
9
10
|
/** Rules uploaded by a managed task are carried as bounded UTF-8 content. */
|
|
10
11
|
export const MAX_MANAGED_GROUP_RULES_BYTES = 4 * 1024;
|
|
@@ -562,6 +563,7 @@ const FS_VALUE_FLAGS = new Set([
|
|
|
562
563
|
'--as', '--format', '--max-bytes', '--head-bytes', '--name', '--pattern', '--type', '--node-type',
|
|
563
564
|
'--size', '--mtime', '--page', '--page-size',
|
|
564
565
|
]);
|
|
566
|
+
const FS_BOOLEAN_FLAGS = new Set(['-p', '--parents', '-r', '--recursive', '-f', '--force', '--overwrite']);
|
|
565
567
|
function failFs(code, reason) {
|
|
566
568
|
return { ok: false, code, reason };
|
|
567
569
|
}
|
|
@@ -587,29 +589,37 @@ function parseRemote(value, allowEmpty) {
|
|
|
587
589
|
}
|
|
588
590
|
if (remotePath === '' && allowEmpty)
|
|
589
591
|
return { groupId, remotePath: '' };
|
|
590
|
-
if (!remotePath.startsWith('/') || remotePath.length > MAX_REMOTE_PATH_LENGTH || /[\u0000-\u001f\u007f]/.test(remotePath)
|
|
592
|
+
if (!remotePath.startsWith('/') || remotePath.length > MAX_REMOTE_PATH_LENGTH || /[\u0000-\u001f\u007f]/.test(remotePath)
|
|
593
|
+
|| remotePath.split('/').includes('..')) {
|
|
591
594
|
return failFs('INVALID_PATH', 'remote group path must be a bounded absolute path');
|
|
592
595
|
}
|
|
593
596
|
return { groupId, remotePath };
|
|
594
597
|
}
|
|
595
|
-
/** Parse the daemon-owned, current-group
|
|
598
|
+
/** Parse the daemon-owned, current-group filesystem grammar. */
|
|
596
599
|
export function resolveManagedFsOperation(argv) {
|
|
597
600
|
const raw = argv[0] === 'fs' ? argv.slice(1) : argv;
|
|
598
|
-
const
|
|
599
|
-
if (!
|
|
600
|
-
return failFs('NOT_ALLOWED', `unsupported managed fs command: ${
|
|
601
|
+
const rawKind = raw[0];
|
|
602
|
+
if (!rawKind || !['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'mkdir', 'cp', 'mv', 'rm'].includes(rawKind)) {
|
|
603
|
+
return failFs('NOT_ALLOWED', `unsupported managed fs command: ${rawKind || '<missing>'}`);
|
|
601
604
|
}
|
|
602
605
|
const values = new Map();
|
|
606
|
+
const booleanFlags = new Set();
|
|
603
607
|
const positionals = [];
|
|
604
608
|
let follow = false;
|
|
605
609
|
for (let index = 1; index < raw.length; index++) {
|
|
606
610
|
const token = raw[index];
|
|
607
611
|
if (token === '-L' || token === '--follow') {
|
|
608
|
-
if (
|
|
612
|
+
if (rawKind !== 'stat')
|
|
609
613
|
return failFs('NOT_ALLOWED', `${token} is only allowed for stat`);
|
|
610
614
|
follow = true;
|
|
611
615
|
continue;
|
|
612
616
|
}
|
|
617
|
+
if (FS_BOOLEAN_FLAGS.has(token)) {
|
|
618
|
+
if (booleanFlags.has(token))
|
|
619
|
+
return failFs('INVALID_ARGUMENT', `${token} may only be specified once`);
|
|
620
|
+
booleanFlags.add(token);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
613
623
|
if (!token.startsWith('-')) {
|
|
614
624
|
positionals.push(token);
|
|
615
625
|
continue;
|
|
@@ -622,12 +632,10 @@ export function resolveManagedFsOperation(argv) {
|
|
|
622
632
|
if (values.has(token))
|
|
623
633
|
return failFs('INVALID_ARGUMENT', `${token} may only be specified once`);
|
|
624
634
|
const value = raw[++index];
|
|
625
|
-
if (!value || value.startsWith('
|
|
635
|
+
if (!value || value.startsWith('-'))
|
|
626
636
|
return failFs('INVALID_ARGUMENT', `${token} requires a value`);
|
|
627
637
|
values.set(token, value);
|
|
628
638
|
}
|
|
629
|
-
if (positionals.length !== 1)
|
|
630
|
-
return failFs('INVALID_ARGUMENT', `${kind} accepts exactly one remote group path`);
|
|
631
639
|
const actorAidValue = values.get('--as');
|
|
632
640
|
const actorAid = actorAidValue === undefined ? undefined : actorAidValue.replace(/^@/, '');
|
|
633
641
|
if (actorAid !== undefined && !isValidAid(actorAid))
|
|
@@ -635,22 +643,77 @@ export function resolveManagedFsOperation(argv) {
|
|
|
635
643
|
const format = values.get('--format');
|
|
636
644
|
if (format !== undefined && format !== 'json')
|
|
637
645
|
return failFs('INVALID_ARGUMENT', '--format only supports json');
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
646
|
+
const expected = rawKind === 'cp' || rawKind === 'mv' ? 2 : 1;
|
|
647
|
+
if (positionals.length !== expected)
|
|
648
|
+
return failFs('INVALID_ARGUMENT', `${rawKind} accepts ${expected} positional argument${expected === 1 ? '' : 's'}`);
|
|
649
|
+
const allowedValues = {
|
|
650
|
+
ls: new Set(['--as', '--format']), stat: new Set(['--as', '--format']), lstat: new Set(['--as', '--format']),
|
|
642
651
|
cat: new Set(['--as', '--format', '--max-bytes', '--head-bytes']),
|
|
643
652
|
find: new Set(['--as', '--format', '--name', '--pattern', '--type', '--node-type', '--size', '--mtime', '--page', '--page-size']),
|
|
644
|
-
df: new Set(['--as', '--format']),
|
|
653
|
+
df: new Set(['--as', '--format']), mkdir: new Set(['--as', '--format']), cp: new Set(['--as', '--format']),
|
|
654
|
+
mv: new Set(['--as', '--format']), rm: new Set(['--as', '--format']),
|
|
645
655
|
};
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
656
|
+
const allowedBoolean = {
|
|
657
|
+
ls: new Set(), stat: new Set(), lstat: new Set(), cat: new Set(), find: new Set(), df: new Set(),
|
|
658
|
+
mkdir: new Set(['-p', '--parents']), cp: new Set(['-r', '--recursive', '-f', '--force', '--overwrite']),
|
|
659
|
+
mv: new Set(['-f', '--force', '--overwrite']), rm: new Set(['-r', '--recursive', '-f', '--force', '--overwrite']),
|
|
660
|
+
};
|
|
661
|
+
for (const flag of values.keys())
|
|
662
|
+
if (!allowedValues[rawKind].has(flag))
|
|
663
|
+
return failFs('NOT_ALLOWED', `${flag} is not allowed for managed fs ${rawKind}`);
|
|
664
|
+
for (const flag of booleanFlags)
|
|
665
|
+
if (!allowedBoolean[rawKind].has(flag))
|
|
666
|
+
return failFs('NOT_ALLOWED', `${flag} is not allowed for managed fs ${rawKind}`);
|
|
667
|
+
const overwrite = booleanFlags.has('-f') || booleanFlags.has('--force') || booleanFlags.has('--overwrite');
|
|
668
|
+
const recursive = booleanFlags.has('-r') || booleanFlags.has('--recursive');
|
|
669
|
+
const remoteOrError = (value, allowEmpty = false) => parseRemote(value, allowEmpty);
|
|
670
|
+
if (rawKind === 'cp') {
|
|
671
|
+
const destination = remoteOrError(positionals[1]);
|
|
672
|
+
if (!('groupId' in destination))
|
|
673
|
+
return destination;
|
|
674
|
+
const source = positionals[0].includes(':') ? remoteOrError(positionals[0]) : undefined;
|
|
675
|
+
if (source && !('groupId' in source))
|
|
676
|
+
return source;
|
|
677
|
+
if (source && source.groupId !== destination.groupId)
|
|
678
|
+
return failFs('INVALID_GROUP', 'managed cp cannot cross groups');
|
|
679
|
+
const kind = source ? 'copy' : 'upload';
|
|
680
|
+
return { ok: true, command: {
|
|
681
|
+
kind, operationId: `ec.fs.${kind}`, canonicalArgv: ['fs', 'cp', positionals[0], positionals[1], ...(actorAid ? ['--as', actorAid] : [])],
|
|
682
|
+
actorAid, groupId: destination.groupId, remotePath: destination.remotePath, destinationPath: destination.remotePath,
|
|
683
|
+
...(source ? { remoteSourcePath: source.remotePath } : { localPath: positionals[0] }), overwrite, recursive,
|
|
684
|
+
} };
|
|
685
|
+
}
|
|
686
|
+
if (rawKind === 'mv') {
|
|
687
|
+
const source = remoteOrError(positionals[0]);
|
|
688
|
+
const destination = remoteOrError(positionals[1]);
|
|
689
|
+
if (!('groupId' in source))
|
|
690
|
+
return source;
|
|
691
|
+
if (!('groupId' in destination))
|
|
692
|
+
return destination;
|
|
693
|
+
if (source.groupId !== destination.groupId)
|
|
694
|
+
return failFs('INVALID_GROUP', 'managed mv cannot cross groups');
|
|
695
|
+
return { ok: true, command: {
|
|
696
|
+
kind: 'move', operationId: 'ec.fs.move', canonicalArgv: ['fs', 'mv', positionals[0], positionals[1], ...(actorAid ? ['--as', actorAid] : [])],
|
|
697
|
+
actorAid, groupId: destination.groupId, remotePath: destination.remotePath, destinationPath: destination.remotePath,
|
|
698
|
+
remoteSourcePath: source.remotePath, overwrite,
|
|
699
|
+
} };
|
|
700
|
+
}
|
|
701
|
+
const remote = remoteOrError(positionals[0], rawKind === 'df');
|
|
651
702
|
if (!('groupId' in remote))
|
|
652
703
|
return remote;
|
|
653
|
-
|
|
704
|
+
if (rawKind === 'mkdir')
|
|
705
|
+
return { ok: true, command: {
|
|
706
|
+
kind: 'mkdir', operationId: 'ec.fs.mkdir', canonicalArgv: ['fs', 'mkdir', positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
707
|
+
actorAid, groupId: remote.groupId, remotePath: remote.remotePath, parents: booleanFlags.has('-p') || booleanFlags.has('--parents'),
|
|
708
|
+
} };
|
|
709
|
+
if (rawKind === 'rm' && remote.remotePath === '/')
|
|
710
|
+
return failFs('INVALID_PATH', 'managed rm cannot target the group root');
|
|
711
|
+
if (rawKind === 'rm')
|
|
712
|
+
return { ok: true, command: {
|
|
713
|
+
kind: 'remove', operationId: 'ec.fs.remove', canonicalArgv: ['fs', 'rm', positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
714
|
+
actorAid, groupId: remote.groupId, remotePath: remote.remotePath, recursive, overwrite,
|
|
715
|
+
} };
|
|
716
|
+
const effectiveKind = rawKind === 'stat' && !follow ? 'lstat' : rawKind;
|
|
654
717
|
const maxBytes = positiveInteger(values.get('--max-bytes'), '--max-bytes', MAX_MANAGED_FS_CAT_BYTES);
|
|
655
718
|
if (typeof maxBytes !== 'number' && maxBytes !== undefined)
|
|
656
719
|
return maxBytes;
|
|
@@ -664,28 +727,13 @@ export function resolveManagedFsOperation(argv) {
|
|
|
664
727
|
if (typeof pageSize !== 'number' && pageSize !== undefined)
|
|
665
728
|
return pageSize;
|
|
666
729
|
const nodeType = values.get('--type') ?? values.get('--node-type');
|
|
667
|
-
if (nodeType !== undefined && !['f', 'd', 'l'].includes(nodeType))
|
|
730
|
+
if (nodeType !== undefined && !['f', 'd', 'l'].includes(nodeType))
|
|
668
731
|
return failFs('INVALID_ARGUMENT', '--type only supports f|d|l');
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
operationId: `ec.fs.${effectiveKind}`,
|
|
675
|
-
canonicalArgv: ['fs', effectiveKind, `${remote.groupId}:${remote.remotePath}`, ...(actorAid ? ['--as', actorAid] : [])],
|
|
676
|
-
actorAid,
|
|
677
|
-
groupId: remote.groupId,
|
|
678
|
-
remotePath: remote.remotePath,
|
|
679
|
-
maxBytes,
|
|
680
|
-
headBytes,
|
|
681
|
-
name: values.get('--name') ?? values.get('--pattern'),
|
|
682
|
-
nodeType,
|
|
683
|
-
size: values.get('--size'),
|
|
684
|
-
mtime: values.get('--mtime'),
|
|
685
|
-
page,
|
|
686
|
-
pageSize,
|
|
687
|
-
},
|
|
688
|
-
};
|
|
732
|
+
return { ok: true, command: {
|
|
733
|
+
kind: effectiveKind, operationId: `ec.fs.${effectiveKind}`, canonicalArgv: ['fs', effectiveKind, positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
734
|
+
actorAid, groupId: remote.groupId, remotePath: remote.remotePath, maxBytes, headBytes,
|
|
735
|
+
name: values.get('--name') ?? values.get('--pattern'), nodeType, size: values.get('--size'), mtime: values.get('--mtime'), page, pageSize,
|
|
736
|
+
} };
|
|
689
737
|
}
|
|
690
738
|
function numberFrom(value, keys) {
|
|
691
739
|
for (const key of keys) {
|
|
@@ -695,18 +743,6 @@ function numberFrom(value, keys) {
|
|
|
695
743
|
}
|
|
696
744
|
return undefined;
|
|
697
745
|
}
|
|
698
|
-
function bytesFromDownload(value) {
|
|
699
|
-
const candidate = value?.bytes ?? value?.data ?? value?.content ?? value?.blob ?? value;
|
|
700
|
-
if (candidate instanceof Uint8Array)
|
|
701
|
-
return candidate;
|
|
702
|
-
if (Buffer.isBuffer(candidate))
|
|
703
|
-
return new Uint8Array(candidate);
|
|
704
|
-
if (typeof candidate === 'string')
|
|
705
|
-
return new Uint8Array(Buffer.from(candidate, 'base64'));
|
|
706
|
-
if (Array.isArray(candidate))
|
|
707
|
-
return new Uint8Array(candidate);
|
|
708
|
-
throw new ManagedFsOperationError('FS_DOWNLOAD_INVALID', 'group filesystem returned an unsupported blob');
|
|
709
|
-
}
|
|
710
746
|
async function withClient(actorAid, fn) {
|
|
711
747
|
const store = await getAidStore({ slotId: SLOT.cli, aunPath: resolveRoot() });
|
|
712
748
|
let client;
|
|
@@ -728,7 +764,7 @@ async function withClient(actorAid, fn) {
|
|
|
728
764
|
catch { /* best effort */ }
|
|
729
765
|
}
|
|
730
766
|
}
|
|
731
|
-
/** Execute a parsed
|
|
767
|
+
/** Execute a parsed operation against the current group using daemon-owned credentials. */
|
|
732
768
|
export async function executeManagedFsOperation(command) {
|
|
733
769
|
if (!command.actorAid)
|
|
734
770
|
throw new ManagedFsOperationError('INVALID_ACTOR', 'managed fs actor is required');
|
|
@@ -736,6 +772,31 @@ export async function executeManagedFsOperation(command) {
|
|
|
736
772
|
try {
|
|
737
773
|
return await withClient(command.actorAid, async (client) => {
|
|
738
774
|
switch (command.kind) {
|
|
775
|
+
case 'mkdir':
|
|
776
|
+
return { ok: true, command: 'mkdir', path: ref, result: await client.group.fs.mkdir(ref, { parents: command.parents }) };
|
|
777
|
+
case 'upload': {
|
|
778
|
+
if (!command.localPath)
|
|
779
|
+
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed upload source is required');
|
|
780
|
+
return { ok: true, command: 'upload', path: ref, localPath: command.localPath,
|
|
781
|
+
result: await client.group.fs.cp(command.localPath, ref, { parents: true, overwrite: command.overwrite, force: command.overwrite }) };
|
|
782
|
+
}
|
|
783
|
+
case 'copy': {
|
|
784
|
+
if (!command.remoteSourcePath)
|
|
785
|
+
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed copy source is required');
|
|
786
|
+
const source = `${command.groupId}:${command.remoteSourcePath}`;
|
|
787
|
+
return { ok: true, command: 'copy', src: source, path: ref,
|
|
788
|
+
result: await client.group.fs.cp(source, ref, { recursive: command.recursive, overwrite: command.overwrite, force: command.overwrite }) };
|
|
789
|
+
}
|
|
790
|
+
case 'move': {
|
|
791
|
+
if (!command.remoteSourcePath)
|
|
792
|
+
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed move source is required');
|
|
793
|
+
const source = `${command.groupId}:${command.remoteSourcePath}`;
|
|
794
|
+
return { ok: true, command: 'move', src: source, path: ref,
|
|
795
|
+
result: await client.group.fs.mv(source, ref, { overwrite: command.overwrite, force: command.overwrite }) };
|
|
796
|
+
}
|
|
797
|
+
case 'remove':
|
|
798
|
+
return { ok: true, command: 'remove', path: ref,
|
|
799
|
+
result: await client.group.fs.rm(ref, { recursive: command.recursive, force: command.overwrite }) };
|
|
739
800
|
case 'ls': {
|
|
740
801
|
const raw = await client.group.fs.ls(ref);
|
|
741
802
|
const value = raw;
|
|
@@ -770,7 +831,7 @@ export async function executeManagedFsOperation(command) {
|
|
|
770
831
|
throw new ManagedFsOperationError('FS_SIZE_REQUIRED', 'group file size is required before managed cat');
|
|
771
832
|
if (size > maxBytes)
|
|
772
833
|
throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
|
|
773
|
-
const bytes =
|
|
834
|
+
const { bytes } = await downloadGroupFsBytes(client, ref);
|
|
774
835
|
if (bytes.byteLength > maxBytes)
|
|
775
836
|
throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
|
|
776
837
|
const headBytes = Math.min(command.headBytes ?? 256, MAX_MANAGED_FS_HEAD_BYTES, bytes.byteLength);
|
package/dist/aun/msg/upload.js
CHANGED
|
@@ -2,7 +2,22 @@ import crypto from 'crypto';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { guessMime, formatSize } from '../../utils/media-cache.js';
|
|
5
|
-
|
|
5
|
+
const EXT_TYPE_MAP = {
|
|
6
|
+
'.png': 'image', '.jpg': 'image', '.jpeg': 'image',
|
|
7
|
+
'.gif': 'image', '.webp': 'image', '.svg': 'image',
|
|
8
|
+
'.bmp': 'image', '.heic': 'image', '.heif': 'image',
|
|
9
|
+
'.mp4': 'video', '.mov': 'video', '.webm': 'video',
|
|
10
|
+
'.avi': 'video', '.mkv': 'video', '.m4v': 'video',
|
|
11
|
+
'.opus': 'voice', '.mp3': 'voice', '.aac': 'voice',
|
|
12
|
+
'.m4a': 'voice', '.wav': 'voice', '.flac': 'voice', '.ogg': 'voice',
|
|
13
|
+
};
|
|
14
|
+
export function inferPayloadType(filename) {
|
|
15
|
+
const ext = path.extname(filename).toLowerCase();
|
|
16
|
+
return EXT_TYPE_MAP[ext] ?? 'file';
|
|
17
|
+
}
|
|
18
|
+
export function isValidPayloadType(value) {
|
|
19
|
+
return value === 'image' || value === 'video' || value === 'voice' || value === 'file';
|
|
20
|
+
}
|
|
6
21
|
/** 单次上传最大大小(与 daemon sendFile 一致)。 */
|
|
7
22
|
export const MAX_AUN_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
|
8
23
|
/** server 端 inline 上限错误的识别特征。优先匹配错误消息里的“inline 上限”字样。 */
|
package/dist/aun/outbox.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { agentOutboxPath } from '../paths.js';
|
|
3
3
|
import { atomicRead, atomicWrite } from '../utils/atomic-write.js';
|
|
4
|
+
import { isDeliveryTarget, isDeliveryTargetForChannel, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
4
5
|
const MAX_ENTRIES_PER_AID = 20;
|
|
5
6
|
const DEFAULT_TTL = 300_000; // 5 minutes
|
|
6
7
|
function outboxFile(aid) {
|
|
@@ -16,6 +17,9 @@ function isExpired(entry) {
|
|
|
16
17
|
return false;
|
|
17
18
|
return Date.now() - entry.ts > entry.ttl;
|
|
18
19
|
}
|
|
20
|
+
export function isDeliveryForChannel(value, channelId) {
|
|
21
|
+
return isDeliveryTargetForChannel(value, channelId);
|
|
22
|
+
}
|
|
19
23
|
function readEntries(aid) {
|
|
20
24
|
const file = outboxFile(aid);
|
|
21
25
|
try {
|
|
@@ -43,17 +47,32 @@ function writeEntries(aid, entries) {
|
|
|
43
47
|
atomicWrite(file, content);
|
|
44
48
|
}
|
|
45
49
|
export function enqueue(aid, opts) {
|
|
46
|
-
const
|
|
50
|
+
const delivery = opts.delivery;
|
|
51
|
+
if (!isDeliveryForChannel(delivery, opts.channelId)) {
|
|
52
|
+
const code = isDeliveryTarget(delivery) ? 'AUN_OUTBOUND_ROUTE_MISMATCH' : 'AUN_OUTBOUND_ROUTE_REQUIRED';
|
|
53
|
+
throw Object.assign(new Error(`invalid AUN outbox delivery route for channelId=${opts.channelId}`), { code });
|
|
54
|
+
}
|
|
55
|
+
let existingEntries = readEntries(aid);
|
|
47
56
|
if (opts.dedupeKey) {
|
|
48
57
|
const existing = existingEntries.find(entry => entry.dedupeKey === opts.dedupeKey && !isExpired(entry));
|
|
49
|
-
if (existing
|
|
58
|
+
if (existing
|
|
59
|
+
&& existing.channelId === opts.channelId
|
|
60
|
+
&& isDeliveryForChannel(existing.delivery, existing.channelId)
|
|
61
|
+
&& sameDeliveryTarget(existing.delivery, delivery)) {
|
|
50
62
|
return existing;
|
|
63
|
+
}
|
|
64
|
+
if (existing) {
|
|
65
|
+
// A pre-route entry must not block corrected code from persisting the
|
|
66
|
+
// same logical operation with an explicit delivery target.
|
|
67
|
+
existingEntries = existingEntries.filter(entry => entry.id !== existing.id);
|
|
68
|
+
}
|
|
51
69
|
}
|
|
52
70
|
const entry = {
|
|
53
71
|
id: generateId(),
|
|
54
72
|
ts: Date.now(),
|
|
55
73
|
aid,
|
|
56
74
|
channelId: opts.channelId,
|
|
75
|
+
delivery,
|
|
57
76
|
dedupeKey: opts.dedupeKey,
|
|
58
77
|
critical: opts.critical,
|
|
59
78
|
type: opts.type,
|
|
@@ -123,6 +142,9 @@ export function cleanup(aid) {
|
|
|
123
142
|
// duplicate sends while allowing a later drain trigger to pick up entries
|
|
124
143
|
// enqueued during an active pass.
|
|
125
144
|
const activeDrainTails = new Map();
|
|
145
|
+
function hasRouteForChannel(entry) {
|
|
146
|
+
return isDeliveryForChannel(entry.delivery, entry.channelId);
|
|
147
|
+
}
|
|
126
148
|
async function drainOnce(aid, sender) {
|
|
127
149
|
const entries = readEntries(aid);
|
|
128
150
|
if (entries.length === 0)
|
|
@@ -137,6 +159,13 @@ async function drainOnce(aid, sender) {
|
|
|
137
159
|
expired++;
|
|
138
160
|
continue;
|
|
139
161
|
}
|
|
162
|
+
if (!hasRouteForChannel(entry)) {
|
|
163
|
+
// A route is security-sensitive metadata. An entry whose groupId does
|
|
164
|
+
// not match its address cannot be repaired from channelId, so remove it
|
|
165
|
+
// instead of retrying the same malformed send forever.
|
|
166
|
+
expired++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
140
169
|
try {
|
|
141
170
|
entry.attempts = (entry.attempts ?? 0) + 1;
|
|
142
171
|
const ok = await sender(entry);
|