@hellolynk-ai/trae-work-mcp 0.0.0-stage → 0.4.95
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/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/integrations/trae-work/package.json +11 -0
- package/integrations/trae-work/runtime/neutral-host-bootstrap.mjs +38 -0
- package/integrations/trae-work/runtime/neutral-host-release-entry.mjs +16 -0
- package/integrations/trae-work/runtime/trae-work-bootstrap.mjs +30 -0
- package/integrations/trae-work/server/dynamic-ui-contract.mjs +376 -0
- package/integrations/trae-work/server/host-lifecycle.mjs +128 -0
- package/integrations/trae-work/server/mcp-proxy.mjs +181 -0
- package/integrations/trae-work/server/mcp-server.mjs +784 -0
- package/integrations/trae-work/server/native-task-cdp.mjs +241 -0
- package/integrations/trae-work/server/native-task-worker.mjs +199 -0
- package/integrations/trae-work/server/neutral-host-adapter.mjs +725 -0
- package/integrations/trae-work/server/trae-neutral-production.mjs +97 -0
- package/integrations/trae-work/server/trae-ui-launch.mjs +182 -0
- package/integrations/trae-work/server/ui-action-gate.mjs +122 -0
- package/integrations/trae-work/skills/hellolynk-inbox/SKILL.md +9 -0
- package/package.json +61 -4
- package/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/runtime/connect-activate.mjs +837 -0
- package/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/runtime/core/hellolynk-client-account.mjs +180 -0
- package/runtime/core/hellolynk-client-core.mjs +770 -0
- package/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/runtime/core/hellolynk-client-update.mjs +453 -0
- package/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/runtime/core/hellolynk-host.mjs +2374 -0
- package/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/runtime/neutral-host-bootstrap.mjs +38 -0
- package/runtime/neutral-host-release-entry.mjs +16 -0
- package/runtime/neutral-host-release-loader.mjs +156 -0
- package/runtime/neutral-host-release-manifest.json +394 -0
- package/runtime/neutral-host-release-pin.json +4 -0
- package/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/runtime/platform/windows/credential-store.ps1 +844 -0
- package/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/runtime/trae-work-bootstrap.mjs +30 -0
- package/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/runtime/workbench/contract.mjs +1314 -0
- package/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/runtime/workbench/host-capabilities.mjs +80 -0
- package/runtime/workbench/i18n.mjs +1127 -0
- package/runtime/workbench/loopback-surface.mjs +740 -0
- package/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/runtime/workbench/service.mjs +485 -0
- package/runtime/workbench/state.mjs +421 -0
- package/runtime/workbench/transports.mjs +171 -0
- package/runtime/workbench/ui/app.mjs +6120 -0
- package/runtime/workbench/ui/connection-form.mjs +155 -0
- package/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/runtime/workbench/ui/http-transport.mjs +247 -0
- package/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/runtime/workbench/ui/icons/close.svg +3 -0
- package/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/runtime/workbench/ui/icons/search.svg +3 -0
- package/runtime/workbench/ui/icons/security.svg +3 -0
- package/runtime/workbench/ui/icons/settings.svg +3 -0
- package/runtime/workbench/ui/index.html +139 -0
- package/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/runtime/workbench/ui/styles.css +686 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
- package/tools/hellolynk-install-client-skills.mjs +1211 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { queryArgs, optionalTarget } from "../../../runtime/core/hellolynk-native-addressing-tools.cjs";
|
|
2
|
+
import { captureHelloLynkModelNotices } from "../../../runtime/core/hellolynk-model-notices.cjs";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { HELLOLYNK_HOST_CAPABILITY_CONTRACT } from "../../../runtime/core/hellolynk-host-auth.mjs";
|
|
6
|
+
import {
|
|
7
|
+
createHostProtectedStore,
|
|
8
|
+
} from "../../../runtime/core/hellolynk-host-protected-store.mjs";
|
|
9
|
+
import {
|
|
10
|
+
discoverHelloLynkNeutralHost,
|
|
11
|
+
HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT,
|
|
12
|
+
requestHelloLynkNeutralHost,
|
|
13
|
+
requestHelloLynkNeutralHostHandover,
|
|
14
|
+
} from "../../../runtime/core/hellolynk-neutral-host-process.mjs";
|
|
15
|
+
import { HELLOLYNK_HOST_PATHS } from "../../../runtime/core/hellolynk-host.mjs";
|
|
16
|
+
import {
|
|
17
|
+
validateWorkbenchCommand,
|
|
18
|
+
validateWorkbenchCommandResult,
|
|
19
|
+
} from "../../../runtime/workbench/contract.mjs";
|
|
20
|
+
|
|
21
|
+
export const HELLOLYNK_TRAE_WORK_NEUTRAL_ADAPTER_CONTRACT = "hellolynk-trae-work-neutral-adapter-v2";
|
|
22
|
+
export const TRAE_WORK_HEARTBEAT_MS = 10_000;
|
|
23
|
+
export const TRAE_WORK_NEUTRAL_EXACT_SCOPES = Object.freeze([
|
|
24
|
+
"app-data:authorized-messaging",
|
|
25
|
+
"app-data:changes-read",
|
|
26
|
+
"app-data:contact-requests-read",
|
|
27
|
+
"app-data:contacts-manage",
|
|
28
|
+
"app-data:contacts-read",
|
|
29
|
+
"app-data:human-send-request",
|
|
30
|
+
"app-data:inbox-manage",
|
|
31
|
+
"app-data:inbox-read",
|
|
32
|
+
"app-data:outbox-read",
|
|
33
|
+
"app-data:outbox-retry",
|
|
34
|
+
"attachments:manage",
|
|
35
|
+
"status:read",
|
|
36
|
+
"surface:human-ui",
|
|
37
|
+
"ui:launch",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/;
|
|
41
|
+
const ACTION_GRANT = /^[A-Za-z0-9._~-]{32,512}$/;
|
|
42
|
+
const FROZEN_WORKBENCH_ACTION_GRANT_PATH = "/host/v1/app/workbench/action-grants";
|
|
43
|
+
if (HELLOLYNK_HOST_PATHS.appWorkbenchActionGrant !== undefined
|
|
44
|
+
&& HELLOLYNK_HOST_PATHS.appWorkbenchActionGrant !== FROZEN_WORKBENCH_ACTION_GRANT_PATH) {
|
|
45
|
+
fail("neutral_host_workbench_action_grant_path_mismatch");
|
|
46
|
+
}
|
|
47
|
+
const WORKBENCH_ACTION_GRANT_PATH = HELLOLYNK_HOST_PATHS.appWorkbenchActionGrant
|
|
48
|
+
?? FROZEN_WORKBENCH_ACTION_GRANT_PATH;
|
|
49
|
+
const SAFE_DISCOVERY_ERRORS = new Set([
|
|
50
|
+
"ENOENT", "neutral_host_discovery_stale", "neutral_host_discovery_generation_mismatch",
|
|
51
|
+
]);
|
|
52
|
+
// Another client, or a newer release, is starting the Host: wait for it instead of failing.
|
|
53
|
+
const LAUNCH_YIELD_ERRORS = new Set(["neutral_host_already_running", "neutral_host_release_superseded"]);
|
|
54
|
+
// A release handover is on hold: wait for the newer Host like already_running, report "switching" only if none appears.
|
|
55
|
+
const RELEASE_SUPERSEDED = "neutral_host_release_superseded";
|
|
56
|
+
const NON_TERMINAL_HOST_APP_ERRORS = new Set([
|
|
57
|
+
"host_app_authorization_required", "host_app_record_not_found", "host_app_state_conflict",
|
|
58
|
+
"host_app_reply_target_deleted",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export function createTraeWorkProtectedBindingProvider(options = {}) {
|
|
62
|
+
const store = options.protectedStore || createHostProtectedStore({
|
|
63
|
+
...(options.storeRoot ? { root: options.storeRoot } : {}),
|
|
64
|
+
...(options.installationStore ? { installationStore: options.installationStore } : {}),
|
|
65
|
+
});
|
|
66
|
+
return () => store.resolveTraeWorkNeutralBinding(options.bindingQuery || {});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function createTraeWorkNeutralHostAdapter(options = {}) {
|
|
70
|
+
return createRestrictedNeutralHostAdapter({
|
|
71
|
+
...options,
|
|
72
|
+
adapterContract: HELLOLYNK_TRAE_WORK_NEUTRAL_ADAPTER_CONTRACT,
|
|
73
|
+
adapterInstancePrefix: "trae-work",
|
|
74
|
+
validateBinding: validateTraeWorkProtectedBinding,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function createRestrictedNeutralHostAdapter(options = {}) {
|
|
79
|
+
if (typeof options.capabilityProvider !== "function") {
|
|
80
|
+
throw codeError("neutral_capability_provider_required");
|
|
81
|
+
}
|
|
82
|
+
if (typeof options.validateBinding !== "function") {
|
|
83
|
+
throw codeError("neutral_capability_binding_validator_required");
|
|
84
|
+
}
|
|
85
|
+
const adapterContract = safeId(options.adapterContract, "neutral_adapter_contract_invalid");
|
|
86
|
+
const adapterInstancePrefix = safeId(options.adapterInstancePrefix, "neutral_adapter_prefix_invalid");
|
|
87
|
+
const binding = options.validateBinding(await options.capabilityProvider());
|
|
88
|
+
const discover = options.discoverHost || discoverHelloLynkNeutralHost;
|
|
89
|
+
const request = options.requestHost || requestHelloLynkNeutralHost;
|
|
90
|
+
const requestHandover = options.requestHostHandover || requestHelloLynkNeutralHostHandover;
|
|
91
|
+
const discoveryInput = publicBinding(binding);
|
|
92
|
+
let discovery = await safeDiscover(discover, discoveryInput);
|
|
93
|
+
// A Host of an earlier release is asked to make room, and this client's release starts in its place.
|
|
94
|
+
if (discovery && typeof options.hostLauncher === "function"
|
|
95
|
+
&& await earlierHostLeft(requestHandover, binding, integer(options.handoverWaitMs, 5_000, 0, 30_000))) {
|
|
96
|
+
discovery = await safeDiscover(discover, discoveryInput);
|
|
97
|
+
}
|
|
98
|
+
let startedHost = false;
|
|
99
|
+
if (!discovery) {
|
|
100
|
+
if (typeof options.hostLauncher !== "function") throw codeError("neutral_host_launcher_unavailable");
|
|
101
|
+
// Another client may have started the shared Host a moment ago: wait for it instead of failing.
|
|
102
|
+
let superseded = false;
|
|
103
|
+
try { await options.hostLauncher(launchBinding(binding)); startedHost = true; }
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!LAUNCH_YIELD_ERRORS.has(error?.code)) throw error;
|
|
106
|
+
superseded = error.code === RELEASE_SUPERSEDED;
|
|
107
|
+
}
|
|
108
|
+
discovery = await waitForDiscovery(discover, discoveryInput, options);
|
|
109
|
+
if (!discovery && superseded) throw codeError(RELEASE_SUPERSEDED);
|
|
110
|
+
}
|
|
111
|
+
if (!discovery) throw codeError("neutral_host_not_found");
|
|
112
|
+
assertDiscovery(discovery, binding);
|
|
113
|
+
|
|
114
|
+
const adapterInstanceId = safeId(
|
|
115
|
+
options.adapterInstanceId || `${adapterInstancePrefix}-${process.pid}-${crypto.randomBytes(12).toString("hex")}`,
|
|
116
|
+
"neutral_host_adapter_instance_invalid",
|
|
117
|
+
);
|
|
118
|
+
const callRaw = async (method, pathname, body = null, requestOptions = {}) => captureHelloLynkModelNotices(await request({
|
|
119
|
+
discovery,
|
|
120
|
+
capability: binding.capability,
|
|
121
|
+
method,
|
|
122
|
+
pathname,
|
|
123
|
+
body,
|
|
124
|
+
...requestOptions,
|
|
125
|
+
}));
|
|
126
|
+
await requireOk(await callRaw("POST", HELLOLYNK_HOST_PATHS.attachmentAttach, { adapterInstanceId }));
|
|
127
|
+
|
|
128
|
+
let closed = false;
|
|
129
|
+
let deadCode = null;
|
|
130
|
+
let inboxActivation = null;
|
|
131
|
+
let heartbeatBusy = false;
|
|
132
|
+
const heartbeatMs = integer(options.heartbeatMs, TRAE_WORK_HEARTBEAT_MS, 10, 30_000);
|
|
133
|
+
const heartbeat = setInterval(async () => {
|
|
134
|
+
if (closed || deadCode || heartbeatBusy) return;
|
|
135
|
+
heartbeatBusy = true;
|
|
136
|
+
try {
|
|
137
|
+
await requireOk(await callRaw("POST", HELLOLYNK_HOST_PATHS.attachmentHeartbeat, { adapterInstanceId }));
|
|
138
|
+
} catch {
|
|
139
|
+
deadCode = "neutral_host_heartbeat_failed";
|
|
140
|
+
clearInterval(heartbeat);
|
|
141
|
+
} finally {
|
|
142
|
+
heartbeatBusy = false;
|
|
143
|
+
}
|
|
144
|
+
}, heartbeatMs);
|
|
145
|
+
heartbeat.unref?.();
|
|
146
|
+
|
|
147
|
+
function assertLive() {
|
|
148
|
+
if (closed) throw codeError("neutral_host_adapter_closed");
|
|
149
|
+
if (deadCode) throw codeError(deadCode);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// A Host without the native inbox routes is of an earlier release: ask it to make room, then fail as before.
|
|
153
|
+
async function askHostToHandOver() {
|
|
154
|
+
try {
|
|
155
|
+
await requestHandover({ runtimeRoot: binding.runtimeRoot, hostInstallationId: binding.hostInstallationId,
|
|
156
|
+
publishedAt: new Date().toISOString(), waitMs: 0 });
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
async function call(method, pathname, body = null, accepted = false, requestOptions = {}) {
|
|
160
|
+
assertLive();
|
|
161
|
+
try {
|
|
162
|
+
const result = await callRaw(method, pathname, body, requestOptions);
|
|
163
|
+
if (([HELLOLYNK_HOST_PATHS.appAuthorizedInboxPending,
|
|
164
|
+
HELLOLYNK_HOST_PATHS.appAuthorizedInboxClaim].includes(pathname) || pathname.startsWith("/host/v1/app/authorized-inbox/batch/"))
|
|
165
|
+
&& (result?.body?.error === "host_route_not_found" || (result?.status === 404 && !result?.body?.error))) {
|
|
166
|
+
await askHostToHandOver();
|
|
167
|
+
throw codeError("host_native_inbox_upgrade_required");
|
|
168
|
+
}
|
|
169
|
+
return accepted ? requireAccepted(result) : requireOk(result);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (isTerminalHostError(error)) {
|
|
172
|
+
deadCode = safeCode(error);
|
|
173
|
+
clearInterval(heartbeat);
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function close() {
|
|
180
|
+
if (closed) return;
|
|
181
|
+
closed = true;
|
|
182
|
+
clearInterval(heartbeat);
|
|
183
|
+
if (!deadCode) {
|
|
184
|
+
await callRaw("POST", HELLOLYNK_HOST_PATHS.attachmentDetach, { adapterInstanceId }).catch(() => {});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
contract: adapterContract,
|
|
190
|
+
mode: "neutral",
|
|
191
|
+
startedHost,
|
|
192
|
+
get state() { return closed ? "closed" : deadCode ? "dead" : "attached"; },
|
|
193
|
+
async getNativeTaskScope() {
|
|
194
|
+
assertLive();
|
|
195
|
+
return Object.freeze({ runtimeRoot: binding.runtimeRoot, adapterId: binding.adapterId,
|
|
196
|
+
region: binding.region, principalId: binding.principalId, delegateId: binding.delegateId });
|
|
197
|
+
},
|
|
198
|
+
async hostStatus() {
|
|
199
|
+
const result = await call("GET", HELLOLYNK_HOST_PATHS.status);
|
|
200
|
+
if (result.status !== "running") throw codeError("neutral_host_not_running");
|
|
201
|
+
return Object.freeze({ ok: true, configured: true, status: "attached" });
|
|
202
|
+
},
|
|
203
|
+
async requestHumanSend(input) {
|
|
204
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appHumanSendRequests, { adapterInstanceId, request: input }, true);
|
|
205
|
+
},
|
|
206
|
+
async getHumanSendStatus(actionId) {
|
|
207
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appHumanSendRequestStatus, {
|
|
208
|
+
adapterInstanceId,
|
|
209
|
+
request: { actionId },
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
async readInbox() {
|
|
213
|
+
return call("GET", HELLOLYNK_HOST_PATHS.appInboxMessages);
|
|
214
|
+
},
|
|
215
|
+
async modelInbox(input) { return call("POST", HELLOLYNK_HOST_PATHS.appModelInbox, input); },
|
|
216
|
+
async readUnreadCount() {
|
|
217
|
+
return call("GET", HELLOLYNK_HOST_PATHS.appInboxUnreadCount);
|
|
218
|
+
},
|
|
219
|
+
async markInboxMessageRead(localMessageId) {
|
|
220
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appInboxMarkRead, { localMessageId });
|
|
221
|
+
},
|
|
222
|
+
async deleteReadInboxMessages(localMessageIds) {
|
|
223
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appInboxDeleteRead, { localMessageIds });
|
|
224
|
+
},
|
|
225
|
+
async listContacts() {
|
|
226
|
+
return projectContactsResponse(await call("GET", HELLOLYNK_HOST_PATHS.appContacts));
|
|
227
|
+
},
|
|
228
|
+
async saveContact(input) {
|
|
229
|
+
return projectContactMutationResponse(await call(
|
|
230
|
+
"POST",
|
|
231
|
+
HELLOLYNK_HOST_PATHS.appContactsUpsert,
|
|
232
|
+
contactWriteInput(input),
|
|
233
|
+
));
|
|
234
|
+
},
|
|
235
|
+
async deleteContact(contactId) {
|
|
236
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appContactsRemove, { contactId });
|
|
237
|
+
},
|
|
238
|
+
async setAutonomousSendAuthorization(contactId, recipientDelegateId, enabled) {
|
|
239
|
+
return projectContactMutationResponse(await call("POST", HELLOLYNK_HOST_PATHS.appContactAutonomousSendAuthorization, {
|
|
240
|
+
contactId,
|
|
241
|
+
recipientDelegateId: safeId(recipientDelegateId, "recipient_delegate_id_invalid"),
|
|
242
|
+
enabled: enabled === true,
|
|
243
|
+
}));
|
|
244
|
+
},
|
|
245
|
+
async listAuthorizedContacts(input = {}) {
|
|
246
|
+
const query = queryArgs(input);
|
|
247
|
+
const value = await call(Object.keys(query).length ? "POST" : "GET", HELLOLYNK_HOST_PATHS.appAuthorizedContacts, Object.keys(query).length ? query : undefined, false, { timeoutMs: 25_000 });
|
|
248
|
+
return Object.freeze({
|
|
249
|
+
...(value.nativeConversations ? { nativeConversations: value.nativeConversations } : {}),
|
|
250
|
+
contacts: Object.freeze((Array.isArray(value?.contacts) ? value.contacts : [])
|
|
251
|
+
.map((contact) => projectContact(contact, ["contactId", "displayName", "recipientDelegateId", "instanceDisplayName"]))),
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
async listPendingMessages(query = null) {
|
|
255
|
+
assertLive();
|
|
256
|
+
// Reuse only successful activation within this fixed Host connection.
|
|
257
|
+
// Each pending GET still validates permission; failures may activate again.
|
|
258
|
+
inboxActivation ??= call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxActivate)
|
|
259
|
+
.then(activated => { if (activated.active !== true) throw codeError("host_app_response_invalid"); })
|
|
260
|
+
.catch(error => { inboxActivation = null; throw error; });
|
|
261
|
+
await inboxActivation;
|
|
262
|
+
const result = await call(query == null ? "GET" : "POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxPending, query);
|
|
263
|
+
if (!Array.isArray(result.messages)) throw codeError("host_app_response_invalid");
|
|
264
|
+
return { messages: result.messages, ...(result.nextCursor == null ? {} : { nextCursor: result.nextCursor }) };
|
|
265
|
+
},
|
|
266
|
+
async claimMessage(localMessageId, conversationKey) {
|
|
267
|
+
if (typeof localMessageId !== "string" || typeof conversationKey !== "string") throw codeError("invalid_params");
|
|
268
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxClaim, {
|
|
269
|
+
localMessageId: safeId(localMessageId, "local_message_id_invalid"),
|
|
270
|
+
conversationKey: safeId(conversationKey, "conversation_key_invalid"),
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
async claimBatch(batchId) {
|
|
274
|
+
if (!/^inbatch_[a-f0-9]{48}$/.test(batchId || "")) throw codeError("invalid_params");
|
|
275
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxClaim, { batchId });
|
|
276
|
+
},
|
|
277
|
+
async inboxBatch(operation, input = {}) {
|
|
278
|
+
if (!["reserve", "open", "attempt", "confirm", "cancel", "expire", "closed"].includes(operation)) throw codeError("invalid_params");
|
|
279
|
+
return call("POST", `/host/v1/app/authorized-inbox/batch/${operation}`, input);
|
|
280
|
+
},
|
|
281
|
+
async listAuthorizedInboxMessages({ before } = {}) {
|
|
282
|
+
return before === undefined ? call("GET", HELLOLYNK_HOST_PATHS.appAuthorizedInboxMessages)
|
|
283
|
+
: call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxMessages, { before: safeId(before, "local_message_id_invalid") });
|
|
284
|
+
},
|
|
285
|
+
async readAuthorizedInboxMessage(localMessageId, conversationKey) {
|
|
286
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxRead, {
|
|
287
|
+
localMessageId: safeId(localMessageId, "local_message_id_invalid"),
|
|
288
|
+
conversationKey: safeId(conversationKey, "conversation_key_invalid"),
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
async listAuthorizedContactRequests() { return call("GET", HELLOLYNK_HOST_PATHS.appAuthorizedContactRequests); },
|
|
292
|
+
async readAuthorizedContactRequest(localRecordId, conversationKey) {
|
|
293
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedContactRequestRead, { localRecordId: safeId(localRecordId, "local_record_id_invalid"), conversationKey: safeId(conversationKey, "conversation_key_invalid") });
|
|
294
|
+
},
|
|
295
|
+
async sendAuthorizedMessage(input) {
|
|
296
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedSend, authorizedSendInput(input));
|
|
297
|
+
},
|
|
298
|
+
async replyAuthorizedMessage(input) {
|
|
299
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedReply, authorizedReplyInput(input));
|
|
300
|
+
},
|
|
301
|
+
async listContactRequests() {
|
|
302
|
+
return call("GET", HELLOLYNK_HOST_PATHS.appContactRequests);
|
|
303
|
+
},
|
|
304
|
+
async readOutbox() {
|
|
305
|
+
return call("GET", HELLOLYNK_HOST_PATHS.appOutboxMessages);
|
|
306
|
+
},
|
|
307
|
+
async issueOutboxRetryGrant(clientMessageId) {
|
|
308
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appOutboxRetryGrant, { adapterInstanceId, clientMessageId });
|
|
309
|
+
},
|
|
310
|
+
async retryOutboxMessage(clientMessageId, retryGrant) {
|
|
311
|
+
return call("POST", HELLOLYNK_HOST_PATHS.appOutboxRetry, { adapterInstanceId, clientMessageId, retryGrant });
|
|
312
|
+
},
|
|
313
|
+
async readChangesSnapshot() {
|
|
314
|
+
return call("GET", HELLOLYNK_HOST_PATHS.appChangesSnapshot);
|
|
315
|
+
},
|
|
316
|
+
async readWorkbenchChanges({ afterCursor = 0 } = {}) {
|
|
317
|
+
assertLive();
|
|
318
|
+
const result = requireOk(await callRaw("POST", HELLOLYNK_HOST_PATHS.appChangesSubscribe, { afterCursor }, { timeoutMs: 8_000 }));
|
|
319
|
+
return { ...result, hostGeneration: discovery.hostGeneration };
|
|
320
|
+
},
|
|
321
|
+
async readWorkbenchSnapshot(input = {}) {
|
|
322
|
+
const result = await call("POST", HELLOLYNK_HOST_PATHS.appWorkbenchSnapshot, input);
|
|
323
|
+
return result.snapshot;
|
|
324
|
+
},
|
|
325
|
+
async issueWorkbenchActionGrant(command) {
|
|
326
|
+
const validated = validateWorkbenchCommand(command);
|
|
327
|
+
assertLive();
|
|
328
|
+
try {
|
|
329
|
+
return requireWorkbenchActionGrantResult(await callRaw(
|
|
330
|
+
"POST",
|
|
331
|
+
WORKBENCH_ACTION_GRANT_PATH,
|
|
332
|
+
{ adapterInstanceId, request: validated },
|
|
333
|
+
));
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (isTerminalHostError(error)) {
|
|
336
|
+
deadCode = safeCode(error);
|
|
337
|
+
clearInterval(heartbeat);
|
|
338
|
+
}
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
async executeWorkbenchCommand(command, context) {
|
|
343
|
+
const validated = validateWorkbenchCommand(command);
|
|
344
|
+
const statusRead = validated.command === "message.send-status";
|
|
345
|
+
const actionGrant = statusRead ? noWorkbenchActionGrantContext(context) : requireWorkbenchActionGrantContext(context);
|
|
346
|
+
assertLive();
|
|
347
|
+
try {
|
|
348
|
+
return requireWorkbenchCommandResult(await callRaw(
|
|
349
|
+
"POST",
|
|
350
|
+
HELLOLYNK_HOST_PATHS.appWorkbenchCommand,
|
|
351
|
+
{ adapterInstanceId, request: validated, ...(statusRead ? {} : { actionGrant }) },
|
|
352
|
+
{},
|
|
353
|
+
), validated.command);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (isTerminalHostError(error)) {
|
|
356
|
+
deadCode = safeCode(error);
|
|
357
|
+
clearInterval(heartbeat);
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
close,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function validateTraeWorkProtectedBinding(value) {
|
|
367
|
+
return validateRestrictedProtectedBinding(value, {
|
|
368
|
+
adapterId: "trae-work",
|
|
369
|
+
bindingContract: "hellolynk-trae-work-neutral-binding-v1",
|
|
370
|
+
exactScopes: TRAE_WORK_NEUTRAL_EXACT_SCOPES,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function validateRestrictedProtectedBinding(value, policy = {}) {
|
|
375
|
+
const adapterId = safeId(policy.adapterId, "neutral_adapter_mismatch");
|
|
376
|
+
const bindingContract = safeId(policy.bindingContract, "neutral_capability_binding_invalid");
|
|
377
|
+
const exactScopes = Object.freeze([...(policy.exactScopes || [])].map(String).sort());
|
|
378
|
+
if (!exactScopes.length || !sameStrings(exactScopes, policy.exactScopes || [])) {
|
|
379
|
+
throw codeError("neutral_capability_scopes_mismatch");
|
|
380
|
+
}
|
|
381
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
382
|
+
throw codeError("neutral_capability_binding_invalid");
|
|
383
|
+
}
|
|
384
|
+
const releaseKeys = ["releaseRoot", "releaseManifestSha256", "runnerRelativePath", "profileManifestPath"];
|
|
385
|
+
const releaseCount = releaseKeys.filter((key) => Object.hasOwn(value, key)).length;
|
|
386
|
+
assertExactKeys(value, [
|
|
387
|
+
"adapterId", "capability", "contract", "delegateId", "hostInstallationId",
|
|
388
|
+
"principalId", "profileId", "region", "runtimeRoot", "scopes", "version",
|
|
389
|
+
...(releaseCount ? releaseKeys : []),
|
|
390
|
+
], "neutral_capability_binding_invalid");
|
|
391
|
+
if (releaseCount !== 0 && releaseCount !== releaseKeys.length) {
|
|
392
|
+
throw codeError("neutral_trae_work_launch_configuration_invalid");
|
|
393
|
+
}
|
|
394
|
+
const capability = value.capability;
|
|
395
|
+
if (!capability || typeof capability !== "object" || Array.isArray(capability)) {
|
|
396
|
+
throw codeError("neutral_capability_invalid");
|
|
397
|
+
}
|
|
398
|
+
assertExactKeys(capability, [
|
|
399
|
+
"adapterId", "capabilityId", "contract", "delegateId", "hostInstallationId",
|
|
400
|
+
"issuedAt", "principalId", "region", "revokedAt", "scopes", "secretBuffer", "state", "version",
|
|
401
|
+
], "neutral_capability_invalid");
|
|
402
|
+
const binding = {
|
|
403
|
+
region: safeId(value.region, "neutral_region_invalid"),
|
|
404
|
+
principalId: safeId(value.principalId, "neutral_principal_invalid"),
|
|
405
|
+
delegateId: safeId(value.delegateId, "neutral_delegate_invalid"),
|
|
406
|
+
hostInstallationId: safeId(value.hostInstallationId, "neutral_host_installation_invalid"),
|
|
407
|
+
profileId: safeId(value.profileId, "neutral_profile_invalid"),
|
|
408
|
+
adapterId: safeId(value.adapterId, "neutral_adapter_invalid"),
|
|
409
|
+
runtimeRoot: String(value.runtimeRoot || ""),
|
|
410
|
+
};
|
|
411
|
+
const bindingScopes = [...new Set((value.scopes || []).map(String))].sort();
|
|
412
|
+
if (value.contract !== bindingContract || Number(value.version) !== 2
|
|
413
|
+
|| !sameStrings(bindingScopes, exactScopes)) {
|
|
414
|
+
throw codeError("neutral_capability_binding_invalid");
|
|
415
|
+
}
|
|
416
|
+
if (binding.adapterId !== adapterId) throw codeError("neutral_adapter_mismatch");
|
|
417
|
+
if (!binding.runtimeRoot || !path.isAbsolute(binding.runtimeRoot)
|
|
418
|
+
|| path.resolve(binding.runtimeRoot) !== binding.runtimeRoot) {
|
|
419
|
+
throw codeError("neutral_runtime_root_invalid");
|
|
420
|
+
}
|
|
421
|
+
if (capability.region !== binding.region || capability.principalId !== binding.principalId
|
|
422
|
+
|| capability.delegateId !== binding.delegateId
|
|
423
|
+
|| capability.hostInstallationId !== binding.hostInstallationId
|
|
424
|
+
|| capability.adapterId !== binding.adapterId) {
|
|
425
|
+
throw codeError("neutral_capability_owner_mismatch");
|
|
426
|
+
}
|
|
427
|
+
const scopes = [...new Set((capability.scopes || []).map(String))].sort();
|
|
428
|
+
if (!sameStrings(scopes, exactScopes)) {
|
|
429
|
+
throw codeError("neutral_capability_scopes_mismatch");
|
|
430
|
+
}
|
|
431
|
+
const secretBuffer = Buffer.isBuffer(capability.secretBuffer)
|
|
432
|
+
? Buffer.from(capability.secretBuffer)
|
|
433
|
+
: capability.secretBuffer instanceof Uint8Array ? Buffer.from(capability.secretBuffer) : null;
|
|
434
|
+
if (!secretBuffer || secretBuffer.length < 32 || secretBuffer.length > 128) {
|
|
435
|
+
throw codeError("neutral_capability_secret_invalid");
|
|
436
|
+
}
|
|
437
|
+
if (Number(capability.version) !== 2) throw codeError("neutral_capability_version_mismatch");
|
|
438
|
+
if (capability.state !== "active") throw codeError("neutral_capability_inactive");
|
|
439
|
+
if (capability.contract !== HELLOLYNK_HOST_CAPABILITY_CONTRACT || capability.revokedAt !== null) {
|
|
440
|
+
throw codeError("neutral_capability_binding_invalid");
|
|
441
|
+
}
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
...binding,
|
|
444
|
+
scopes: Object.freeze(bindingScopes),
|
|
445
|
+
...(releaseCount === 0 ? {} : {
|
|
446
|
+
releaseRoot: absolutePath(value.releaseRoot, "neutral_trae_work_launch_configuration_invalid"),
|
|
447
|
+
releaseManifestSha256: sha256(value.releaseManifestSha256, "neutral_trae_work_launch_configuration_invalid"),
|
|
448
|
+
runnerRelativePath: value.runnerRelativePath === "runtime/neutral-host-bootstrap.mjs"
|
|
449
|
+
? value.runnerRelativePath : fail("neutral_trae_work_launch_configuration_invalid"),
|
|
450
|
+
profileManifestPath: absolutePath(value.profileManifestPath, "neutral_trae_work_launch_configuration_invalid"),
|
|
451
|
+
}),
|
|
452
|
+
capability: Object.freeze({
|
|
453
|
+
contract: HELLOLYNK_HOST_CAPABILITY_CONTRACT,
|
|
454
|
+
capabilityId: safeId(capability.capabilityId, "neutral_capability_id_invalid"),
|
|
455
|
+
region: binding.region,
|
|
456
|
+
principalId: binding.principalId,
|
|
457
|
+
delegateId: binding.delegateId,
|
|
458
|
+
hostInstallationId: binding.hostInstallationId,
|
|
459
|
+
adapterId,
|
|
460
|
+
version: 2,
|
|
461
|
+
state: "active",
|
|
462
|
+
scopes: Object.freeze(scopes),
|
|
463
|
+
issuedAt: integer(capability.issuedAt, undefined, 0, Number.MAX_SAFE_INTEGER),
|
|
464
|
+
revokedAt: null,
|
|
465
|
+
secretBuffer,
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function publicBinding(value) {
|
|
471
|
+
return {
|
|
472
|
+
region: value.region,
|
|
473
|
+
principalId: value.principalId,
|
|
474
|
+
hostInstallationId: value.hostInstallationId,
|
|
475
|
+
runtimeRoot: value.runtimeRoot,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function launchBinding(value) {
|
|
480
|
+
return Object.freeze({
|
|
481
|
+
...publicBinding(value), delegateId: value.delegateId, profileId: value.profileId,
|
|
482
|
+
adapterId: "trae-work", version: 2, scopes: Object.freeze([...value.scopes]),
|
|
483
|
+
...(value.releaseRoot ? {
|
|
484
|
+
releaseRoot: value.releaseRoot, releaseManifestSha256: value.releaseManifestSha256,
|
|
485
|
+
runnerRelativePath: value.runnerRelativePath, profileManifestPath: value.profileManifestPath,
|
|
486
|
+
} : {}),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// The same or a higher release answers "current" and nothing is written. An earlier one finishes
|
|
491
|
+
// its admitted work and leaves; while it is busy it keeps serving and this client attaches to it.
|
|
492
|
+
async function earlierHostLeft(requestHandover, binding, waitMs) {
|
|
493
|
+
try {
|
|
494
|
+
const { status } = await requestHandover({ runtimeRoot: binding.runtimeRoot,
|
|
495
|
+
hostInstallationId: binding.hostInstallationId, publishedAt: new Date().toISOString(), waitMs, untilStopped: true });
|
|
496
|
+
return ["stopped", "restarted", "no_host"].includes(status);
|
|
497
|
+
} catch { return false; }
|
|
498
|
+
}
|
|
499
|
+
async function safeDiscover(discover, input) {
|
|
500
|
+
try { return await discover(input); }
|
|
501
|
+
catch (error) { if (SAFE_DISCOVERY_ERRORS.has(error?.code)) return null; throw error; }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function waitForDiscovery(discover, input, options) {
|
|
505
|
+
const wait = options.wait || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
506
|
+
const timeoutMs = integer(options.discoveryWaitMs, 120_000, 10, 120_000);
|
|
507
|
+
const deadline = Date.now() + timeoutMs;
|
|
508
|
+
while (Date.now() < deadline) {
|
|
509
|
+
const value = await safeDiscover(discover, input);
|
|
510
|
+
if (value) return value;
|
|
511
|
+
await wait(Math.min(50, Math.max(1, deadline - Date.now())));
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function assertDiscovery(value, binding) {
|
|
517
|
+
if (!value || value.contractId !== HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT
|
|
518
|
+
|| value.region !== binding.region || value.principalId !== binding.principalId
|
|
519
|
+
|| value.hostInstallationId !== binding.hostInstallationId || value.host !== "127.0.0.1"
|
|
520
|
+
|| !Number.isSafeInteger(value.port) || value.port < 1 || value.port > 65_535
|
|
521
|
+
|| !/^[a-f0-9]{48}$/.test(String(value.hostGeneration || ""))) {
|
|
522
|
+
throw codeError("neutral_host_discovery_owner_mismatch");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function requireOk(result) {
|
|
527
|
+
if (!result || result.status !== 200 || result.body?.ok !== true) {
|
|
528
|
+
throw codeError(String(result?.body?.error || `neutral_host_http_${result?.status || 0}`));
|
|
529
|
+
}
|
|
530
|
+
return result.body;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function requireWorkbenchCommandResult(result, expectedCommand) {
|
|
534
|
+
if (!result || result.status !== 200) {
|
|
535
|
+
throw codeError(String(result?.body?.error || `neutral_host_http_${result?.status || 0}`));
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
return validateWorkbenchCommandResult(result.body, expectedCommand);
|
|
539
|
+
} catch {
|
|
540
|
+
throw codeError("neutral_host_workbench_command_response_invalid");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function requireWorkbenchActionGrantResult(result) {
|
|
545
|
+
if (!result || result.status !== 200 || !result.body || typeof result.body !== "object"
|
|
546
|
+
|| Array.isArray(result.body)) {
|
|
547
|
+
throw codeError(String(result?.body?.error || `neutral_host_http_${result?.status || 0}`));
|
|
548
|
+
}
|
|
549
|
+
assertExactKeys(
|
|
550
|
+
result.body,
|
|
551
|
+
["ok", "actionGrant", "expiresAtMs"],
|
|
552
|
+
"neutral_host_workbench_action_grant_response_invalid",
|
|
553
|
+
);
|
|
554
|
+
if (result.body.ok !== true || typeof result.body.actionGrant !== "string"
|
|
555
|
+
|| !ACTION_GRANT.test(result.body.actionGrant)
|
|
556
|
+
|| !Number.isSafeInteger(result.body.expiresAtMs) || result.body.expiresAtMs <= Date.now()) {
|
|
557
|
+
throw codeError("neutral_host_workbench_action_grant_response_invalid");
|
|
558
|
+
}
|
|
559
|
+
return Object.freeze({ actionGrant: result.body.actionGrant });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function requireWorkbenchActionGrantContext(value) {
|
|
563
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
564
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
|
|
565
|
+
throw codeError("neutral_host_workbench_action_grant_required");
|
|
566
|
+
}
|
|
567
|
+
assertExactKeys(value, ["actionGrant"], "neutral_host_workbench_action_grant_required");
|
|
568
|
+
if (typeof value.actionGrant !== "string" || !ACTION_GRANT.test(value.actionGrant)) {
|
|
569
|
+
throw codeError("neutral_host_workbench_action_grant_required");
|
|
570
|
+
}
|
|
571
|
+
return value.actionGrant;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function requireAccepted(result) {
|
|
575
|
+
if (!result || result.status !== 202 || result.body?.ok !== true) {
|
|
576
|
+
throw codeError(String(result?.body?.error || `neutral_host_http_${result?.status || 0}`));
|
|
577
|
+
}
|
|
578
|
+
return result.body;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function isTerminalHostError(error) {
|
|
582
|
+
const code = safeCode(error);
|
|
583
|
+
if (NON_TERMINAL_HOST_APP_ERRORS.has(code)) return false;
|
|
584
|
+
return code.startsWith("neutral_host_") || code.startsWith("host_");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function noWorkbenchActionGrantContext(value) {
|
|
588
|
+
if (value != null && (!value || typeof value !== "object" || Array.isArray(value)
|
|
589
|
+
|| Object.keys(value).length !== 0)) {
|
|
590
|
+
throw codeError("neutral_host_workbench_action_grant_forbidden");
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function safeId(value, code) {
|
|
596
|
+
const text = String(value || "");
|
|
597
|
+
if (!SAFE_ID.test(text)) throw codeError(code);
|
|
598
|
+
return text;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function authorizedSendInput(value) {
|
|
602
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
603
|
+
|| Object.keys(value).filter(key => key !== "targetConversationId" && key !== "targetConversationTitle").sort().join("|") !== "clientMessageId|contactId|recipientDelegateId|text") {
|
|
604
|
+
throw codeError("authorized_send_input_invalid");
|
|
605
|
+
}
|
|
606
|
+
const text = String(value.text ?? "");
|
|
607
|
+
if (!text || text.includes("\u0000") || Buffer.byteLength(text, "utf8") > 20 * 1024) {
|
|
608
|
+
throw codeError("message_text_invalid");
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
...optionalTarget(value),
|
|
612
|
+
clientMessageId: safeId(value.clientMessageId, "client_message_id_invalid"),
|
|
613
|
+
contactId: safeId(value.contactId, "contact_id_invalid"),
|
|
614
|
+
recipientDelegateId: safeId(value.recipientDelegateId, "recipient_delegate_id_invalid"),
|
|
615
|
+
text,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function authorizedReplyInput(value) {
|
|
620
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
621
|
+
|| Object.keys(value).sort().join("|") !== "clientMessageId|conversationKey|localMessageId|text"
|
|
622
|
+
|| [value.localMessageId, value.conversationKey, value.clientMessageId, value.text].some((item) => typeof item !== "string")) {
|
|
623
|
+
throw codeError("authorized_reply_input_invalid");
|
|
624
|
+
}
|
|
625
|
+
const text = value.text;
|
|
626
|
+
if (!text || text.includes("\u0000") || Buffer.byteLength(text, "utf8") > 20 * 1024) {
|
|
627
|
+
throw codeError("message_text_invalid");
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
localMessageId: safeId(value.localMessageId, "local_message_id_invalid"),
|
|
631
|
+
conversationKey: safeId(value.conversationKey, "conversation_key_invalid"),
|
|
632
|
+
clientMessageId: safeId(value.clientMessageId, "client_message_id_invalid"),
|
|
633
|
+
text,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function contactWriteInput(value) {
|
|
638
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
639
|
+
throw codeError("contact_input_invalid");
|
|
640
|
+
}
|
|
641
|
+
const expected = value.contactId == null
|
|
642
|
+
? ["displayName", "hellolynkId"]
|
|
643
|
+
: ["contactId", "displayName", "hellolynkId"];
|
|
644
|
+
assertExactKeys(value, expected, "contact_input_invalid");
|
|
645
|
+
return {
|
|
646
|
+
...(value.contactId == null ? {} : { contactId: safeId(value.contactId, "contact_id_invalid") }),
|
|
647
|
+
hellolynkId: String(value.hellolynkId || ""),
|
|
648
|
+
displayName: String(value.displayName || ""),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function projectContactsResponse(value) {
|
|
653
|
+
if (!Array.isArray(value?.contacts)) throw codeError("neutral_host_response_invalid");
|
|
654
|
+
return Object.freeze({
|
|
655
|
+
contacts: Object.freeze(value.contacts.map((contact) => projectContact(contact, [
|
|
656
|
+
"contactId", "hellolynkId", "displayName",
|
|
657
|
+
"autonomousSendAuthorized", "createdAt", "updatedAt",
|
|
658
|
+
]))),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function projectContactMutationResponse(value) {
|
|
663
|
+
if (!value?.contact || typeof value.contact !== "object" || Array.isArray(value.contact)) {
|
|
664
|
+
throw codeError("neutral_host_response_invalid");
|
|
665
|
+
}
|
|
666
|
+
return Object.freeze({
|
|
667
|
+
contact: projectContact(value.contact, [
|
|
668
|
+
"contactId", "hellolynkId", "displayName",
|
|
669
|
+
"autonomousSendAuthorized", "createdAt", "updatedAt",
|
|
670
|
+
]),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function projectContact(value, allowed) {
|
|
675
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
676
|
+
throw codeError("neutral_host_response_invalid");
|
|
677
|
+
}
|
|
678
|
+
const result = {};
|
|
679
|
+
for (const key of allowed) if (Object.hasOwn(value, key)) result[key] = structuredClone(value[key]);
|
|
680
|
+
return Object.freeze(result);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function absolutePath(value, code) {
|
|
684
|
+
const text = String(value || "");
|
|
685
|
+
if (!path.isAbsolute(text) || path.resolve(text) !== text) throw codeError(code);
|
|
686
|
+
return text;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function sha256(value, code) {
|
|
690
|
+
const text = String(value || "");
|
|
691
|
+
if (!/^[a-f0-9]{64}$/.test(text)) throw codeError(code);
|
|
692
|
+
return text;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function integer(value, fallback, minimum, maximum) {
|
|
696
|
+
const number = Number(value ?? fallback);
|
|
697
|
+
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
|
698
|
+
throw codeError("neutral_limit_invalid");
|
|
699
|
+
}
|
|
700
|
+
return number;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function sameStrings(left, right) {
|
|
704
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function assertExactKeys(value, expected, code) {
|
|
708
|
+
const actual = Object.keys(value).sort();
|
|
709
|
+
const wanted = [...expected].sort();
|
|
710
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
711
|
+
throw codeError(code);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function safeCode(error) {
|
|
716
|
+
return String(error?.code || error?.message || "neutral_host_unavailable")
|
|
717
|
+
.replace(/[^A-Za-z0-9._:-]+/g, "_").slice(0, 120) || "neutral_host_unavailable";
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function codeError(code) {
|
|
721
|
+
const error = new Error(code);
|
|
722
|
+
error.code = code;
|
|
723
|
+
return error;
|
|
724
|
+
}
|
|
725
|
+
function fail(code) { throw codeError(code); }
|