@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100
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/.agents/plugins/marketplace.json +20 -0
- 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/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { lstat } from "node:fs/promises";
|
|
4
|
+
import { ACCOUNT_CLIENTS, readClientAccount, clientAccountBaseRoot } from "./hellolynk-client-account.mjs";
|
|
5
|
+
import { createNeutralHostInstallationStore } from "./hellolynk-neutral-host-installation-store.mjs";
|
|
6
|
+
import { createHostProtectedStore, verifyHostProtectedStorePath } from "./hellolynk-host-protected-store.mjs";
|
|
7
|
+
import { normalizeHelloLynkBaseUrl } from "./hellolynk-client-core.mjs";
|
|
8
|
+
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
9
|
+
|
|
10
|
+
export const LOCAL_IDENTITY_PROOF_PATH = "/host/v1/local-identity-proof";
|
|
11
|
+
export const LOCAL_IDENTITY_PROOF_CONTRACT = "hellolynk-local-identity-proof-v1";
|
|
12
|
+
const CLIENT_METHODS = Object.freeze({ codex: "Codex", openclaw: "OpenClaw", "trae-work": "TraeWork", workbuddy: "WorkBuddy", cursor: "Cursor", dsh: "Dsh", "claude-code": "ClaudeCode" });
|
|
13
|
+
const fail = () => Object.assign(new Error("local_identity_proof_invalid"), { code: "local_identity_proof_invalid" });
|
|
14
|
+
function exact(value, fields) {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
16
|
+
|| Object.keys(value).sort().join(",") !== [...fields].sort().join(",")) throw fail();
|
|
17
|
+
}
|
|
18
|
+
function id(value) {
|
|
19
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/.test(value)) throw fail();
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function token(value) { if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) throw fail(); return value; }
|
|
23
|
+
function samePath(a, b) { return path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase(); }
|
|
24
|
+
function same(a, b) { return JSON.stringify(a) === JSON.stringify(b); }
|
|
25
|
+
|
|
26
|
+
export function normalizeLocalIdentityOwner(value) {
|
|
27
|
+
exact(value, ["region", "baseUrl", "principalId", "hellolynkId"]);
|
|
28
|
+
const region = value.region === "international" ? "intl" : value.region;
|
|
29
|
+
if (!["intl", "cn"].includes(region)
|
|
30
|
+
|| !runtimeSafety.helloLynkStableIdMatchesRegion(value.hellolynkId, region)) throw fail();
|
|
31
|
+
return Object.freeze({ region, baseUrl: normalizeHelloLynkBaseUrl(value.baseUrl),
|
|
32
|
+
principalId: id(value.principalId), hellolynkId: id(value.hellolynkId) });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateLocalIdentityProofRequest(value) {
|
|
36
|
+
exact(value, ["challenge", "owner", "target"]);
|
|
37
|
+
exact(value.target, ["hostInstallationId", "delegateId", "authorizationVersion"]);
|
|
38
|
+
if (!Number.isSafeInteger(value.target.authorizationVersion) || value.target.authorizationVersion < 1) throw fail();
|
|
39
|
+
return Object.freeze({ challenge: token(value.challenge), owner: normalizeLocalIdentityOwner(value.owner),
|
|
40
|
+
target: Object.freeze({ hostInstallationId: id(value.target.hostInstallationId), delegateId: id(value.target.delegateId),
|
|
41
|
+
authorizationVersion: value.target.authorizationVersion }) });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function proofPayload({ request, hostGeneration, capabilityId, identity }) {
|
|
45
|
+
request = validateLocalIdentityProofRequest(request);
|
|
46
|
+
exact(identity, ["deviceId", "identityKeyId", "safetyCode"]);
|
|
47
|
+
// The fingerprint is internal proof data, never a Workbench/MCP result.
|
|
48
|
+
if (typeof identity.safetyCode !== "string" || !/^[A-F0-9]{4}(?:-[A-F0-9]{4}){11}$/.test(identity.safetyCode)) throw fail();
|
|
49
|
+
return { contract: LOCAL_IDENTITY_PROOF_CONTRACT, request, hostGeneration: id(hostGeneration), capabilityId: id(capabilityId),
|
|
50
|
+
identity: { deviceId: id(identity.deviceId), identityKeyId: id(identity.identityKeyId), safetyCode: identity.safetyCode.toUpperCase() } };
|
|
51
|
+
}
|
|
52
|
+
function mac(payload, secretBuffer) {
|
|
53
|
+
if (!Buffer.isBuffer(secretBuffer) || secretBuffer.length < 32) throw fail();
|
|
54
|
+
return createHmac("sha256", secretBuffer).update(`${LOCAL_IDENTITY_PROOF_CONTRACT}\n${JSON.stringify(payload)}`).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
export function signLocalIdentityProof(input) {
|
|
57
|
+
const payload = proofPayload(input);
|
|
58
|
+
return Object.freeze({ ...payload, proof: mac(payload, input.secretBuffer) });
|
|
59
|
+
}
|
|
60
|
+
export function verifyLocalIdentityProof({ response, request, hostGeneration, capabilityId, secretBuffer }) {
|
|
61
|
+
exact(response, ["contract", "request", "hostGeneration", "capabilityId", "identity", "proof"]);
|
|
62
|
+
const payload = proofPayload(response);
|
|
63
|
+
if (response.contract !== LOCAL_IDENTITY_PROOF_CONTRACT || !same(payload.request, validateLocalIdentityProofRequest(request))
|
|
64
|
+
|| payload.hostGeneration !== hostGeneration || payload.capabilityId !== capabilityId) throw fail();
|
|
65
|
+
const expected = Buffer.from(mac(payload, secretBuffer), "hex"), received = Buffer.from(token(response.proof), "hex");
|
|
66
|
+
if (!timingSafeEqual(expected, received)) throw fail();
|
|
67
|
+
return payload.identity.safetyCode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// All clients start from the same protected installation. Per-client account
|
|
71
|
+
// selectors still isolate a client that disconnects or changes account.
|
|
72
|
+
export function localIdentityInstallationCandidates({ root, clients }) {
|
|
73
|
+
const base = clientAccountBaseRoot(root);
|
|
74
|
+
if (path.basename(base) !== "host" || !Array.isArray(clients) || !clients.length
|
|
75
|
+
|| clients.some((client) => !ACCOUNT_CLIENTS.includes(client))) throw fail();
|
|
76
|
+
return ACCOUNT_CLIENTS.map((client) => ({ client, baseRoot: base }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function accountSnapshot(candidate, operation) {
|
|
80
|
+
const { signal } = operation;
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
// The selector check, or the registry check when no selector exists, also
|
|
83
|
+
// verifies its root before any content is read. No root-only helper is needed.
|
|
84
|
+
const selector = path.join(candidate.baseRoot, `client-account-${candidate.client}.json`);
|
|
85
|
+
try { await lstat(selector); await verifyHostProtectedStorePath({ root: candidate.baseRoot, path: selector, signal }); }
|
|
86
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
87
|
+
const account = readClientAccount(candidate.client, candidate.baseRoot);
|
|
88
|
+
if (account.phase !== "ready") throw fail();
|
|
89
|
+
return account;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadCandidate(candidate, operation) {
|
|
93
|
+
const account = await accountSnapshot(candidate, operation);
|
|
94
|
+
if (samePath(account.root, operation.excludeRoot)) return null;
|
|
95
|
+
const { signal, installations } = operation;
|
|
96
|
+
if (!installations.has(account.root)) {
|
|
97
|
+
const store = createNeutralHostInstallationStore({ root: account.root, readOnly: true,
|
|
98
|
+
verifyPath: ({ path: target, directory }) => verifyHostProtectedStorePath({ root: account.root, path: target, directory, signal }) });
|
|
99
|
+
installations.set(account.root, store.status().then((snapshot) => ({ store, snapshot })));
|
|
100
|
+
}
|
|
101
|
+
const { store: installationStore, snapshot } = await installations.get(account.root);
|
|
102
|
+
const active = snapshot.installations.filter((item) => item.state === "active");
|
|
103
|
+
if (active.length !== 1 || active[0].principalId !== operation.owner.principalId
|
|
104
|
+
|| !runtimeSafety.helloLynkStableIdMatchesRegion(operation.owner.hellolynkId, active[0].region)) return null;
|
|
105
|
+
const matchingTarget = active.length === 1 ? active[0].delegateBindings.find((item) => item.delegateId === operation.peerDelegateId
|
|
106
|
+
&& item.adapters.includes(candidate.client)) : null;
|
|
107
|
+
if (!matchingTarget) return null;
|
|
108
|
+
const selectionKey = `${account.root.toLowerCase()}\0${operation.peerDelegateId}`;
|
|
109
|
+
if (operation.selectedTargets.has(selectionKey)) return null;
|
|
110
|
+
operation.selectedTargets.add(selectionKey);
|
|
111
|
+
// Reuse one ACL-verified registry snapshot for this single operation. Every
|
|
112
|
+
// successful proof is followed by a fresh registry and binding verification.
|
|
113
|
+
const snapshotStore = { status: async () => snapshot, resolveBinding: async (input) => {
|
|
114
|
+
const active = snapshot.installations.filter((item) => item.state === "active");
|
|
115
|
+
if (active.length !== 1) throw fail();
|
|
116
|
+
const installation = active[0];
|
|
117
|
+
const matches = installation.delegateBindings.filter((item) => (!input.profileId || item.profileId === input.profileId)
|
|
118
|
+
&& (!input.delegateId || item.delegateId === input.delegateId));
|
|
119
|
+
if (matches.length !== 1) throw fail();
|
|
120
|
+
return { region: installation.region, principalId: installation.principalId, hostInstallationId: installation.hostInstallationId,
|
|
121
|
+
canonicalProfileId: installation.canonicalProfileId, ...matches[0] };
|
|
122
|
+
} };
|
|
123
|
+
// Several adapters may share one delegate. Resolve just one active installed
|
|
124
|
+
// adapter for that exact target, trying another only when the first is absent
|
|
125
|
+
// or revoked. Do not launch one PowerShell process per adapter per message.
|
|
126
|
+
let lastError = fail();
|
|
127
|
+
for (const choice of operation.candidates.filter((item) => matchingTarget.adapters.includes(item.client)
|
|
128
|
+
&& samePath(item.baseRoot, clientAccountBaseRoot(account.root)))) {
|
|
129
|
+
let binding;
|
|
130
|
+
try {
|
|
131
|
+
const selectedAccount = await accountSnapshot(choice, operation);
|
|
132
|
+
if (!samePath(selectedAccount.root, account.root)) continue;
|
|
133
|
+
const store = createHostProtectedStore({ root: account.root, readOnly: true, followClientAccount: false, signal,
|
|
134
|
+
installationStore: snapshotStore });
|
|
135
|
+
binding = await store[`resolve${CLIENT_METHODS[choice.client]}NeutralBinding`]();
|
|
136
|
+
const target = await snapshotStore.resolveBinding({ profileId: binding.profileId, delegateId: binding.delegateId });
|
|
137
|
+
if (binding.delegateId !== operation.peerDelegateId || binding.capability.state !== "active"
|
|
138
|
+
|| !binding.capability.scopes.includes("app-data:contacts-read")) throw fail();
|
|
139
|
+
return { account: selectedAccount, binding, target, async recheck() {
|
|
140
|
+
const [currentAccount, current, currentTarget] = await Promise.all([
|
|
141
|
+
accountSnapshot(choice, operation),
|
|
142
|
+
store[`verify${CLIENT_METHODS[choice.client]}NeutralBinding`](),
|
|
143
|
+
installationStore.resolveBinding({ profileId: binding.profileId, delegateId: binding.delegateId }),
|
|
144
|
+
]);
|
|
145
|
+
if (!same(selectedAccount, currentAccount)) throw fail();
|
|
146
|
+
if (current.capability.capabilityId !== binding.capability.capabilityId
|
|
147
|
+
|| current.hostInstallationId !== binding.hostInstallationId || current.delegateId !== binding.delegateId) throw fail();
|
|
148
|
+
if (!same(currentTarget, target)) throw fail();
|
|
149
|
+
} };
|
|
150
|
+
} catch (error) { binding?.capability.secretBuffer?.fill(0); lastError = error; }
|
|
151
|
+
signal.throwIfAborted();
|
|
152
|
+
}
|
|
153
|
+
throw lastError;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// No background polling, retries, trust writes, contact grants or outbox work.
|
|
157
|
+
// A failure supplies no additional trust; the normal remote verification gate
|
|
158
|
+
// remains responsible for unknown devices. Verified identity pins retain the
|
|
159
|
+
// existing E2EE semantics; this does not turn historical pins into live grants.
|
|
160
|
+
export function createLocalPeerAccess(options, dependencies = {}) {
|
|
161
|
+
const profile = options.profile;
|
|
162
|
+
const owner = normalizeLocalIdentityOwner({ region: profile.region, baseUrl: profile.baseUrl,
|
|
163
|
+
principalId: profile.principalId, hellolynkId: profile.hellolynkId });
|
|
164
|
+
const candidates = localIdentityInstallationCandidates({ root: options.root, clients: options.clients });
|
|
165
|
+
const assertSourceCurrent = options.assertCurrent;
|
|
166
|
+
if (typeof assertSourceCurrent !== "function") throw fail();
|
|
167
|
+
const load = dependencies.loadCandidate || loadCandidate;
|
|
168
|
+
const discover = dependencies.discover || (async (input) => (await import("./hellolynk-neutral-host-process.mjs")).discoverHelloLynkNeutralHost(input));
|
|
169
|
+
const request = dependencies.request || (async (input) => (await import("./hellolynk-neutral-host-process.mjs")).requestHelloLynkNeutralHost(input));
|
|
170
|
+
return async ({ peerHelloLynkId, peerDelegateId, peerHostId, peerAuthorizationVersion }, visit,
|
|
171
|
+
{ all = false, required = false, proveIdentity = true, timeoutMs = 12_000, signal: callerSignal } = {}) => {
|
|
172
|
+
const absent = () => {
|
|
173
|
+
if (required) throw Object.assign(new Error("local_peer_unavailable"), { code: "local_peer_unavailable" });
|
|
174
|
+
return all ? [] : null;
|
|
175
|
+
};
|
|
176
|
+
if (peerHelloLynkId !== owner.hellolynkId || !peerDelegateId || process.platform !== "win32") return absent();
|
|
177
|
+
if (typeof visit !== "function" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 30_000) throw fail();
|
|
178
|
+
id(peerDelegateId);
|
|
179
|
+
await assertSourceCurrent();
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const signal = callerSignal ? AbortSignal.any([controller.signal, callerSignal]) : controller.signal;
|
|
182
|
+
const deadline = performance.now() + timeoutMs;
|
|
183
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
184
|
+
const operation = { signal, peerDelegateId, owner, excludeRoot: options.root, candidates,
|
|
185
|
+
installations: new Map(), selectedTargets: new Set() };
|
|
186
|
+
const loadedPeers = [];
|
|
187
|
+
let localTargetFound = false;
|
|
188
|
+
try {
|
|
189
|
+
const results = await Promise.allSettled(candidates.map(async (candidate) => {
|
|
190
|
+
const loaded = await load(candidate, operation);
|
|
191
|
+
if (!loaded) return null;
|
|
192
|
+
loadedPeers.push(loaded);
|
|
193
|
+
const { binding, target } = loaded;
|
|
194
|
+
if (signal.aborted || binding.delegateId !== peerDelegateId || binding.hostInstallationId === profile.hostId || binding.principalId !== owner.principalId
|
|
195
|
+
|| !runtimeSafety.helloLynkStableIdMatchesRegion(owner.hellolynkId, binding.region)
|
|
196
|
+
|| (peerHostId && binding.hostInstallationId !== peerHostId)
|
|
197
|
+
|| (peerAuthorizationVersion != null && target.authorizationVersion !== peerAuthorizationVersion)) return null;
|
|
198
|
+
localTargetFound = true;
|
|
199
|
+
const discovery = await discover(binding);
|
|
200
|
+
if (!discovery || signal.aborted) return null;
|
|
201
|
+
// A signed exchange already authenticates its response. It only
|
|
202
|
+
// needs local binding selection here; E2EE independently checks keys.
|
|
203
|
+
if (!proveIdentity) {
|
|
204
|
+
await loaded.recheck();
|
|
205
|
+
return { loaded, discovery, code: null, identity: null };
|
|
206
|
+
}
|
|
207
|
+
const body = validateLocalIdentityProofRequest({ challenge: randomBytes(32).toString("hex"), owner,
|
|
208
|
+
target: { hostInstallationId: binding.hostInstallationId, delegateId: binding.delegateId,
|
|
209
|
+
authorizationVersion: target.authorizationVersion } });
|
|
210
|
+
// A cold Host may need protected capability resolution before it can
|
|
211
|
+
// answer. Give that HTTP request the remaining operation budget,
|
|
212
|
+
// without restarting the budget or retrying after a timeout.
|
|
213
|
+
const remainingMs = Math.floor(deadline - performance.now());
|
|
214
|
+
if (signal.aborted || remainingMs < 100) return null;
|
|
215
|
+
const result = await request({ discovery, capability: binding.capability, method: "POST",
|
|
216
|
+
pathname: LOCAL_IDENTITY_PROOF_PATH, body, timeoutMs: remainingMs, signal });
|
|
217
|
+
if (result.status !== 200 || signal.aborted) return null;
|
|
218
|
+
const code = verifyLocalIdentityProof({ response: result.body, request: body,
|
|
219
|
+
hostGeneration: discovery.hostGeneration, capabilityId: binding.capability.capabilityId,
|
|
220
|
+
secretBuffer: binding.capability.secretBuffer });
|
|
221
|
+
await loaded.recheck();
|
|
222
|
+
const currentDiscovery = await discover(binding);
|
|
223
|
+
if (!currentDiscovery || currentDiscovery.hostGeneration !== discovery.hostGeneration
|
|
224
|
+
|| currentDiscovery.host !== discovery.host || currentDiscovery.port !== discovery.port) return null;
|
|
225
|
+
await assertSourceCurrent();
|
|
226
|
+
return signal.aborted ? null : { loaded, discovery, code, identity: result.body.identity };
|
|
227
|
+
}));
|
|
228
|
+
for (const result of results) if (result.status === "rejected" && dependencies.onCandidateRejected) {
|
|
229
|
+
const code = /^[a-z][a-z0-9_]{1,100}$/.test(result.reason?.code || "") ? result.reason.code : "local_identity_candidate_unavailable";
|
|
230
|
+
dependencies.onCandidateRejected(code);
|
|
231
|
+
}
|
|
232
|
+
await assertSourceCurrent();
|
|
233
|
+
callerSignal?.throwIfAborted();
|
|
234
|
+
const peers = results.filter((result) => result.status === "fulfilled" && result.value).map((result) => result.value);
|
|
235
|
+
if (!peers.length) {
|
|
236
|
+
// A known local binding whose Host is stopped must not turn into a
|
|
237
|
+
// cloud send (and a runtime-renewal requirement) just because it is off.
|
|
238
|
+
if (!all && localTargetFound) throw Object.assign(new Error("local_peer_unavailable"), { code: "local_peer_unavailable" });
|
|
239
|
+
return absent();
|
|
240
|
+
}
|
|
241
|
+
if (!all && peers.length !== 1) throw fail();
|
|
242
|
+
// Selection is read-only. Once a visitor starts a claim or delivery, its
|
|
243
|
+
// errors must propagate; never swallow them as a failed discovery and
|
|
244
|
+
// retry the message through another transport.
|
|
245
|
+
const values = [];
|
|
246
|
+
for (const peer of peers) {
|
|
247
|
+
signal.throwIfAborted();
|
|
248
|
+
const { binding, target } = peer.loaded;
|
|
249
|
+
values.push(await visit({ owner, binding, target, discovery: peer.discovery, code: peer.code,
|
|
250
|
+
identity: peer.identity, signal,
|
|
251
|
+
async request(pathname, body) {
|
|
252
|
+
signal.throwIfAborted();
|
|
253
|
+
await assertSourceCurrent();
|
|
254
|
+
const remainingMs = Math.floor(deadline - performance.now());
|
|
255
|
+
if (remainingMs < 100) throw fail();
|
|
256
|
+
return request({ discovery: peer.discovery, capability: binding.capability, method: "POST",
|
|
257
|
+
pathname, body, timeoutMs: remainingMs, signal });
|
|
258
|
+
},
|
|
259
|
+
}));
|
|
260
|
+
if (!all) await assertSourceCurrent();
|
|
261
|
+
signal.throwIfAborted();
|
|
262
|
+
}
|
|
263
|
+
return all ? values : values[0];
|
|
264
|
+
} finally {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
for (const peer of loadedPeers) peer.binding.capability.secretBuffer?.fill(0);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function createLocalIdentityTrustProvider(options, dependencies = {}) {
|
|
272
|
+
const access = createLocalPeerAccess(options, dependencies);
|
|
273
|
+
return async (input) => [...new Set(await access(input, ({ code }) => code, { all: true }))];
|
|
274
|
+
}
|