@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,747 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
createHmac,
|
|
4
|
+
createCipheriv,
|
|
5
|
+
createDecipheriv,
|
|
6
|
+
randomBytes as cryptoRandomBytes,
|
|
7
|
+
} from "node:crypto";
|
|
8
|
+
import { lstat, mkdir, open } from "node:fs/promises";
|
|
9
|
+
import { createConnection } from "node:net";
|
|
10
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { TextDecoder } from "node:util";
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
16
|
+
|
|
17
|
+
const { sanitizeHelloLynkChildEnvironment } = runtimeSafety;
|
|
18
|
+
|
|
19
|
+
export const HELLOLYNK_LOCAL_DATA_ENVELOPE_CONTRACT = "hellolynk-local-data-envelope-v1";
|
|
20
|
+
export const HELLOLYNK_LOCAL_DATA_ENVELOPE_SCHEMA_VERSION = 1;
|
|
21
|
+
export const HELLOLYNK_LOCAL_DATA_ENVELOPE_ALGORITHM = "A256GCM";
|
|
22
|
+
export const HELLOLYNK_LOCAL_DATA_KEY_RECORD_CONTRACT = "hellolynk-local-data-key-v1";
|
|
23
|
+
|
|
24
|
+
const CURRENT_LOCAL_DATA_MIGRATION_CONTRACT = "hellolynk-local-data-migration-v1";
|
|
25
|
+
|
|
26
|
+
const ENVELOPE_FIELDS = Object.freeze([
|
|
27
|
+
"algorithm",
|
|
28
|
+
"ciphertext",
|
|
29
|
+
"contract",
|
|
30
|
+
"keyId",
|
|
31
|
+
"nonce",
|
|
32
|
+
"schemaVersion",
|
|
33
|
+
"tag",
|
|
34
|
+
]);
|
|
35
|
+
const ENVELOPE_MARKER_FIELDS = new Set([
|
|
36
|
+
"algorithm",
|
|
37
|
+
"ciphertext",
|
|
38
|
+
"keyId",
|
|
39
|
+
"nonce",
|
|
40
|
+
"tag",
|
|
41
|
+
]);
|
|
42
|
+
const KEY_ID_PATTERN = /^ldk_[A-Za-z0-9_-]{22}$/;
|
|
43
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]*$/;
|
|
44
|
+
const MIGRATION_STATE_FILE = "local-data-migration-v1.json";
|
|
45
|
+
const WINDOWS_KEY_FILE = "local-data-key-v1.dpapi.json";
|
|
46
|
+
const MAX_KEY_STORE_BYTES = 64 * 1024;
|
|
47
|
+
const HELPER_TIMEOUT_MS = 30_000;
|
|
48
|
+
const WINDOWS_HELPER_PATH = join(
|
|
49
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
50
|
+
"..",
|
|
51
|
+
"platform",
|
|
52
|
+
"windows",
|
|
53
|
+
"hellolynk-local-data-key.ps1",
|
|
54
|
+
);
|
|
55
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
56
|
+
|
|
57
|
+
export function createLocalDataProtector(options = {}) {
|
|
58
|
+
const binding = Object.freeze({
|
|
59
|
+
region: assertBindingValue(options.region, "local_data_region_invalid"),
|
|
60
|
+
principalId: assertBindingValue(options.principalId, "local_data_principal_id_invalid"),
|
|
61
|
+
hostInstallationId: assertBindingValue(
|
|
62
|
+
options.hostInstallationId,
|
|
63
|
+
"local_data_host_installation_id_invalid",
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
const keyProvider = assertKeyProvider(options.keyProvider);
|
|
67
|
+
const random = typeof options.randomBytes === "function" ? options.randomBytes : cryptoRandomBytes;
|
|
68
|
+
|
|
69
|
+
async function ensureKey() {
|
|
70
|
+
let record;
|
|
71
|
+
try {
|
|
72
|
+
record = await keyProvider.getOrCreate();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw normalizeKeyProviderError(error);
|
|
75
|
+
}
|
|
76
|
+
return normalizeKeyRecord(record);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function sealJson(value, context = {}) {
|
|
80
|
+
const serialized = serializeJson(value);
|
|
81
|
+
const record = await ensureKey();
|
|
82
|
+
try {
|
|
83
|
+
return sealBytes(Buffer.from(serialized, "utf8"), record, binding, context, random);
|
|
84
|
+
} finally {
|
|
85
|
+
record.key.fill(0);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function assertKeyAvailable(keyId) {
|
|
90
|
+
const expectedKeyId = assertKeyId(keyId);
|
|
91
|
+
let record;
|
|
92
|
+
try {
|
|
93
|
+
record = await keyProvider.load(expectedKeyId);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw normalizeKeyProviderError(error);
|
|
96
|
+
}
|
|
97
|
+
if (record == null) throw codeError("local_data_key_missing");
|
|
98
|
+
const normalized = normalizeKeyRecord(record);
|
|
99
|
+
try {
|
|
100
|
+
if (normalized.keyId !== expectedKeyId) throw codeError("local_data_key_id_mismatch");
|
|
101
|
+
return Object.freeze({ keyId: expectedKeyId, available: true });
|
|
102
|
+
} finally {
|
|
103
|
+
normalized.key.fill(0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function openJson(value, context = {}, openOptions = {}) {
|
|
108
|
+
if (!looksLikeEnvelope(value)) {
|
|
109
|
+
if (openOptions.requireProtected !== false) {
|
|
110
|
+
throw codeError("local_data_plaintext_forbidden");
|
|
111
|
+
}
|
|
112
|
+
return cloneJson(value);
|
|
113
|
+
}
|
|
114
|
+
const envelope = assertEnvelope(value).envelope;
|
|
115
|
+
let record;
|
|
116
|
+
try {
|
|
117
|
+
record = await keyProvider.load(envelope.keyId);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw normalizeKeyProviderError(error);
|
|
120
|
+
}
|
|
121
|
+
if (record == null) throw codeError("local_data_key_missing");
|
|
122
|
+
const normalized = normalizeKeyRecord(record);
|
|
123
|
+
let plain;
|
|
124
|
+
try {
|
|
125
|
+
if (normalized.keyId !== envelope.keyId) throw codeError("local_data_key_id_mismatch");
|
|
126
|
+
plain = openBytes(envelope, normalized, binding, context);
|
|
127
|
+
} finally {
|
|
128
|
+
normalized.key.fill(0);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(utf8Decoder.decode(plain));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error?.code) throw error;
|
|
134
|
+
throw codeError("local_data_plaintext_json_invalid");
|
|
135
|
+
} finally {
|
|
136
|
+
plain.fill(0);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function sealArchiveLine(value, context = {}) {
|
|
141
|
+
return JSON.stringify(await sealJson(value, context));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function openArchiveLine(line, context = {}, openOptions = {}) {
|
|
145
|
+
if (typeof line !== "string" || !line || line.includes("\n") || line.includes("\r")) {
|
|
146
|
+
throw codeError("local_data_archive_line_invalid");
|
|
147
|
+
}
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(line);
|
|
151
|
+
} catch {
|
|
152
|
+
throw codeError("local_data_archive_line_invalid");
|
|
153
|
+
}
|
|
154
|
+
return openJson(parsed, context, openOptions);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The former encrypted index keyed its identifiers with this derivation. The
|
|
158
|
+
// one-time plaintext conversion stores it so existing rows keep their ids.
|
|
159
|
+
async function deriveIndexKey(keyId, context = {}) {
|
|
160
|
+
const expectedKeyId = assertKeyId(keyId);
|
|
161
|
+
const normalizedContext = normalizeContext(context);
|
|
162
|
+
let record;
|
|
163
|
+
try {
|
|
164
|
+
record = await keyProvider.load(expectedKeyId);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw normalizeKeyProviderError(error);
|
|
167
|
+
}
|
|
168
|
+
if (record == null) throw codeError("local_data_key_missing");
|
|
169
|
+
const normalized = normalizeKeyRecord(record);
|
|
170
|
+
try {
|
|
171
|
+
if (normalized.keyId !== expectedKeyId) throw codeError("local_data_key_id_mismatch");
|
|
172
|
+
return createHmac("sha256", normalized.key).update(JSON.stringify([
|
|
173
|
+
"hellolynk-local-index-key-v1", binding, normalizedContext,
|
|
174
|
+
])).digest();
|
|
175
|
+
} finally {
|
|
176
|
+
normalized.key.fill(0);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
contract: HELLOLYNK_LOCAL_DATA_ENVELOPE_CONTRACT,
|
|
182
|
+
ensureKey,
|
|
183
|
+
assertKeyAvailable,
|
|
184
|
+
sealJson,
|
|
185
|
+
openJson,
|
|
186
|
+
sealArchiveLine,
|
|
187
|
+
openArchiveLine,
|
|
188
|
+
deriveIndexKey,
|
|
189
|
+
withKeyBatch(operation) {
|
|
190
|
+
if (typeof operation !== "function") throw codeError("local_data_key_batch_invalid");
|
|
191
|
+
return typeof keyProvider.withBatch === "function" ? keyProvider.withBatch(operation) : operation();
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function createMemoryLocalDataKeyProvider(options = {}) {
|
|
197
|
+
const random = typeof options.randomBytes === "function" ? options.randomBytes : cryptoRandomBytes;
|
|
198
|
+
let current = options.record == null ? null : normalizeKeyRecord(options.record);
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
async getOrCreate() {
|
|
201
|
+
if (current == null) {
|
|
202
|
+
current = normalizeKeyRecord({
|
|
203
|
+
keyId: `ldk_${randomExact(random, 16, "local_data_key_generation_failed").toString("base64url")}`,
|
|
204
|
+
key: randomExact(random, 32, "local_data_key_generation_failed"),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return copyKeyRecord(current);
|
|
208
|
+
},
|
|
209
|
+
async load(keyId) {
|
|
210
|
+
const id = assertKeyId(keyId);
|
|
211
|
+
return current?.keyId === id ? copyKeyRecord(current) : null;
|
|
212
|
+
},
|
|
213
|
+
clear() {
|
|
214
|
+
if (current) current.key.fill(0);
|
|
215
|
+
current = null;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function assertLocalDataProtectionAccess(root, protectedAccess) {
|
|
221
|
+
const acceptedRoot = assertAbsoluteRoot(root);
|
|
222
|
+
const state = await readProtectionState(join(acceptedRoot, MIGRATION_STATE_FILE));
|
|
223
|
+
if (!state) return Object.freeze({ required: false });
|
|
224
|
+
if (protectedAccess !== true) throw codeError("local_data_protection_required");
|
|
225
|
+
if (state.phase !== "committed") throw codeError("local_data_migration_incomplete");
|
|
226
|
+
return Object.freeze({ required: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function readProtectionState(statePath) {
|
|
230
|
+
let handle;
|
|
231
|
+
let contract;
|
|
232
|
+
let phase;
|
|
233
|
+
try {
|
|
234
|
+
const before = await lstat(statePath);
|
|
235
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size < 2 || before.size > 1024 * 1024) {
|
|
236
|
+
throw codeError("local_data_migration_state_invalid");
|
|
237
|
+
}
|
|
238
|
+
handle = await open(statePath, "r");
|
|
239
|
+
const opened = await handle.stat();
|
|
240
|
+
if (!sameFile(before, opened) || opened.size > 1024 * 1024) {
|
|
241
|
+
throw codeError("local_data_migration_state_invalid");
|
|
242
|
+
}
|
|
243
|
+
const value = JSON.parse(await handle.readFile("utf8"));
|
|
244
|
+
const fields = Object.keys(value || {}).sort().join(",");
|
|
245
|
+
if (fields !== "assets,bindingDigest,contract,keyId,phase,schemaVersion"
|
|
246
|
+
|| value.contract !== CURRENT_LOCAL_DATA_MIGRATION_CONTRACT || value.schemaVersion !== 1
|
|
247
|
+
|| !["preparing", "staged", "committed"].includes(value.phase)) {
|
|
248
|
+
throw codeError("local_data_migration_state_invalid");
|
|
249
|
+
}
|
|
250
|
+
contract = value.contract;
|
|
251
|
+
phase = value.phase;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error?.code === "ENOENT") return null;
|
|
254
|
+
if (typeof error?.code === "string" && /^local_data_[a-z0-9_]+$/.test(error.code)) throw error;
|
|
255
|
+
throw codeError("local_data_migration_state_invalid");
|
|
256
|
+
} finally {
|
|
257
|
+
await handle?.close().catch(() => {});
|
|
258
|
+
}
|
|
259
|
+
return Object.freeze({ contract, phase });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function createWindowsLocalDataKeyProvider(options = {}) {
|
|
263
|
+
const platform = options.platform || process.platform;
|
|
264
|
+
if (platform !== "win32") throw codeError("local_data_key_platform_unsupported");
|
|
265
|
+
const root = assertAbsoluteRoot(options.root);
|
|
266
|
+
const helperPath = resolve(options.helperPath || WINDOWS_HELPER_PATH);
|
|
267
|
+
const invoke = options.invoke || ((command) => invokeWindowsDataKey({ command, root, helperPath }));
|
|
268
|
+
let current = null;
|
|
269
|
+
let durableFingerprint = null;
|
|
270
|
+
const batches = new AsyncLocalStorage();
|
|
271
|
+
|
|
272
|
+
async function rememberDurableSlot() {
|
|
273
|
+
try {
|
|
274
|
+
durableFingerprint = await keySlotFingerprint(root);
|
|
275
|
+
const batch = batches.getStore();
|
|
276
|
+
if (batch?.active) batch.record = current;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
current?.key.fill(0);
|
|
279
|
+
current = null;
|
|
280
|
+
durableFingerprint = null;
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function assertDurableSlotUnchanged() {
|
|
286
|
+
const batch = batches.getStore();
|
|
287
|
+
if (batch?.active && current != null && batch.record === current) return;
|
|
288
|
+
let observed;
|
|
289
|
+
try {
|
|
290
|
+
observed = await keySlotFingerprint(root);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
current?.key.fill(0);
|
|
293
|
+
current = null;
|
|
294
|
+
durableFingerprint = null;
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
if (durableFingerprint == null || observed !== durableFingerprint) {
|
|
298
|
+
current?.key.fill(0);
|
|
299
|
+
current = null;
|
|
300
|
+
durableFingerprint = null;
|
|
301
|
+
throw codeError("local_data_key_store_changed");
|
|
302
|
+
}
|
|
303
|
+
if (batch?.active) batch.record = current;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
async withBatch(operation) {
|
|
308
|
+
if (typeof operation !== "function") throw codeError("local_data_key_batch_invalid");
|
|
309
|
+
if (batches.getStore()?.active) return operation();
|
|
310
|
+
const batch = { active: true, record: null };
|
|
311
|
+
return batches.run(batch, async () => {
|
|
312
|
+
try { return await operation(); }
|
|
313
|
+
finally { batch.active = false; batch.record = null; }
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
async getOrCreate() {
|
|
317
|
+
if (current != null) {
|
|
318
|
+
await assertDurableSlotUnchanged();
|
|
319
|
+
return copyKeyRecord(current);
|
|
320
|
+
}
|
|
321
|
+
current = normalizeKeyRecord(await invoke("CreateOrLoad"));
|
|
322
|
+
await rememberDurableSlot();
|
|
323
|
+
return copyKeyRecord(current);
|
|
324
|
+
},
|
|
325
|
+
async load(keyId) {
|
|
326
|
+
const expectedKeyId = assertKeyId(keyId);
|
|
327
|
+
if (current != null) {
|
|
328
|
+
if (current.keyId !== expectedKeyId) throw codeError("local_data_key_id_mismatch");
|
|
329
|
+
await assertDurableSlotUnchanged();
|
|
330
|
+
return copyKeyRecord(current);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const result = normalizeKeyRecord(await invoke("Load"));
|
|
334
|
+
if (result.keyId !== expectedKeyId) throw codeError("local_data_key_id_mismatch");
|
|
335
|
+
current = result;
|
|
336
|
+
await rememberDurableSlot();
|
|
337
|
+
return copyKeyRecord(current);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error?.code === "local_data_key_missing") return null;
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
clear() {
|
|
344
|
+
current?.key.fill(0);
|
|
345
|
+
current = null;
|
|
346
|
+
durableFingerprint = null;
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function keySlotFingerprint(root) {
|
|
352
|
+
const rootMetadata = await lstat(root).catch((error) => {
|
|
353
|
+
if (error?.code === "ENOENT") throw codeError("local_data_key_missing");
|
|
354
|
+
throw error;
|
|
355
|
+
});
|
|
356
|
+
if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) {
|
|
357
|
+
throw codeError("local_data_key_root_invalid");
|
|
358
|
+
}
|
|
359
|
+
const slotPath = join(root, WINDOWS_KEY_FILE);
|
|
360
|
+
let before;
|
|
361
|
+
let handle;
|
|
362
|
+
try {
|
|
363
|
+
before = await lstat(slotPath);
|
|
364
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size < 2 || before.size > MAX_KEY_STORE_BYTES) {
|
|
365
|
+
throw codeError("local_data_key_store_invalid");
|
|
366
|
+
}
|
|
367
|
+
handle = await open(slotPath, "r");
|
|
368
|
+
const opened = await handle.stat();
|
|
369
|
+
if (!sameFile(before, opened) || opened.size > MAX_KEY_STORE_BYTES) {
|
|
370
|
+
throw codeError("local_data_key_store_invalid");
|
|
371
|
+
}
|
|
372
|
+
const bytes = await handle.readFile();
|
|
373
|
+
return `${opened.dev}:${opened.ino}:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error?.code === "ENOENT") throw codeError("local_data_key_missing");
|
|
376
|
+
throw error;
|
|
377
|
+
} finally {
|
|
378
|
+
await handle?.close().catch(() => {});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sameFile(left, right) {
|
|
383
|
+
if (process.platform === "win32" && (left.dev === 0 || right.dev === 0)
|
|
384
|
+
&& left.ino !== right.ino) return false;
|
|
385
|
+
return left.isFile() && right.isFile()
|
|
386
|
+
&& (!Number.isSafeInteger(left.dev) || !Number.isSafeInteger(left.ino)
|
|
387
|
+
|| !Number.isSafeInteger(right.dev) || !Number.isSafeInteger(right.ino)
|
|
388
|
+
|| (left.ino === right.ino
|
|
389
|
+
&& (left.dev === right.dev
|
|
390
|
+
|| (process.platform === "win32" && (left.dev === 0 || right.dev === 0)))));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function sealBytes(plain, record, binding, context, random) {
|
|
394
|
+
const normalizedContext = normalizeContext(context);
|
|
395
|
+
const nonce = randomExact(random, 12, "local_data_nonce_generation_failed");
|
|
396
|
+
let ciphertext;
|
|
397
|
+
let tag;
|
|
398
|
+
try {
|
|
399
|
+
const cipher = createCipheriv("aes-256-gcm", record.key, nonce, { authTagLength: 16 });
|
|
400
|
+
cipher.setAAD(aadBytes(binding, normalizedContext, record.keyId));
|
|
401
|
+
ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
402
|
+
tag = cipher.getAuthTag();
|
|
403
|
+
} catch {
|
|
404
|
+
throw codeError("local_data_encrypt_failed");
|
|
405
|
+
} finally {
|
|
406
|
+
plain.fill(0);
|
|
407
|
+
}
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
contract: HELLOLYNK_LOCAL_DATA_ENVELOPE_CONTRACT,
|
|
410
|
+
schemaVersion: HELLOLYNK_LOCAL_DATA_ENVELOPE_SCHEMA_VERSION,
|
|
411
|
+
algorithm: HELLOLYNK_LOCAL_DATA_ENVELOPE_ALGORITHM,
|
|
412
|
+
keyId: record.keyId,
|
|
413
|
+
nonce: nonce.toString("base64url"),
|
|
414
|
+
ciphertext: ciphertext.toString("base64url"),
|
|
415
|
+
tag: tag.toString("base64url"),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function openBytes(envelope, record, binding, context) {
|
|
420
|
+
const normalizedContext = normalizeContext(context);
|
|
421
|
+
const nonce = decodeBase64Url(envelope.nonce, 12, "local_data_envelope_nonce_invalid");
|
|
422
|
+
const ciphertext = decodeBase64Url(
|
|
423
|
+
envelope.ciphertext,
|
|
424
|
+
null,
|
|
425
|
+
"local_data_envelope_ciphertext_invalid",
|
|
426
|
+
);
|
|
427
|
+
const tag = decodeBase64Url(envelope.tag, 16, "local_data_envelope_tag_invalid");
|
|
428
|
+
try {
|
|
429
|
+
const decipher = createDecipheriv("aes-256-gcm", record.key, nonce, { authTagLength: 16 });
|
|
430
|
+
decipher.setAAD(aadBytes(binding, normalizedContext, record.keyId));
|
|
431
|
+
decipher.setAuthTag(tag);
|
|
432
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
433
|
+
} catch {
|
|
434
|
+
throw codeError("local_data_authentication_failed");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function aadBytes(binding, context, keyId) {
|
|
439
|
+
return Buffer.from(JSON.stringify({
|
|
440
|
+
contract: HELLOLYNK_LOCAL_DATA_ENVELOPE_CONTRACT,
|
|
441
|
+
schemaVersion: HELLOLYNK_LOCAL_DATA_ENVELOPE_SCHEMA_VERSION,
|
|
442
|
+
region: binding.region,
|
|
443
|
+
principalId: binding.principalId,
|
|
444
|
+
hostInstallationId: binding.hostInstallationId,
|
|
445
|
+
profileId: context.profileId,
|
|
446
|
+
assetKind: context.assetKind,
|
|
447
|
+
logicalPath: context.logicalPath,
|
|
448
|
+
keyId,
|
|
449
|
+
}), "utf8");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function assertEnvelope(value) {
|
|
453
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
454
|
+
throw codeError("local_data_envelope_invalid");
|
|
455
|
+
}
|
|
456
|
+
const actual = Object.keys(value).sort();
|
|
457
|
+
if (actual.length !== ENVELOPE_FIELDS.length
|
|
458
|
+
|| actual.some((field, index) => field !== ENVELOPE_FIELDS[index])) {
|
|
459
|
+
throw codeError("local_data_envelope_fields_invalid");
|
|
460
|
+
}
|
|
461
|
+
if (value.contract !== HELLOLYNK_LOCAL_DATA_ENVELOPE_CONTRACT) {
|
|
462
|
+
throw codeError("local_data_envelope_contract_invalid");
|
|
463
|
+
}
|
|
464
|
+
if (value.schemaVersion !== HELLOLYNK_LOCAL_DATA_ENVELOPE_SCHEMA_VERSION) {
|
|
465
|
+
throw codeError("local_data_envelope_version_unsupported");
|
|
466
|
+
}
|
|
467
|
+
if (value.algorithm !== HELLOLYNK_LOCAL_DATA_ENVELOPE_ALGORITHM) {
|
|
468
|
+
throw codeError("local_data_envelope_algorithm_unsupported");
|
|
469
|
+
}
|
|
470
|
+
assertKeyId(value.keyId);
|
|
471
|
+
decodeBase64Url(value.nonce, 12, "local_data_envelope_nonce_invalid");
|
|
472
|
+
decodeBase64Url(value.ciphertext, null, "local_data_envelope_ciphertext_invalid");
|
|
473
|
+
decodeBase64Url(value.tag, 16, "local_data_envelope_tag_invalid");
|
|
474
|
+
return Object.freeze({
|
|
475
|
+
kind: "current",
|
|
476
|
+
contract: value.contract,
|
|
477
|
+
schemaVersion: HELLOLYNK_LOCAL_DATA_ENVELOPE_SCHEMA_VERSION,
|
|
478
|
+
envelope: value,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function classifyHelloLynkLocalDataEnvelope(value) {
|
|
483
|
+
if (!looksLikeEnvelope(value)) return null;
|
|
484
|
+
return assertEnvelope(value).kind;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function looksLikeEnvelope(value) {
|
|
488
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
489
|
+
return value.contract === HELLOLYNK_LOCAL_DATA_ENVELOPE_CONTRACT
|
|
490
|
+
|| Object.keys(value).some((field) => ENVELOPE_MARKER_FIELDS.has(field));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function normalizeContext(value) {
|
|
494
|
+
return Object.freeze({
|
|
495
|
+
profileId: assertBindingValue(value?.profileId, "local_data_profile_id_invalid"),
|
|
496
|
+
assetKind: assertBindingValue(value?.assetKind, "local_data_asset_kind_invalid"),
|
|
497
|
+
logicalPath: assertBindingValue(value?.logicalPath, "local_data_logical_path_invalid", 1024),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function assertBindingValue(value, errorCode, maxLength = 256) {
|
|
502
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maxLength
|
|
503
|
+
|| value.includes("\0") || value.trim() !== value) {
|
|
504
|
+
throw codeError(errorCode);
|
|
505
|
+
}
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function assertKeyProvider(value) {
|
|
510
|
+
if (!value || typeof value.getOrCreate !== "function" || typeof value.load !== "function") {
|
|
511
|
+
throw codeError("local_data_key_provider_invalid");
|
|
512
|
+
}
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function normalizeKeyRecord(value) {
|
|
517
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
518
|
+
throw codeError("local_data_key_record_invalid");
|
|
519
|
+
}
|
|
520
|
+
const keyId = assertKeyId(value.keyId);
|
|
521
|
+
let key;
|
|
522
|
+
if (Buffer.isBuffer(value.key) || value.key instanceof Uint8Array) {
|
|
523
|
+
key = Buffer.from(value.key);
|
|
524
|
+
} else if (typeof value.key === "string") {
|
|
525
|
+
key = decodeBase64Url(value.key, 32, "local_data_key_invalid");
|
|
526
|
+
} else {
|
|
527
|
+
throw codeError("local_data_key_invalid");
|
|
528
|
+
}
|
|
529
|
+
if (key.length !== 32) throw codeError("local_data_key_invalid");
|
|
530
|
+
return { keyId, key };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function copyKeyRecord(record) {
|
|
534
|
+
return { keyId: record.keyId, key: Buffer.from(record.key) };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function assertKeyId(value) {
|
|
538
|
+
if (typeof value !== "string" || !KEY_ID_PATTERN.test(value)) {
|
|
539
|
+
throw codeError("local_data_key_id_invalid");
|
|
540
|
+
}
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function serializeJson(value) {
|
|
545
|
+
let serialized;
|
|
546
|
+
try {
|
|
547
|
+
serialized = JSON.stringify(value);
|
|
548
|
+
} catch {
|
|
549
|
+
throw codeError("local_data_plaintext_json_invalid");
|
|
550
|
+
}
|
|
551
|
+
if (typeof serialized !== "string") throw codeError("local_data_plaintext_json_invalid");
|
|
552
|
+
return serialized;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function cloneJson(value) {
|
|
556
|
+
const serialized = serializeJson(value);
|
|
557
|
+
try {
|
|
558
|
+
return JSON.parse(serialized);
|
|
559
|
+
} catch {
|
|
560
|
+
throw codeError("local_data_plaintext_json_invalid");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function decodeBase64Url(value, expectedLength, errorCode) {
|
|
565
|
+
if (typeof value !== "string" || !BASE64URL_PATTERN.test(value)) throw codeError(errorCode);
|
|
566
|
+
let decoded;
|
|
567
|
+
try {
|
|
568
|
+
decoded = Buffer.from(value, "base64url");
|
|
569
|
+
} catch {
|
|
570
|
+
throw codeError(errorCode);
|
|
571
|
+
}
|
|
572
|
+
if (decoded.toString("base64url") !== value
|
|
573
|
+
|| (expectedLength != null && decoded.length !== expectedLength)) {
|
|
574
|
+
throw codeError(errorCode);
|
|
575
|
+
}
|
|
576
|
+
return decoded;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function randomExact(random, size, errorCode) {
|
|
580
|
+
let value;
|
|
581
|
+
try {
|
|
582
|
+
value = Buffer.from(random(size));
|
|
583
|
+
} catch {
|
|
584
|
+
throw codeError(errorCode);
|
|
585
|
+
}
|
|
586
|
+
if (value.length !== size) throw codeError(errorCode);
|
|
587
|
+
return value;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function assertAbsoluteRoot(value) {
|
|
591
|
+
if (typeof value !== "string" || !value || value.includes("\0") || !isAbsolute(value)) {
|
|
592
|
+
throw codeError("local_data_key_root_invalid");
|
|
593
|
+
}
|
|
594
|
+
return resolve(value);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function invokeWindowsDataKey(input) {
|
|
598
|
+
try {
|
|
599
|
+
return await invokeWindowsDataKeyUnsafe(input);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (typeof error?.code === "string" && /^local_data_[a-z0-9_]+$/.test(error.code)) throw error;
|
|
602
|
+
throw codeError("local_data_key_helper_failed");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function invokeWindowsDataKeyUnsafe(input) {
|
|
607
|
+
let helperMetadata;
|
|
608
|
+
try {
|
|
609
|
+
helperMetadata = await lstat(input.helperPath);
|
|
610
|
+
} catch {
|
|
611
|
+
throw codeError("local_data_key_helper_invalid");
|
|
612
|
+
}
|
|
613
|
+
if (!helperMetadata.isFile() || helperMetadata.isSymbolicLink()) {
|
|
614
|
+
throw codeError("local_data_key_helper_invalid");
|
|
615
|
+
}
|
|
616
|
+
if (input.command === "CreateOrLoad") {
|
|
617
|
+
await mkdir(input.root, { recursive: true, mode: 0o700 });
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const rootMetadata = await lstat(input.root);
|
|
621
|
+
if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) {
|
|
622
|
+
throw codeError("local_data_key_root_invalid");
|
|
623
|
+
}
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (input.command === "Load" && error?.code === "ENOENT") {
|
|
626
|
+
// The read-only helper returns the stable missing-slot error without
|
|
627
|
+
// creating the root. Keep this path inside the real Windows boundary.
|
|
628
|
+
} else {
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const pipeName = `HelloLynkLocalDataKey-${process.pid}-${cryptoRandomBytes(18).toString("base64url")}`;
|
|
634
|
+
const pipePath = `\\\\.\\pipe\\${pipeName}`;
|
|
635
|
+
const args = [
|
|
636
|
+
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", input.helperPath,
|
|
637
|
+
"-Command", input.command,
|
|
638
|
+
"-StoreRoot", input.root,
|
|
639
|
+
"-ResultPipeName", pipeName,
|
|
640
|
+
];
|
|
641
|
+
const child = spawn("pwsh.exe", args, {
|
|
642
|
+
windowsHide: true,
|
|
643
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
644
|
+
env: sanitizeHelloLynkChildEnvironment(process.env),
|
|
645
|
+
});
|
|
646
|
+
let cancelled = false;
|
|
647
|
+
const resultPromise = readCurrentUserPipe(pipePath, () => cancelled);
|
|
648
|
+
resultPromise.catch(() => {});
|
|
649
|
+
const stdout = [];
|
|
650
|
+
const stderr = [];
|
|
651
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
652
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
653
|
+
let exitCode;
|
|
654
|
+
let timeout;
|
|
655
|
+
try {
|
|
656
|
+
const exitPromise = new Promise((resolveExit, rejectExit) => {
|
|
657
|
+
child.on("error", rejectExit);
|
|
658
|
+
child.on("close", resolveExit);
|
|
659
|
+
});
|
|
660
|
+
const timeoutPromise = new Promise((_, rejectTimeout) => {
|
|
661
|
+
timeout = setTimeout(() => {
|
|
662
|
+
cancelled = true;
|
|
663
|
+
child.kill();
|
|
664
|
+
rejectTimeout(codeError("local_data_key_helper_timeout"));
|
|
665
|
+
}, HELPER_TIMEOUT_MS);
|
|
666
|
+
timeout.unref?.();
|
|
667
|
+
});
|
|
668
|
+
exitCode = await Promise.race([exitPromise, timeoutPromise]);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
cancelled = true;
|
|
671
|
+
if (child.exitCode == null) child.kill();
|
|
672
|
+
if (error?.code === "local_data_key_helper_timeout") throw error;
|
|
673
|
+
throw codeError("local_data_key_helper_unavailable");
|
|
674
|
+
} finally {
|
|
675
|
+
clearTimeout(timeout);
|
|
676
|
+
}
|
|
677
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
678
|
+
const stderrText = Buffer.concat(stderr).toString("utf8").trim();
|
|
679
|
+
if (stdoutText.length) {
|
|
680
|
+
cancelled = true;
|
|
681
|
+
throw codeError("local_data_key_helper_stdout_forbidden");
|
|
682
|
+
}
|
|
683
|
+
if (exitCode !== 0) {
|
|
684
|
+
cancelled = true;
|
|
685
|
+
throw codeError(publicHelperError(stderrText));
|
|
686
|
+
}
|
|
687
|
+
let result;
|
|
688
|
+
try {
|
|
689
|
+
result = JSON.parse(await resultPromise);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (error?.code) throw error;
|
|
692
|
+
throw codeError("local_data_key_result_invalid");
|
|
693
|
+
}
|
|
694
|
+
const actual = Object.keys(result || {}).sort().join(",");
|
|
695
|
+
if (actual !== "contract,key,keyId"
|
|
696
|
+
|| result.contract !== HELLOLYNK_LOCAL_DATA_KEY_RECORD_CONTRACT) {
|
|
697
|
+
throw codeError("local_data_key_result_invalid");
|
|
698
|
+
}
|
|
699
|
+
return { keyId: result.keyId, key: result.key };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function readCurrentUserPipe(pipePath, cancelled) {
|
|
703
|
+
const deadline = Date.now() + 30_000;
|
|
704
|
+
while (true) {
|
|
705
|
+
try {
|
|
706
|
+
return await new Promise((resolvePipe, rejectPipe) => {
|
|
707
|
+
const chunks = [];
|
|
708
|
+
let bytes = 0;
|
|
709
|
+
const socket = createConnection(pipePath);
|
|
710
|
+
socket.setTimeout(Math.max(1, deadline - Date.now()), () => {
|
|
711
|
+
socket.destroy(codeError("local_data_key_result_timeout"));
|
|
712
|
+
});
|
|
713
|
+
socket.on("data", (chunk) => {
|
|
714
|
+
bytes += chunk.length;
|
|
715
|
+
if (bytes > 4096) {
|
|
716
|
+
socket.destroy(codeError("local_data_key_result_too_large"));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
chunks.push(chunk);
|
|
720
|
+
});
|
|
721
|
+
socket.on("error", rejectPipe);
|
|
722
|
+
socket.on("end", () => resolvePipe(Buffer.concat(chunks).toString("utf8")));
|
|
723
|
+
});
|
|
724
|
+
} catch (error) {
|
|
725
|
+
if (cancelled()) throw codeError("local_data_key_result_cancelled");
|
|
726
|
+
if (error?.code !== "ENOENT" || Date.now() >= deadline) throw error;
|
|
727
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function publicHelperError(value) {
|
|
733
|
+
return /^[a-z][a-z0-9_]{2,127}$/.test(value)
|
|
734
|
+
? value
|
|
735
|
+
: "local_data_key_helper_failed";
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function normalizeKeyProviderError(error) {
|
|
739
|
+
if (typeof error?.code === "string" && /^local_data_[a-z0-9_]+$/.test(error.code)) return error;
|
|
740
|
+
return codeError("local_data_key_provider_failed");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function codeError(code) {
|
|
744
|
+
const error = new Error(code);
|
|
745
|
+
error.code = code;
|
|
746
|
+
return error;
|
|
747
|
+
}
|