@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,191 @@
|
|
|
1
|
+
import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
assertCredentialProfileId,
|
|
7
|
+
runWindowsClientTrustCommand,
|
|
8
|
+
} from "./hellolynk-platform-credential-store.mjs";
|
|
9
|
+
import { normalizeHelloLynkBaseUrl } from "./hellolynk-client-core.mjs";
|
|
10
|
+
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
containsHelloLynkCredentialMaterial,
|
|
14
|
+
normalizeHelloLynkClientProfileContainer,
|
|
15
|
+
} = runtimeSafety;
|
|
16
|
+
|
|
17
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/;
|
|
18
|
+
const SAFE_SCOPE = /^[a-z][a-z0-9_.:-]{0,127}$/;
|
|
19
|
+
const MAX_PROFILE_BYTES = 64 * 1024;
|
|
20
|
+
|
|
21
|
+
export function defaultHelloLynkClientRoot(platform = process.platform, env = process.env) {
|
|
22
|
+
const home = resolve(String(env.HOME || env.USERPROFILE || homedir()));
|
|
23
|
+
if (platform === "win32") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk");
|
|
24
|
+
if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk");
|
|
25
|
+
return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function safeId(value, code) {
|
|
29
|
+
const normalized = String(value || "").trim();
|
|
30
|
+
if (!SAFE_ID.test(normalized)) throw new Error(code);
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateProfile(value) {
|
|
35
|
+
const allowedScopes = Array.isArray(value.allowedScopes) ? value.allowedScopes.map((scope) => {
|
|
36
|
+
const normalized = String(scope || "");
|
|
37
|
+
if (!SAFE_SCOPE.test(normalized)) throw new Error("client_profile_scope_invalid");
|
|
38
|
+
return normalized;
|
|
39
|
+
}) : [];
|
|
40
|
+
const schemaVersion = value.schemaVersion == null ? 1 : Number(value.schemaVersion);
|
|
41
|
+
if (![1, 2].includes(schemaVersion)) throw new Error("client_profile_schema_unsupported");
|
|
42
|
+
const delegateId = value.delegateId == null
|
|
43
|
+
? null : safeId(value.delegateId, "client_profile_delegate_id_invalid");
|
|
44
|
+
const hostId = value.hostId == null ? null : safeId(value.hostId, "client_profile_host_id_invalid");
|
|
45
|
+
const authorizationVersion = value.authorizationVersion == null ? null : Number(value.authorizationVersion);
|
|
46
|
+
if (
|
|
47
|
+
(schemaVersion === 1 && (hostId != null || authorizationVersion != null))
|
|
48
|
+
|| (schemaVersion === 2 && (
|
|
49
|
+
!delegateId || !hostId || !Number.isSafeInteger(authorizationVersion) || authorizationVersion < 1
|
|
50
|
+
))
|
|
51
|
+
) throw new Error("client_profile_route_binding_invalid");
|
|
52
|
+
return {
|
|
53
|
+
contract: value.contract,
|
|
54
|
+
schemaVersion,
|
|
55
|
+
profileId: assertCredentialProfileId(value.profileId),
|
|
56
|
+
baseUrl: normalizeHelloLynkBaseUrl(value.baseUrl, { allowLoopbackHttp: value.allowLoopbackHttp === true }),
|
|
57
|
+
allowLoopbackHttp: value.allowLoopbackHttp === true,
|
|
58
|
+
deviceId: safeId(value.deviceId, "client_profile_device_id_invalid"),
|
|
59
|
+
credentialId: safeId(value.credentialId, "client_profile_credential_id_invalid"),
|
|
60
|
+
principalId: safeId(value.principalId, "client_profile_principal_id_invalid"),
|
|
61
|
+
hellolynkId: value.hellolynkId == null ? null : safeId(value.hellolynkId, "client_profile_hellolynk_id_invalid"),
|
|
62
|
+
agentId: safeId(value.agentId, "client_profile_agent_id_invalid"),
|
|
63
|
+
delegateId,
|
|
64
|
+
...(schemaVersion === 2 ? { hostId, authorizationVersion } : {}),
|
|
65
|
+
region: safeId(value.region, "client_profile_region_invalid"),
|
|
66
|
+
allowedScopes,
|
|
67
|
+
platform: String(value.platform || process.platform),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function atomicWriteJson(path, value) {
|
|
72
|
+
await mkdir(dirname(path), { recursive: true });
|
|
73
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
74
|
+
const body = `${JSON.stringify(value, null, 2)}\n`;
|
|
75
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
76
|
+
try {
|
|
77
|
+
await handle.writeFile(body, "utf8");
|
|
78
|
+
await handle.sync();
|
|
79
|
+
} finally {
|
|
80
|
+
await handle.close();
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await rename(temporary, path);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
await rm(temporary, { force: true });
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function windowsClientFile(path, category) {
|
|
91
|
+
const requested = String(path || "");
|
|
92
|
+
if (!requested || !isAbsolute(requested)) throw new Error("client_root_path_invalid");
|
|
93
|
+
const absolute = resolve(requested);
|
|
94
|
+
const parent = dirname(absolute);
|
|
95
|
+
const profileId = assertCredentialProfileId(basename(absolute, ".json"));
|
|
96
|
+
const clientRoot = dirname(parent);
|
|
97
|
+
if (
|
|
98
|
+
basename(parent).toLowerCase() !== category
|
|
99
|
+
|| absolute.toLowerCase() !== join(clientRoot, category, `${profileId}.json`).toLowerCase()
|
|
100
|
+
) throw new Error("client_root_path_invalid");
|
|
101
|
+
return { clientRoot, profileId };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function writeWindowsClientJson(path, category, command, value) {
|
|
105
|
+
const target = windowsClientFile(path, category);
|
|
106
|
+
await runWindowsClientTrustCommand(command, {
|
|
107
|
+
...target,
|
|
108
|
+
stdin: `${JSON.stringify(value, null, 2)}\n`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function writeClientProfile(path, value) {
|
|
113
|
+
const original = JSON.stringify(value);
|
|
114
|
+
if (containsHelloLynkCredentialMaterial(original)) {
|
|
115
|
+
throw new Error("client_profile_contains_secret");
|
|
116
|
+
}
|
|
117
|
+
const container = normalizeHelloLynkClientProfileContainer(value);
|
|
118
|
+
const profile = validateProfile(container.value);
|
|
119
|
+
const serialized = JSON.stringify(profile);
|
|
120
|
+
if (containsHelloLynkCredentialMaterial(serialized)) {
|
|
121
|
+
throw new Error("client_profile_contains_secret");
|
|
122
|
+
}
|
|
123
|
+
if (process.platform === "win32") {
|
|
124
|
+
await writeWindowsClientJson(path, "profiles", "StoreProfile", profile);
|
|
125
|
+
} else {
|
|
126
|
+
await atomicWriteJson(path, profile);
|
|
127
|
+
}
|
|
128
|
+
return profile;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function readClientProfile(path) {
|
|
132
|
+
const body = await readClientProfileBody(path);
|
|
133
|
+
const container = normalizeHelloLynkClientProfileContainer(JSON.parse(body));
|
|
134
|
+
return validateProfile(container.value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A profile holds no secret, so Windows reads it directly too, avoiding
|
|
138
|
+
// one helper process per profile. Writes keep the helper.
|
|
139
|
+
async function readClientProfileBody(path) {
|
|
140
|
+
const windows = process.platform === "win32";
|
|
141
|
+
if (windows) windowsClientFile(path, "profiles");
|
|
142
|
+
let metadata;
|
|
143
|
+
try { metadata = await (windows ? lstat : stat)(path); }
|
|
144
|
+
catch (error) { throw windows && error?.code === "ENOENT" ? new Error("client_profile_missing") : error; }
|
|
145
|
+
if (!metadata.isFile()) throw new Error("client_profile_not_regular_file");
|
|
146
|
+
if (metadata.size > MAX_PROFILE_BYTES) throw new Error("client_profile_too_large");
|
|
147
|
+
const bytes = await readFile(path);
|
|
148
|
+
if (bytes.length > MAX_PROFILE_BYTES) throw new Error("client_profile_too_large");
|
|
149
|
+
let body;
|
|
150
|
+
try { body = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
|
151
|
+
catch { throw new Error("client_profile_invalid_utf8"); }
|
|
152
|
+
if (containsHelloLynkCredentialMaterial(body)) throw new Error("client_profile_contains_secret");
|
|
153
|
+
return body;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function writeSanitizedHostStatus(path, value) {
|
|
157
|
+
const runtimeSessionExpiresAt = value.runtimeSessionExpiresAt == null
|
|
158
|
+
? null
|
|
159
|
+
: new Date(value.runtimeSessionExpiresAt).toISOString();
|
|
160
|
+
const status = {
|
|
161
|
+
schemaVersion: 1,
|
|
162
|
+
checkedAt: new Date(value.checkedAt || Date.now()).toISOString(),
|
|
163
|
+
ok: value.ok === true,
|
|
164
|
+
serviceReachable: value.serviceReachable === true,
|
|
165
|
+
credentialPresent: value.credentialPresent === true,
|
|
166
|
+
runtimeSessionReady: value.runtimeSessionReady === true,
|
|
167
|
+
runtimeSessionExpiresAt,
|
|
168
|
+
messageReceiverReady: value.messageReceiverReady === true,
|
|
169
|
+
lastPollStatus: ["no_messages", "events_projected", "receiver_error"].includes(value.lastPollStatus)
|
|
170
|
+
? value.lastPollStatus
|
|
171
|
+
: null,
|
|
172
|
+
newEventCount: Number.isSafeInteger(Number(value.newEventCount)) && Number(value.newEventCount) >= 0
|
|
173
|
+
? Number(value.newEventCount)
|
|
174
|
+
: 0,
|
|
175
|
+
newNotificationCount: Number.isSafeInteger(Number(value.newNotificationCount)) && Number(value.newNotificationCount) >= 0
|
|
176
|
+
? Number(value.newNotificationCount)
|
|
177
|
+
: 0,
|
|
178
|
+
platform: String(value.platform || process.platform),
|
|
179
|
+
errorCode: value.errorCode == null ? null : String(value.errorCode),
|
|
180
|
+
};
|
|
181
|
+
const serialized = JSON.stringify(status);
|
|
182
|
+
if (containsHelloLynkCredentialMaterial(serialized)) {
|
|
183
|
+
throw new Error("host_status_contains_secret");
|
|
184
|
+
}
|
|
185
|
+
if (process.platform === "win32") {
|
|
186
|
+
await writeWindowsClientJson(path, "status", "StoreStatus", status);
|
|
187
|
+
} else {
|
|
188
|
+
await atomicWriteJson(path, status);
|
|
189
|
+
}
|
|
190
|
+
return status;
|
|
191
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const HELLOLYNK_HOST_REQUEST_AUTH_DOMAIN = "hellolynk-host-request-v1";
|
|
4
|
+
export const HELLOLYNK_HOST_REQUEST_MAX_SKEW_MS = 30_000;
|
|
5
|
+
export const HELLOLYNK_HOST_NONCE_TTL_MS = 90_000;
|
|
6
|
+
export const HELLOLYNK_HOST_NONCE_CACHE_MAX_ENTRIES = 4_096;
|
|
7
|
+
// One installed capability is shared by every open conversation, receiver and
|
|
8
|
+
// card. Its replay history may use the existing Host-wide budget; a smaller
|
|
9
|
+
// default quota throttles ordinary reads and then rejects unrelated new sends.
|
|
10
|
+
export const HELLOLYNK_HOST_NONCE_CACHE_MAX_ENTRIES_PER_CAPABILITY = HELLOLYNK_HOST_NONCE_CACHE_MAX_ENTRIES;
|
|
11
|
+
export const HELLOLYNK_HOST_CAPABILITY_CONTRACT = "hellolynk-host-capability-v1";
|
|
12
|
+
|
|
13
|
+
export const HELLOLYNK_HOST_AUTH_HEADERS = Object.freeze({
|
|
14
|
+
capabilityId: "x-hellolynk-host-capability-id",
|
|
15
|
+
adapterId: "x-hellolynk-host-adapter-id",
|
|
16
|
+
hostGeneration: "x-hellolynk-host-generation",
|
|
17
|
+
timestampMs: "x-hellolynk-host-timestamp",
|
|
18
|
+
nonce: "x-hellolynk-host-nonce",
|
|
19
|
+
proof: "x-hellolynk-host-proof",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,126}[A-Za-z0-9])?$/;
|
|
23
|
+
const GENERATION_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,126}[A-Za-z0-9])?$/;
|
|
24
|
+
const NONCE_PATTERN = /^[A-Za-z0-9_-]{22,128}$/;
|
|
25
|
+
const PROOF_PATTERN = /^[a-f0-9]{64}$/;
|
|
26
|
+
|
|
27
|
+
export function sha256RawBody(rawBody) {
|
|
28
|
+
return crypto.createHash("sha256").update(asRawBody(rawBody)).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createHostRequestAuth(input) {
|
|
32
|
+
const normalized = normalizeRequestAuthInput(input);
|
|
33
|
+
const proof = crypto
|
|
34
|
+
.createHmac("sha256", normalized.secretBuffer)
|
|
35
|
+
.update(normalized.message, "utf8")
|
|
36
|
+
.digest("hex");
|
|
37
|
+
return {
|
|
38
|
+
capabilityId: normalized.capabilityId,
|
|
39
|
+
adapterId: normalized.adapterId,
|
|
40
|
+
hostGeneration: normalized.hostGeneration,
|
|
41
|
+
timestampMs: normalized.timestampMs,
|
|
42
|
+
nonce: normalized.nonce,
|
|
43
|
+
bodySha256: normalized.bodySha256,
|
|
44
|
+
proof,
|
|
45
|
+
headers: {
|
|
46
|
+
[HELLOLYNK_HOST_AUTH_HEADERS.capabilityId]: normalized.capabilityId,
|
|
47
|
+
[HELLOLYNK_HOST_AUTH_HEADERS.adapterId]: normalized.adapterId,
|
|
48
|
+
[HELLOLYNK_HOST_AUTH_HEADERS.hostGeneration]: normalized.hostGeneration,
|
|
49
|
+
[HELLOLYNK_HOST_AUTH_HEADERS.timestampMs]: String(normalized.timestampMs),
|
|
50
|
+
[HELLOLYNK_HOST_AUTH_HEADERS.nonce]: normalized.nonce,
|
|
51
|
+
[HELLOLYNK_HOST_AUTH_HEADERS.proof]: proof,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function hostRequestAuthMatches(input) {
|
|
57
|
+
try {
|
|
58
|
+
const expected = createHostRequestAuth(input).proof;
|
|
59
|
+
const candidate = String(input?.proof || "").toLowerCase();
|
|
60
|
+
if (!PROOF_PATTERN.test(candidate)) return false;
|
|
61
|
+
return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(candidate, "hex"));
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function canonicalHostRequestMessage(input) {
|
|
68
|
+
return normalizeRequestAuthInput(input).message;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createHostNonce(bytes = 24) {
|
|
72
|
+
if (!Number.isSafeInteger(bytes) || bytes < 16 || bytes > 64) {
|
|
73
|
+
throw codeError("host_nonce_size_invalid");
|
|
74
|
+
}
|
|
75
|
+
return crypto.randomBytes(bytes).toString("base64url");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createNonceReplayGuard(options = {}) {
|
|
79
|
+
const now = normalizeNow(options.clock);
|
|
80
|
+
const ttlMs = normalizePositiveInteger(options.ttlMs, HELLOLYNK_HOST_NONCE_TTL_MS, 1_000, 1_800_001);
|
|
81
|
+
const maxEntries = normalizePositiveInteger(
|
|
82
|
+
options.maxEntries,
|
|
83
|
+
HELLOLYNK_HOST_NONCE_CACHE_MAX_ENTRIES,
|
|
84
|
+
1,
|
|
85
|
+
65_536,
|
|
86
|
+
);
|
|
87
|
+
const maxEntriesPerCapability = normalizePositiveInteger(
|
|
88
|
+
options.maxEntriesPerCapability,
|
|
89
|
+
Math.min(HELLOLYNK_HOST_NONCE_CACHE_MAX_ENTRIES_PER_CAPABILITY, maxEntries),
|
|
90
|
+
1,
|
|
91
|
+
maxEntries,
|
|
92
|
+
);
|
|
93
|
+
const entries = new Map();
|
|
94
|
+
const capabilityEntryCounts = new Map();
|
|
95
|
+
|
|
96
|
+
function consume(capabilityId, nonce, timestampMs) {
|
|
97
|
+
const normalizedCapabilityId = assertIdentifier(capabilityId, "host_capability_id_invalid");
|
|
98
|
+
const normalizedNonce = assertNonce(nonce);
|
|
99
|
+
const normalizedTimestamp = assertTimestamp(timestampMs);
|
|
100
|
+
const current = assertTimestamp(now());
|
|
101
|
+
prune(current);
|
|
102
|
+
const key = `${normalizedCapabilityId}\0${normalizedNonce}`;
|
|
103
|
+
if (entries.has(key)) return false;
|
|
104
|
+
const capabilityEntryCount = capabilityEntryCounts.get(normalizedCapabilityId) || 0;
|
|
105
|
+
if (capabilityEntryCount >= maxEntriesPerCapability) {
|
|
106
|
+
throw codeError("host_nonce_capability_cache_full");
|
|
107
|
+
}
|
|
108
|
+
if (entries.size >= maxEntries) throw codeError("host_nonce_cache_full");
|
|
109
|
+
entries.set(key, {
|
|
110
|
+
capabilityId: normalizedCapabilityId,
|
|
111
|
+
expiresAt: Math.max(current, normalizedTimestamp) + ttlMs,
|
|
112
|
+
});
|
|
113
|
+
capabilityEntryCounts.set(normalizedCapabilityId, capabilityEntryCount + 1);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function prune(current = assertTimestamp(now())) {
|
|
118
|
+
for (const [key, entry] of entries) {
|
|
119
|
+
if (entry.expiresAt <= current) {
|
|
120
|
+
entries.delete(key);
|
|
121
|
+
const count = capabilityEntryCounts.get(entry.capabilityId) || 0;
|
|
122
|
+
if (count <= 1) capabilityEntryCounts.delete(entry.capabilityId);
|
|
123
|
+
else capabilityEntryCounts.set(entry.capabilityId, count - 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
consume,
|
|
130
|
+
prune,
|
|
131
|
+
get size() {
|
|
132
|
+
prune();
|
|
133
|
+
return entries.size;
|
|
134
|
+
},
|
|
135
|
+
sizeForCapability(capabilityId) {
|
|
136
|
+
prune();
|
|
137
|
+
return capabilityEntryCounts.get(assertIdentifier(capabilityId, "host_capability_id_invalid")) || 0;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function assertRequestFresh(timestampMs, nowMs, maxSkewMs = HELLOLYNK_HOST_REQUEST_MAX_SKEW_MS) {
|
|
143
|
+
const timestamp = assertTimestamp(timestampMs);
|
|
144
|
+
const current = assertTimestamp(nowMs);
|
|
145
|
+
const allowedSkew = normalizePositiveInteger(maxSkewMs, HELLOLYNK_HOST_REQUEST_MAX_SKEW_MS, 1, 300_000);
|
|
146
|
+
if (Math.abs(current - timestamp) > allowedSkew) throw codeError("host_request_stale");
|
|
147
|
+
return timestamp;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function assertCapabilityRecord(value) {
|
|
151
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
152
|
+
throw codeError("host_capability_invalid");
|
|
153
|
+
}
|
|
154
|
+
const expectedKeys = [
|
|
155
|
+
"adapterId",
|
|
156
|
+
"capabilityId",
|
|
157
|
+
"contract",
|
|
158
|
+
"delegateId",
|
|
159
|
+
"hostInstallationId",
|
|
160
|
+
"issuedAt",
|
|
161
|
+
"principalId",
|
|
162
|
+
"region",
|
|
163
|
+
"revokedAt",
|
|
164
|
+
"scopes",
|
|
165
|
+
"secretBuffer",
|
|
166
|
+
"state",
|
|
167
|
+
"version",
|
|
168
|
+
];
|
|
169
|
+
const actualKeys = Object.keys(value).sort();
|
|
170
|
+
if (
|
|
171
|
+
actualKeys.length !== expectedKeys.length
|
|
172
|
+
|| actualKeys.some((key, index) => key !== expectedKeys[index])
|
|
173
|
+
) {
|
|
174
|
+
throw codeError("host_capability_fields_invalid");
|
|
175
|
+
}
|
|
176
|
+
if (value.contract !== HELLOLYNK_HOST_CAPABILITY_CONTRACT) {
|
|
177
|
+
throw codeError("host_capability_contract_invalid");
|
|
178
|
+
}
|
|
179
|
+
const capabilityId = assertIdentifier(value.capabilityId, "host_capability_id_invalid");
|
|
180
|
+
const region = assertIdentifier(value.region, "host_capability_region_invalid");
|
|
181
|
+
const principalId = assertIdentifier(value.principalId, "host_principal_id_invalid");
|
|
182
|
+
const adapterId = assertIdentifier(value.adapterId, "host_adapter_id_invalid");
|
|
183
|
+
const delegateId = assertIdentifier(value.delegateId, "host_delegate_id_invalid");
|
|
184
|
+
const hostInstallationId = assertIdentifier(
|
|
185
|
+
value.hostInstallationId,
|
|
186
|
+
"host_installation_id_invalid",
|
|
187
|
+
);
|
|
188
|
+
const version = Number(value.version);
|
|
189
|
+
if (!Number.isSafeInteger(version) || version < 1) throw codeError("host_capability_version_invalid");
|
|
190
|
+
if (value.state !== "active") throw codeError("host_capability_inactive");
|
|
191
|
+
const issuedAt = assertTimestamp(value.issuedAt);
|
|
192
|
+
if (value.revokedAt !== null) throw codeError("host_capability_revoked_at_invalid");
|
|
193
|
+
if (!Array.isArray(value.scopes) || value.scopes.length > 64) {
|
|
194
|
+
throw codeError("host_capability_scopes_invalid");
|
|
195
|
+
}
|
|
196
|
+
const scopes = value.scopes.map((scope) => (
|
|
197
|
+
assertIdentifier(scope, "host_capability_scope_invalid")
|
|
198
|
+
));
|
|
199
|
+
const canonicalScopes = [...new Set(scopes)].sort();
|
|
200
|
+
if (
|
|
201
|
+
canonicalScopes.length !== scopes.length
|
|
202
|
+
|| canonicalScopes.some((scope, index) => scope !== scopes[index])
|
|
203
|
+
) {
|
|
204
|
+
throw codeError("host_capability_scopes_noncanonical");
|
|
205
|
+
}
|
|
206
|
+
const secretBuffer = assertSecretBuffer(value.secretBuffer);
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
contract: HELLOLYNK_HOST_CAPABILITY_CONTRACT,
|
|
209
|
+
capabilityId,
|
|
210
|
+
region,
|
|
211
|
+
principalId,
|
|
212
|
+
delegateId,
|
|
213
|
+
hostInstallationId,
|
|
214
|
+
adapterId,
|
|
215
|
+
version,
|
|
216
|
+
state: "active",
|
|
217
|
+
scopes: Object.freeze(scopes),
|
|
218
|
+
issuedAt,
|
|
219
|
+
revokedAt: null,
|
|
220
|
+
secretBuffer,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function assertIdentifier(value, errorCode = "host_identifier_invalid") {
|
|
225
|
+
const identifier = String(value || "");
|
|
226
|
+
if (!IDENTIFIER_PATTERN.test(identifier)) throw codeError(errorCode);
|
|
227
|
+
return identifier;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function assertHostGeneration(value) {
|
|
231
|
+
const generation = String(value || "");
|
|
232
|
+
if (!GENERATION_PATTERN.test(generation)) throw codeError("host_generation_invalid");
|
|
233
|
+
return generation;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function assertNonce(value) {
|
|
237
|
+
const nonce = String(value || "");
|
|
238
|
+
if (!NONCE_PATTERN.test(nonce)) throw codeError("host_nonce_invalid");
|
|
239
|
+
return nonce;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function assertPathname(value) {
|
|
243
|
+
const pathname = String(value || "");
|
|
244
|
+
if (
|
|
245
|
+
!pathname.startsWith("/")
|
|
246
|
+
|| pathname.length > 512
|
|
247
|
+
|| pathname.includes("?")
|
|
248
|
+
|| pathname.includes("#")
|
|
249
|
+
|| pathname.includes("\r")
|
|
250
|
+
|| pathname.includes("\n")
|
|
251
|
+
|| pathname.includes("\\")
|
|
252
|
+
) {
|
|
253
|
+
throw codeError("host_pathname_invalid");
|
|
254
|
+
}
|
|
255
|
+
return pathname;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function assertMethod(value) {
|
|
259
|
+
const method = String(value || "");
|
|
260
|
+
if (!/^[A-Z]{3,12}$/.test(method)) throw codeError("host_method_invalid");
|
|
261
|
+
return method;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function asRawBody(value) {
|
|
265
|
+
if (Buffer.isBuffer(value)) return value;
|
|
266
|
+
if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
267
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
268
|
+
if (value == null) return Buffer.alloc(0);
|
|
269
|
+
throw codeError("host_raw_body_invalid");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeRequestAuthInput(input = {}) {
|
|
273
|
+
const capabilityId = assertIdentifier(input.capabilityId, "host_capability_id_invalid");
|
|
274
|
+
const adapterId = assertIdentifier(input.adapterId, "host_adapter_id_invalid");
|
|
275
|
+
const method = assertMethod(input.method);
|
|
276
|
+
const pathname = assertPathname(input.pathname);
|
|
277
|
+
const hostGeneration = assertHostGeneration(input.hostGeneration);
|
|
278
|
+
const timestampMs = assertTimestamp(input.timestampMs);
|
|
279
|
+
const nonce = assertNonce(input.nonce);
|
|
280
|
+
const secretBuffer = assertSecretBuffer(input.secretBuffer);
|
|
281
|
+
const bodySha256 = sha256RawBody(input.rawBody);
|
|
282
|
+
const message = [
|
|
283
|
+
HELLOLYNK_HOST_REQUEST_AUTH_DOMAIN,
|
|
284
|
+
capabilityId,
|
|
285
|
+
adapterId,
|
|
286
|
+
method,
|
|
287
|
+
pathname,
|
|
288
|
+
hostGeneration,
|
|
289
|
+
String(timestampMs),
|
|
290
|
+
nonce,
|
|
291
|
+
bodySha256,
|
|
292
|
+
].join("\n");
|
|
293
|
+
return {
|
|
294
|
+
capabilityId,
|
|
295
|
+
adapterId,
|
|
296
|
+
method,
|
|
297
|
+
pathname,
|
|
298
|
+
hostGeneration,
|
|
299
|
+
timestampMs,
|
|
300
|
+
nonce,
|
|
301
|
+
secretBuffer,
|
|
302
|
+
bodySha256,
|
|
303
|
+
message,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function assertSecretBuffer(value) {
|
|
308
|
+
const secret = Buffer.isBuffer(value)
|
|
309
|
+
? Buffer.from(value)
|
|
310
|
+
: value instanceof Uint8Array
|
|
311
|
+
? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
312
|
+
: null;
|
|
313
|
+
if (!secret || secret.length < 32 || secret.length > 128) {
|
|
314
|
+
throw codeError("host_capability_secret_invalid");
|
|
315
|
+
}
|
|
316
|
+
return secret;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function assertTimestamp(value) {
|
|
320
|
+
if (typeof value !== "number" && !/^(?:0|[1-9][0-9]{0,15})$/.test(String(value || ""))) {
|
|
321
|
+
throw codeError("host_timestamp_invalid");
|
|
322
|
+
}
|
|
323
|
+
const timestamp = typeof value === "number" ? value : Number(value);
|
|
324
|
+
if (!Number.isSafeInteger(timestamp) || timestamp < 0) throw codeError("host_timestamp_invalid");
|
|
325
|
+
return timestamp;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeNow(clock) {
|
|
329
|
+
if (clock == null) return () => Date.now();
|
|
330
|
+
if (typeof clock === "function") return clock;
|
|
331
|
+
if (typeof clock?.now === "function") return () => clock.now();
|
|
332
|
+
throw codeError("host_clock_invalid");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function normalizePositiveInteger(value, fallback, minimum, maximum) {
|
|
336
|
+
const normalized = Number(value ?? fallback);
|
|
337
|
+
if (!Number.isSafeInteger(normalized) || normalized < minimum || normalized > maximum) {
|
|
338
|
+
throw codeError("host_limit_invalid");
|
|
339
|
+
}
|
|
340
|
+
return normalized;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function codeError(code) {
|
|
344
|
+
const error = new Error(code);
|
|
345
|
+
error.code = code;
|
|
346
|
+
return error;
|
|
347
|
+
}
|