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