@hellolynk-ai/codex-plugin 0.2.102 → 0.2.103-macos.1
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/README.md +17 -17
- package/bin/hellolynk-client-setup.mjs +70 -10
- package/hellolynk-client-pack-manifest.json +101 -61
- package/hellolynk-client-pack-pin.json +1 -1
- package/package.json +8 -4
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/hellolynk-codex/.mcp.json +20 -19
- package/plugins/hellolynk-codex/package.json +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +269 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-vault.mjs +153 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +20 -8
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +68 -14
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +5 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +7 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +62 -33
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +62 -18
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +269 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-vault.mjs +153 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +20 -8
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +53 -6
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +22 -7
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +9 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +68 -14
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +101 -2
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +5 -3
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +56 -24
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +46 -10
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +14 -8
- package/tools/hellolynk-activate-neutral-host-clients.mjs +5 -1
package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkDarwinPath, DARWIN_HELPER, invokeDarwinHelper, withDarwinVault } from "./hellolynk-darwin-vault.mjs";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
3
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -260,6 +261,14 @@ export async function runWindowsClientTrustCommand(command, options = {}) {
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
export async function prepareWindowsClientRoot(clientRoot, options = {}) {
|
|
264
|
+
if ((options.platform || process.platform) === "darwin") {
|
|
265
|
+
checkDarwinPath(clientRoot, { directory: true, create: true, privateMode: true });
|
|
266
|
+
for (const sub of ["profiles", "credentials", "e2ee-private-keys", "status", "host", "host/runtime", "releases"]) {
|
|
267
|
+
checkDarwinPath(join(clientRoot, sub), { directory: true, create: true, privateMode: true });
|
|
268
|
+
}
|
|
269
|
+
await invokeDarwinHelper(options.helperPath || DARWIN_HELPER, ["verify-acl", clientRoot]);
|
|
270
|
+
return "prepared";
|
|
271
|
+
}
|
|
263
272
|
return runWindowsClientTrustCommand("PrepareRoot", {
|
|
264
273
|
...options,
|
|
265
274
|
clientRoot,
|
|
@@ -272,6 +281,14 @@ export async function secureWindowsClientReleaseTree(clientRoot, releaseId, opti
|
|
|
272
281
|
if (!/^(?!\.{1,2}$)[A-Za-z0-9.][A-Za-z0-9_.-]{2,191}$/.test(acceptedReleaseId)) {
|
|
273
282
|
throw new HelloLynkCredentialStoreError("client_release_path_invalid");
|
|
274
283
|
}
|
|
284
|
+
if ((options.platform || process.platform) === "darwin") {
|
|
285
|
+
checkDarwinPath(clientRoot, { directory: true, privateMode: true });
|
|
286
|
+
checkDarwinPath(join(clientRoot, "releases"), { directory: true, privateMode: true });
|
|
287
|
+
const release = join(clientRoot, "releases", acceptedReleaseId);
|
|
288
|
+
checkDarwinPath(release, { directory: true });
|
|
289
|
+
await invokeDarwinHelper(options.helperPath || DARWIN_HELPER, [options.verifyOnly ? "verify-tree" : "secure-tree", release]);
|
|
290
|
+
return options.verifyOnly ? "verified" : "secured";
|
|
291
|
+
}
|
|
275
292
|
return runWindowsClientTrustCommand(options.verifyOnly === true ? "VerifyReleaseTree" : "SecureReleaseTree", {
|
|
276
293
|
...options,
|
|
277
294
|
clientRoot,
|
|
@@ -460,24 +477,61 @@ export function createPlatformCredentialStore(options = {}) {
|
|
|
460
477
|
}
|
|
461
478
|
|
|
462
479
|
if (platform === "darwin") {
|
|
463
|
-
const
|
|
464
|
-
homedir(), "Library", "Application Support", "HelloLynk"
|
|
465
|
-
);
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
);
|
|
480
|
+
const clientRoot = options.clientRoot || (options.storeRoot ? windowsClientRootForStore(options.storeRoot)
|
|
481
|
+
: join(homedir(), "Library", "Application Support", "HelloLynk"));
|
|
482
|
+
if (clientRoot !== resolve(clientRoot) || clientRoot === "/") throw new HelloLynkCredentialStoreError("client_root_path_invalid");
|
|
483
|
+
const credentials = join(clientRoot, "credentials");
|
|
484
|
+
const privateKeys = join(clientRoot, "e2ee-private-keys");
|
|
485
|
+
const root = options.storeRoot || credentials;
|
|
486
|
+
const e2eeRoot = options.e2eeStoreRoot || privateKeys;
|
|
487
|
+
if (root !== clientRoot && root !== credentials) throw new HelloLynkCredentialStoreError("credential_store_root_invalid");
|
|
488
|
+
if (e2eeRoot !== privateKeys) throw new HelloLynkCredentialStoreError("e2ee_private_key_store_root_invalid");
|
|
489
|
+
function entry(location, section, profile, operation, value) {
|
|
490
|
+
const slot = section + ":" + assertCredentialProfileId(profile);
|
|
491
|
+
return withDarwinVault({ root: location, helperPath: options.helperPath, readOnly: operation === "load", allowMissing:true }, data => {
|
|
492
|
+
if (operation === "save") data[slot] = value;
|
|
493
|
+
if (operation === "remove") delete data[slot];
|
|
494
|
+
return data[slot] ?? null;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
471
497
|
return {
|
|
472
498
|
platform,
|
|
473
|
-
async save(profile, credential) {
|
|
474
|
-
await invoke("Store", assertCredentialProfileId(profile), assertCredential(credential));
|
|
475
|
-
},
|
|
499
|
+
async save(profile, credential) { await entry(root, "device", profile, "save", assertCredential(credential)); },
|
|
476
500
|
async load(profile) {
|
|
477
|
-
|
|
501
|
+
const value = await entry(root, "device", profile, "load");
|
|
502
|
+
if (value === null) throw new HelloLynkCredentialStoreError("device_credential_missing");
|
|
503
|
+
return assertCredential(value);
|
|
478
504
|
},
|
|
479
|
-
async remove(profile) {
|
|
480
|
-
|
|
505
|
+
async remove(profile) { await entry(root, "device", profile, "remove"); },
|
|
506
|
+
runtimeSessions: {
|
|
507
|
+
async save(profile, session) { await entry(root, "session", profile, "save", assertRuntimeSession(session)); },
|
|
508
|
+
async load(profile) {
|
|
509
|
+
const value = await entry(root, "session", profile, "load");
|
|
510
|
+
return value === null ? null : assertRuntimeSession(value);
|
|
511
|
+
},
|
|
512
|
+
async remove(profile) { await entry(root, "session", profile, "remove"); },
|
|
513
|
+
async withLock(profile, operation) {
|
|
514
|
+
const id = assertCredentialProfileId(profile);
|
|
515
|
+
if (typeof operation !== "function") throw new HelloLynkCredentialStoreError("runtime_session_lock_operation_invalid");
|
|
516
|
+
checkDarwinPath(root, { directory:true, create:true, privateMode:true });
|
|
517
|
+
return withRuntimeSessionFileLock(root, id, operation, options.runtimeSessionLockDependencies, options.timeoutMs);
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
directTextPrivateKeys: {
|
|
521
|
+
async save(profile, material) { await entry(e2eeRoot, "keys", profile, "save", encodeDirectTextPrivateKeyMaterial(material)); },
|
|
522
|
+
async load(profile) {
|
|
523
|
+
const value = await entry(e2eeRoot, "keys", profile, "load");
|
|
524
|
+
return value === null ? null : decodeDirectTextPrivateKeyMaterial(value);
|
|
525
|
+
},
|
|
526
|
+
async remove(profile) { await entry(e2eeRoot, "keys", profile, "remove"); },
|
|
527
|
+
async preload(profiles) {
|
|
528
|
+
const ids = [...new Set((Array.isArray(profiles) ? profiles : []).map(assertCredentialProfileId))];
|
|
529
|
+
if (!ids.length) return;
|
|
530
|
+
if (ids.length > MAX_E2EE_BATCH_PROFILES) throw new HelloLynkCredentialStoreError("e2ee_private_key_batch_invalid");
|
|
531
|
+
await withDarwinVault({root:e2eeRoot, helperPath:options.helperPath, readOnly:true, allowMissing:true}, data => {
|
|
532
|
+
for (const id of ids) if (data["keys:" + id] !== undefined) decodeDirectTextPrivateKeyMaterial(data["keys:" + id]);
|
|
533
|
+
});
|
|
534
|
+
},
|
|
481
535
|
},
|
|
482
536
|
};
|
|
483
537
|
}
|
package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin
ADDED
|
Binary file
|
package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Rebuild in this directory: xcrun swiftc -O -target arm64-apple-macos13.0 -module-name HelloLynkKeychain hellolynk-keychain-helper.swift -o hellolynk-keychain-helper.bin
|
|
2
|
+
import AppKit
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import Foundation
|
|
5
|
+
import Security
|
|
6
|
+
|
|
7
|
+
func fail(_ code: String) -> Never {
|
|
8
|
+
FileHandle.standardError.write(Data((code + "\n").utf8))
|
|
9
|
+
exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func requireNoGrantACL(_ path: String) {
|
|
13
|
+
guard let acl = acl_get_file(path, ACL_TYPE_EXTENDED) else {
|
|
14
|
+
if errno == ENOENT { return } // A first write creates the file with mode 0600.
|
|
15
|
+
fail("macos_vault_acl_unavailable")
|
|
16
|
+
}
|
|
17
|
+
defer { acl_free(UnsafeMutableRawPointer(acl)) }
|
|
18
|
+
var entry: acl_entry_t?
|
|
19
|
+
var selector = ACL_FIRST_ENTRY
|
|
20
|
+
while acl_get_entry(acl, Int32(selector.rawValue), &entry) == 0 {
|
|
21
|
+
var tag = ACL_UNDEFINED_TAG
|
|
22
|
+
guard acl_get_tag_type(entry, &tag) == 0, tag == ACL_EXTENDED_DENY else {
|
|
23
|
+
fail("macos_vault_acl_invalid")
|
|
24
|
+
}
|
|
25
|
+
selector = ACL_NEXT_ENTRY
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The caller verifies the release path's ancestors and package hashes. Walk
|
|
30
|
+
// with lstat so a symlink or hard link cannot redirect a permission change.
|
|
31
|
+
func secureRelease(_ path: String, verifyOnly: Bool) {
|
|
32
|
+
var pending = [path]
|
|
33
|
+
while let entry = pending.popLast() {
|
|
34
|
+
var st = stat()
|
|
35
|
+
guard lstat(entry, &st) == 0, st.st_uid == getuid() else { fail("macos_vault_release_invalid") }
|
|
36
|
+
let type = st.st_mode & S_IFMT
|
|
37
|
+
guard type == S_IFDIR || (type == S_IFREG && st.st_nlink == 1) else { fail("macos_vault_release_invalid") }
|
|
38
|
+
let mode: mode_t = type == S_IFDIR || entry.hasSuffix(".bin") ? 0o700 : 0o600
|
|
39
|
+
if (st.st_mode & 0o7777) != mode {
|
|
40
|
+
guard !verifyOnly, chmod(entry, mode) == 0 else { fail("macos_vault_release_permissions_invalid") }
|
|
41
|
+
}
|
|
42
|
+
requireNoGrantACL(entry)
|
|
43
|
+
if type == S_IFDIR {
|
|
44
|
+
guard let names = try? FileManager.default.contentsOfDirectory(atPath: entry) else { fail("macos_vault_release_invalid") }
|
|
45
|
+
pending.append(contentsOf: names.map { entry + "/" + $0 })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func masterKey(_ root: String, create: Bool) -> Data {
|
|
51
|
+
let account = SHA256.hash(data: Data(root.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
52
|
+
let base: [String: Any] = [
|
|
53
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
54
|
+
kSecAttrService as String: "com.hellolynk.codex.macos-vault.v1",
|
|
55
|
+
kSecAttrAccount as String: account
|
|
56
|
+
]
|
|
57
|
+
var lookup = base
|
|
58
|
+
lookup[kSecReturnData as String] = true
|
|
59
|
+
lookup[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
60
|
+
var result: CFTypeRef?
|
|
61
|
+
let status = SecItemCopyMatching(lookup as CFDictionary, &result)
|
|
62
|
+
if status == errSecSuccess {
|
|
63
|
+
guard let key = result as? Data, key.count == 32 else { fail("macos_vault_key_invalid") }
|
|
64
|
+
return key
|
|
65
|
+
}
|
|
66
|
+
guard status == errSecItemNotFound else { fail("macos_keychain_access_\(abs(Int(status)))") }
|
|
67
|
+
guard create else { fail("macos_vault_key_missing") }
|
|
68
|
+
let key = SymmetricKey(size: .bits256).withUnsafeBytes { Data($0) }
|
|
69
|
+
var add = base
|
|
70
|
+
add[kSecValueData as String] = key
|
|
71
|
+
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
72
|
+
let inserted = SecItemAdd(add as CFDictionary, nil)
|
|
73
|
+
if inserted == errSecDuplicateItem { return masterKey(root, create: false) }
|
|
74
|
+
guard inserted == errSecSuccess else { fail("macos_keychain_access_\(abs(Int(inserted)))") }
|
|
75
|
+
return key
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func nativeConfirmation() {
|
|
79
|
+
let raw = FileHandle.standardInput.readDataToEndOfFile()
|
|
80
|
+
guard raw.count <= 20480 * 6 + 8192,
|
|
81
|
+
var v = (try? JSONSerialization.jsonObject(with: raw)) as? [String: Any],
|
|
82
|
+
let body = v["displayText"] as? String, !body.isEmpty, body.utf8.count <= 20480,
|
|
83
|
+
let encoded = v["hmacKey"] as? String, encoded.count == 43,
|
|
84
|
+
let key = Data(base64Encoded: encoded.replacingOccurrences(of: "-", with: "+")
|
|
85
|
+
.replacingOccurrences(of: "_", with: "/") + "="), key.count == 32,
|
|
86
|
+
let recipient = v["recipientHelloLynkId"] as? String,
|
|
87
|
+
v["contract"] as? String == "hellolynk-native-human-confirmation-v1",
|
|
88
|
+
v["textDigest"] as? String == SHA256.hash(data: Data(body.utf8)).map({ String(format: "%02x", $0) }).joined()
|
|
89
|
+
else { fail("human_confirmation_request_invalid") }
|
|
90
|
+
|
|
91
|
+
var fields = ["contract", "actionId", "decision", "principalId", "delegateId", "hostInstallationId",
|
|
92
|
+
"adapterId", "adapterInstanceId", "hostGeneration", "recipientHelloLynkId"]
|
|
93
|
+
var label = "收件人:" + recipient
|
|
94
|
+
if v["recipientDelegateId"] != nil || v["recipientInstanceDisplayName"] != nil {
|
|
95
|
+
guard let name = v["recipientInstanceDisplayName"] as? String, !name.isEmpty, name.utf8.count <= 512,
|
|
96
|
+
name.range(of: #"[\x00-\x1F\x7F-\x9F\u2028\u2029\u202A-\u202E\u2066-\u2069]"#, options: .regularExpression) == nil,
|
|
97
|
+
v["recipientDelegateId"] is String else { fail("human_confirmation_request_invalid") }
|
|
98
|
+
fields += ["recipientDelegateId", "recipientInstanceDisplayName"]
|
|
99
|
+
label += "\n目标 AI:" + name
|
|
100
|
+
}
|
|
101
|
+
fields += ["clientMessageId", "textDigest", "createdAtMs", "expiresAtMs", "challenge"]
|
|
102
|
+
var expected = Set(fields)
|
|
103
|
+
expected.remove("decision")
|
|
104
|
+
expected.formUnion(["displayText", "hmacKey"])
|
|
105
|
+
guard expected == Set(v.keys) else { fail("human_confirmation_request_invalid") }
|
|
106
|
+
let app = NSApplication.shared
|
|
107
|
+
app.setActivationPolicy(.accessory)
|
|
108
|
+
app.activate(ignoringOtherApps: true)
|
|
109
|
+
let dialog = NSAlert()
|
|
110
|
+
dialog.messageText = "是否允许 HelloLynk 发送以下文本?"
|
|
111
|
+
dialog.informativeText = label
|
|
112
|
+
dialog.addButton(withTitle: "取消")
|
|
113
|
+
dialog.addButton(withTitle: "批准发送")
|
|
114
|
+
let scroll = NSScrollView(frame: NSRect(x: 0, y: 0, width: 500, height: 230))
|
|
115
|
+
scroll.hasVerticalScroller = true
|
|
116
|
+
let view = NSTextView(frame: scroll.contentView.bounds)
|
|
117
|
+
view.isEditable = false
|
|
118
|
+
view.isRichText = false
|
|
119
|
+
view.isVerticallyResizable = true
|
|
120
|
+
view.autoresizingMask = [.width]
|
|
121
|
+
view.textContainer?.widthTracksTextView = true
|
|
122
|
+
view.string = body
|
|
123
|
+
scroll.documentView = view
|
|
124
|
+
dialog.accessoryView = scroll
|
|
125
|
+
v["decision"] = dialog.runModal() == .alertSecondButtonReturn ? "approve" : "cancel"
|
|
126
|
+
let message = fields.map { field -> String in
|
|
127
|
+
if let text = v[field] as? String { return text }
|
|
128
|
+
if ["createdAtMs","expiresAtMs"].contains(field), let n = v[field] as? NSNumber { return n.stringValue }
|
|
129
|
+
fail("human_confirmation_request_invalid")
|
|
130
|
+
}.joined(separator: "\n")
|
|
131
|
+
let proof = HMAC<SHA256>.authenticationCode(for: Data(message.utf8), using: SymmetricKey(data: key))
|
|
132
|
+
v["proof"] = proof.map { String(format: "%02x", $0) }.joined()
|
|
133
|
+
for field in ["hmacKey", "challenge", "displayText"] { v.removeValue(forKey: field) }
|
|
134
|
+
guard let output = try? JSONSerialization.data(withJSONObject: v) else { fail("human_confirmation_response_invalid") }
|
|
135
|
+
FileHandle.standardOutput.write(output)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let args = Array(CommandLine.arguments.dropFirst())
|
|
139
|
+
if args == ["confirm"] { nativeConfirmation() }
|
|
140
|
+
else if args.count == 3, args[0] == "key", args[1].hasPrefix("/"), ["load","create"].contains(args[2]) {
|
|
141
|
+
requireNoGrantACL(args[1])
|
|
142
|
+
requireNoGrantACL(args[1] + "/protected-v1.sqlite")
|
|
143
|
+
let key = masterKey(args[1], create: args[2] == "create")
|
|
144
|
+
FileHandle(fileDescriptor: 3, closeOnDealloc: false).write(key)
|
|
145
|
+
} else if args.count == 2, ["verify-tree", "secure-tree"].contains(args[0]), args[1].hasPrefix("/") {
|
|
146
|
+
secureRelease(args[1], verifyOnly: args[0] == "verify-tree")
|
|
147
|
+
} else if args.count == 2, args[0] == "verify-acl", args[1].hasPrefix("/") {
|
|
148
|
+
requireNoGrantACL(args[1])
|
|
149
|
+
} else { fail("macos_vault_helper_arguments_invalid") }
|
|
@@ -5,6 +5,7 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
8
|
+
import { verifyDarwinUnclaimedAuthorization } from "./core/hellolynk-darwin-host.mjs";
|
|
8
9
|
import { readClientProfile } from "./core/hellolynk-client-profile.mjs";
|
|
9
10
|
import { createNeutralHostInstallationStore } from "./core/hellolynk-neutral-host-installation-store.mjs";
|
|
10
11
|
import { clientAccountBaseRoot } from "./core/hellolynk-client-account.mjs";
|
|
@@ -164,7 +165,7 @@ assert.equal(releaseVerification.verifiedFileCount >= 8, true);
|
|
|
164
165
|
let transport = null;
|
|
165
166
|
let bootstrapTransportRequired = false;
|
|
166
167
|
let protectedStorePresent = true;
|
|
167
|
-
try { await fs.lstat(path.join(root, "capabilities-v1.dpapi.clixml")); }
|
|
168
|
+
try { await fs.lstat(path.join(root, process.platform === "darwin" ? "protected-v1.sqlite" : "capabilities-v1.dpapi.clixml")); }
|
|
168
169
|
catch (error) {
|
|
169
170
|
if (error?.code !== "ENOENT") throw error;
|
|
170
171
|
protectedStorePresent = false;
|
|
@@ -173,6 +174,9 @@ if (!protectedStorePresent) {
|
|
|
173
174
|
if (!(freshPreBind || recoveryPreBind)) await store.status();
|
|
174
175
|
await verifyHostProtectedStorePath({ root, path: root, directory: true });
|
|
175
176
|
bootstrapTransportRequired = true;
|
|
177
|
+
} else if (process.platform === "darwin" && freshPreBind) {
|
|
178
|
+
await verifyDarwinUnclaimedAuthorization(root);
|
|
179
|
+
bootstrapTransportRequired = true;
|
|
176
180
|
} else {
|
|
177
181
|
assert.equal((await store.status()).protected, true);
|
|
178
182
|
try { transport = preflightOnly ? await store.verifyTransport() : await store.resolveTransport(); }
|
|
@@ -68,7 +68,7 @@ export function withClientAccountLock(client, root, operation) {
|
|
|
68
68
|
|
|
69
69
|
export function createClientAccountActions(options) {
|
|
70
70
|
const client = options?.client;
|
|
71
|
-
const root = options?.storeRoot || defaultNeutralHostStoreRoot(
|
|
71
|
+
const root = options?.storeRoot || defaultNeutralHostStoreRoot(process.platform, options?.env || process.env);
|
|
72
72
|
statePath(client, clientAccountBaseRoot(root));
|
|
73
73
|
const readAccountState = () => {
|
|
74
74
|
const state = readClientAccount(client, root);
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { clientAccountBaseRoot } from "./hellolynk-client-account.mjs";
|
|
4
4
|
import { startClientUpdateRuntime } from "./hellolynk-client-update-runtime.mjs";
|
|
5
|
+
import { clientUpdateCatalogId, MAC_CLIENT_VERSION } from "./hellolynk-client-update.mjs";
|
|
5
6
|
|
|
6
7
|
const running = new Map();
|
|
7
8
|
|
|
@@ -9,7 +10,9 @@ const running = new Map();
|
|
|
9
10
|
// to the Host. A checkout or an uninstalled test fixture never self-updates.
|
|
10
11
|
export function startInstalledClientUpdate(options = {}) {
|
|
11
12
|
const { client, packageRoot, storeRoot } = options;
|
|
12
|
-
|
|
13
|
+
const catalogId = clientUpdateCatalogId(client, process.platform);
|
|
14
|
+
const mac = process.platform === "darwin";
|
|
15
|
+
if (!catalogId || (mac && process.arch !== "arm64") || !path.isAbsolute(String(packageRoot || ""))) return Promise.resolve(null);
|
|
13
16
|
let baseRoot;
|
|
14
17
|
try { baseRoot = clientAccountBaseRoot(storeRoot); } catch { return Promise.resolve(null); }
|
|
15
18
|
const key = `${client}\0${path.resolve(packageRoot)}\0${baseRoot}`;
|
|
@@ -21,8 +24,9 @@ export function startInstalledClientUpdate(options = {}) {
|
|
|
21
24
|
const metadata = await fs.lstat(packageFile);
|
|
22
25
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 64 * 1024) return null;
|
|
23
26
|
const manifest = JSON.parse(await fs.readFile(packageFile, "utf8"));
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const version = mac ? MAC_CLIENT_VERSION : /^\d+\.\d+\.\d+$/u;
|
|
28
|
+
if (manifest.hellolynkClient?.client !== client || !version.test(manifest.version || "")
|
|
29
|
+
|| (mac && (manifest.os?.[0] !== "darwin" || manifest.cpu?.[0] !== "arm64"))) return null;
|
|
26
30
|
return startClientUpdateRuntime({
|
|
27
31
|
root: baseRoot, client, currentVersion: manifest.version,
|
|
28
32
|
installContext: { ...options.installContext, packageRoot: path.resolve(packageRoot) },
|
|
@@ -4,7 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { createClientUpdateManager, extractClientUpdateArchive, compareClientUpdateVersions,
|
|
7
|
+
import { createClientUpdateManager, extractClientUpdateArchive, compareClientUpdateVersions, clientUpdateCatalogId,
|
|
8
|
+
CLIENT_UPDATE_CLIENTS, CLIENT_UPDATE_RELEASES, MAC_CLIENT_VERSION } from "./hellolynk-client-update.mjs";
|
|
8
9
|
import { withNeutralHostStoreLock } from "./hellolynk-neutral-host-installation-store.mjs";
|
|
9
10
|
import { readClientAccount } from "./hellolynk-client-account.mjs";
|
|
10
11
|
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
@@ -26,7 +27,7 @@ const RETRY_AFTER_MS = 10 * 60_000;
|
|
|
26
27
|
// waits only for DSH's lease, and a downloaded update is applied by a worker in
|
|
27
28
|
// the background. Checking the network is always detached.
|
|
28
29
|
export async function startClientUpdateRuntime(options = {}) {
|
|
29
|
-
if ((options.platform || process.platform)
|
|
30
|
+
if (!clientUpdateCatalogId(options.client, options.platform || process.platform)) return disabled();
|
|
30
31
|
let config;
|
|
31
32
|
try { config = normalize(options); await verifyInstalledContext(config); }
|
|
32
33
|
catch { return disabled(); }
|
|
@@ -94,8 +95,8 @@ export async function runClientUpdateWorker(options = {}) {
|
|
|
94
95
|
function createRuntime(config, options) {
|
|
95
96
|
const { root, client, currentVersion, installContext } = config;
|
|
96
97
|
const directory = path.join(root, "client-updates", client);
|
|
97
|
-
const statePath = path.join(directory, "runtime.json"), leasePath = path.join(directory, "leases.json"), installedPath = path.join(directory, "installed.json");
|
|
98
|
-
const manager = createClientUpdateManager({ ...options, root, client, currentVersion });
|
|
98
|
+
const statePath = path.join(directory, "runtime-v2.json"), leasePath = path.join(directory, "leases.json"), installedPath = path.join(directory, "installed.json");
|
|
99
|
+
const manager = createClientUpdateManager({ ...options, root, client, currentVersion, platform: config.platform });
|
|
99
100
|
const alive = options.isProcessAlive || processAlive;
|
|
100
101
|
async function gate(operation) {
|
|
101
102
|
await safeDirectory(directory);
|
|
@@ -200,7 +201,8 @@ function createRuntime(config, options) {
|
|
|
200
201
|
const attempted = runtime.sha256 === downloaded.sha256 && !(runtime.stage === "applying" || retryable);
|
|
201
202
|
if (downloaded.stage !== "downloaded" || downloaded.error || attempted) return;
|
|
202
203
|
// A damaged worker record means no known worker.
|
|
203
|
-
const
|
|
204
|
+
const workerPath = path.join(directory, "worker-v2.json");
|
|
205
|
+
const worker = await readJson(workerPath, null).catch((error) => { if (!damaged(error)) throw error; return null; });
|
|
204
206
|
if (worker?.client === client && Number.isSafeInteger(worker.pid) && alive(worker.pid)) return;
|
|
205
207
|
const launch = options.spawnWorker || launchWorker;
|
|
206
208
|
let pid;
|
|
@@ -210,13 +212,13 @@ function createRuntime(config, options) {
|
|
|
210
212
|
} catch (error) {
|
|
211
213
|
// Retain only this rejected launch attempt, not raw errors or proof of
|
|
212
214
|
// worker absence. Keep an earlier PID's conservative liveness check.
|
|
213
|
-
await atomicJson(
|
|
215
|
+
await atomicJson(workerPath, { client,
|
|
214
216
|
...(worker?.client === client && Number.isSafeInteger(worker.pid) ? { pid: worker.pid } : {}),
|
|
215
217
|
sha256: downloaded.sha256, error: "client_update_worker_launch_failed", failedAt: Date.now(),
|
|
216
218
|
}).catch(() => {});
|
|
217
219
|
throw error;
|
|
218
220
|
}
|
|
219
|
-
await atomicJson(
|
|
221
|
+
await atomicJson(workerPath, { client, pid });
|
|
220
222
|
});
|
|
221
223
|
}
|
|
222
224
|
async function applyPending() {
|
|
@@ -236,7 +238,7 @@ function createRuntime(config, options) {
|
|
|
236
238
|
if (sha(await safeFile(path.join(proof.packageRoot, "hellolynk-client-pack-manifest.json")))
|
|
237
239
|
!== sha(await safeFile(path.join(installContext.packageRoot, "hellolynk-client-pack-manifest.json")))) throw failure("adoption_invalid");
|
|
238
240
|
const doctor = await (options.runInstaller || runInstaller)({ executable: installContext.nodePath || process.execPath,
|
|
239
|
-
args: [path.join(installContext.packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor"], cwd: installContext.packageRoot, root, timeoutMs: 180_000 });
|
|
241
|
+
args: [path.join(installContext.packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor", "--json"], cwd: installContext.packageRoot, root, timeoutMs: 180_000 });
|
|
240
242
|
if (!doctorProves(doctor, client, currentVersion)) throw failure("adoption_invalid");
|
|
241
243
|
const adoptedRoot = path.join(path.dirname(root), "releases", `${client}-${sha(await safeFile(path.join(proof.packageRoot, "hellolynk-client-pack-manifest.json")))}`);
|
|
242
244
|
if (!options.runInstaller) await verifyPackage(adoptedRoot, client, currentVersion);
|
|
@@ -274,7 +276,7 @@ function createRuntime(config, options) {
|
|
|
274
276
|
try {
|
|
275
277
|
await verifyPackage(packageRoot, client, version);
|
|
276
278
|
const inspected = await run({ executable: installContext.nodePath || process.execPath,
|
|
277
|
-
args: [path.join(packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor"], cwd: packageRoot, root, timeoutMs: 180_000 });
|
|
279
|
+
args: [path.join(packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor", "--json"], cwd: packageRoot, root, timeoutMs: 180_000 });
|
|
278
280
|
return doctorProves(inspected, client, version);
|
|
279
281
|
} catch { return false; }
|
|
280
282
|
};
|
|
@@ -397,7 +399,7 @@ async function prepareCandidate(config, record) {
|
|
|
397
399
|
const directory = path.join(config.root, "client-updates", config.client);
|
|
398
400
|
if (!HASH.test(record.sha256) || typeof record.version !== "string" || !/^[0-9A-Za-z.+-]{1,100}$/.test(record.version)
|
|
399
401
|
|| typeof record.filename !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.zip$/.test(record.filename)) throw failure("candidate_invalid");
|
|
400
|
-
const artifactPath = path.join(directory,
|
|
402
|
+
const artifactPath = path.join(directory, CLIENT_UPDATE_RELEASES, `${record.version}-${record.sha256}`, record.filename);
|
|
401
403
|
if (artifactPath !== record.artifactPath) throw failure("candidate_path_invalid");
|
|
402
404
|
const archive = await safeFile(artifactPath, 64 * 1024 * 1024);
|
|
403
405
|
if (archive.length !== record.bytes || sha(archive) !== record.sha256) throw failure("artifact_integrity_invalid");
|
|
@@ -406,43 +408,63 @@ async function prepareCandidate(config, record) {
|
|
|
406
408
|
const extractedRoot = path.join(temporaryRoot, "extracted");
|
|
407
409
|
try {
|
|
408
410
|
// Re-extract the hash-verified archive; never execute a mutable earlier
|
|
409
|
-
// extractedRoot or an executable path supplied by state.
|
|
411
|
+
// extractedRoot or an executable path supplied by update state.
|
|
410
412
|
const sealed = path.join(temporaryRoot, record.filename);
|
|
411
413
|
await fs.writeFile(sealed, archive, { flag: "wx", mode: 0o600 });
|
|
412
|
-
await extractClientUpdateArchive({ archivePath: sealed, destination: extractedRoot });
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const descriptor = await readJson(path.join(extractedRoot, "trusted-release.json"));
|
|
416
|
-
const manifestBytes = await safeFile(path.join(extractedRoot, "dist", "package", "hellolynk-dsh-nav-build.json"));
|
|
417
|
-
if (descriptor?.contract !== "hellolynk-dsh-nav-release-v1" || descriptor.manifestSha256 !== sha(manifestBytes)) throw failure("package_pin_invalid");
|
|
418
|
-
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
419
|
-
if (manifest.contract !== "hellolynk-dsh-nav-build-v3") throw failure("package_pin_invalid");
|
|
420
|
-
await verifyRecords(path.join(extractedRoot, "dist", "package"), manifest.packageFiles, ["hellolynk-dsh-nav-build.json"]);
|
|
421
|
-
packageRoot = path.join(extractedRoot, "dist", "package", "runtime", "connect");
|
|
422
|
-
await safeFile(path.join(extractedRoot, "scripts", "install.mjs"));
|
|
423
|
-
} else {
|
|
424
|
-
const candidates = [extractedRoot, path.join(extractedRoot, "package"), path.join(extractedRoot, `hellolynk-${config.client}-${record.version}`, "package")];
|
|
425
|
-
const matches = [];
|
|
426
|
-
for (const candidate of candidates) if (await exists(path.join(candidate, "package.json"))) matches.push(candidate);
|
|
427
|
-
if (matches.length !== 1) throw failure("package_identity_invalid");
|
|
428
|
-
packageRoot = matches[0];
|
|
429
|
-
}
|
|
430
|
-
await verifyPackage(packageRoot, config.client, record.version);
|
|
414
|
+
await extractClientUpdateArchive({ archivePath: sealed, destination: extractedRoot, platform: config.platform });
|
|
415
|
+
const { packageRoot } = await verifyExtractedClientUpdatePackage(extractedRoot,
|
|
416
|
+
{ client: config.client, platform: config.platform, version: record.version });
|
|
431
417
|
return { temporaryRoot, extractedRoot, artifactPath: sealed, packageRoot, setupPath: path.join(packageRoot, "bin", "hellolynk-client-setup.mjs") };
|
|
432
418
|
} catch (error) { await fs.rm(temporaryRoot, { recursive: true, force: true }); throw error; }
|
|
433
419
|
}
|
|
434
420
|
|
|
421
|
+
export async function verifyExtractedClientUpdatePackage(extractedRoot, { client, platform, version }) {
|
|
422
|
+
if (!CLIENT_UPDATE_CLIENTS.includes(client) || typeof version !== "string" || !VERSION.test(version)) throw failure("package_identity_invalid");
|
|
423
|
+
let packageRoot;
|
|
424
|
+
if (client === "dsh") {
|
|
425
|
+
const descriptor = await readJson(path.join(extractedRoot, "trusted-release.json"));
|
|
426
|
+
const manifestBytes = await safeFile(path.join(extractedRoot, "dist", "package", "hellolynk-dsh-nav-build.json"));
|
|
427
|
+
if (descriptor?.contract !== "hellolynk-dsh-nav-release-v1" || descriptor.manifestSha256 !== sha(manifestBytes)) throw failure("package_pin_invalid");
|
|
428
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
429
|
+
if (manifest.contract !== "hellolynk-dsh-nav-build-v3") throw failure("package_pin_invalid");
|
|
430
|
+
await verifyRecords(path.join(extractedRoot, "dist", "package"), manifest.packageFiles, ["hellolynk-dsh-nav-build.json"]);
|
|
431
|
+
packageRoot = path.join(extractedRoot, "dist", "package", "runtime", "connect");
|
|
432
|
+
await safeFile(path.join(extractedRoot, "scripts", "install.mjs"));
|
|
433
|
+
} else {
|
|
434
|
+
const id = clientUpdateCatalogId(client, platform) || client;
|
|
435
|
+
const candidates = [extractedRoot, path.join(extractedRoot, "package"), path.join(extractedRoot, `hellolynk-${id}-${version}`, "package")];
|
|
436
|
+
const matches = [];
|
|
437
|
+
for (const candidate of candidates) if (await exists(path.join(candidate, "package.json"))) matches.push(candidate);
|
|
438
|
+
if (matches.length !== 1) throw failure("package_identity_invalid");
|
|
439
|
+
packageRoot = matches[0];
|
|
440
|
+
}
|
|
441
|
+
const checked = await verifyPackage(packageRoot, client, version);
|
|
442
|
+
return { packageRoot, ...checked };
|
|
443
|
+
}
|
|
444
|
+
|
|
435
445
|
async function verifyPackage(root, client, version) {
|
|
436
446
|
const pkg = await readJson(path.join(root, "package.json"));
|
|
437
447
|
const manifestBytes = await safeFile(path.join(root, "hellolynk-client-pack-manifest.json"));
|
|
438
448
|
const pin = await readJson(path.join(root, "hellolynk-client-pack-pin.json"));
|
|
439
449
|
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
440
450
|
if (pkg.version !== version || pkg.hellolynkClient?.client !== client || pkg.name !== NAMES[client]
|
|
451
|
+
|| (MAC_CLIENT_VERSION.test(version) && (!clientUpdateCatalogId(client, "darwin") || pkg.os?.[0] !== "darwin" || pkg.cpu?.[0] !== "arm64"))
|
|
441
452
|
|| manifest.contract !== "hellolynk-client-npm-pack-manifest-v1" || manifest.client !== client
|
|
442
453
|
|| manifest.packageName !== pkg.name || manifest.version !== version
|
|
443
454
|
|| pin.contract !== "hellolynk-client-npm-pack-pin-v1" || pin.manifestSha256 !== sha(manifestBytes)) throw failure("package_identity_invalid");
|
|
444
455
|
await verifyRecords(root, manifest.files, ["hellolynk-client-pack-manifest.json", "hellolynk-client-pack-pin.json"]);
|
|
445
456
|
if (!manifest.files.some((file) => file.path === "bin/hellolynk-client-setup.mjs")) throw failure("package_pin_invalid");
|
|
457
|
+
if (MAC_CLIENT_VERSION.test(version)) {
|
|
458
|
+
const helpers = client === "codex"
|
|
459
|
+
? ["plugins/hellolynk-codex/runtime/platform/darwin", "plugins/hellolynk-codex/runtime/bootstrap/platform/darwin"]
|
|
460
|
+
: ["runtime/platform/darwin"];
|
|
461
|
+
for (const relative of helpers) {
|
|
462
|
+
const file = `${relative}/hellolynk-keychain-helper.bin`;
|
|
463
|
+
if (!manifest.files.some(record => record.path === file)
|
|
464
|
+
|| (process.platform !== "win32" && !((await fs.lstat(path.join(root, file))).mode & 0o100))) throw failure("package_pin_invalid");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return { coreRoot: pkg.hellolynkClient.coreRoot, files: manifest.files };
|
|
446
468
|
}
|
|
447
469
|
async function verifyRecords(root, records, allowedExtra) {
|
|
448
470
|
if (!Array.isArray(records) || records.length < 1 || records.length > 5000) throw failure("package_pin_invalid");
|
|
@@ -471,7 +493,9 @@ async function verifyInstalledContext(config) {
|
|
|
471
493
|
await verifyPackage(context.packageRoot, config.client, config.currentVersion);
|
|
472
494
|
}
|
|
473
495
|
function normalize(value) {
|
|
496
|
+
const platform = value.platform || process.platform;
|
|
474
497
|
if (!CLIENT_UPDATE_CLIENTS.includes(value.client) || typeof value.currentVersion !== "string" || !VERSION.test(value.currentVersion)
|
|
498
|
+
|| (platform === "darwin" && !MAC_CLIENT_VERSION.test(value.currentVersion))
|
|
475
499
|
|| !path.isAbsolute(String(value.root || "")) || !path.isAbsolute(String(value.installContext?.packageRoot || ""))) throw failure("configuration_invalid");
|
|
476
500
|
const context = { packageRoot: path.resolve(value.installContext.packageRoot) };
|
|
477
501
|
for (const name of ["nodePath", "dshProfile", "dshRoot"]) {
|
|
@@ -484,7 +508,7 @@ function normalize(value) {
|
|
|
484
508
|
if (!HASH.test(value.installContext.expectedInstalledManifestSha256)) throw failure("configuration_invalid");
|
|
485
509
|
context.expectedInstalledManifestSha256 = value.installContext.expectedInstalledManifestSha256;
|
|
486
510
|
}
|
|
487
|
-
return { root: path.resolve(value.root), client: value.client, currentVersion: value.currentVersion, installContext: context };
|
|
511
|
+
return { root: path.resolve(value.root), client: value.client, currentVersion: value.currentVersion, platform, installContext: context };
|
|
488
512
|
}
|
|
489
513
|
async function launchWorker(config) {
|
|
490
514
|
// A host may execute this module from an ephemeral captured import graph.
|
|
@@ -510,8 +534,9 @@ async function launchWorker(config) {
|
|
|
510
534
|
async function runInstaller({ executable, args, cwd, root, timeoutMs }) {
|
|
511
535
|
executable = await runtimeSafety.resolveHelloLynkStandaloneNode({ preferredExecutable: executable });
|
|
512
536
|
const env = { ...runtimeSafety.standaloneHelloLynkNodeEnvironment(process.env), HELLOLYNK_NEUTRAL_STORE_ROOT: root };
|
|
537
|
+
const mac = process.platform === "darwin";
|
|
513
538
|
return new Promise((resolve, reject) => {
|
|
514
|
-
const child = spawn(executable, args, { cwd, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
539
|
+
const child = spawn(executable, args, { cwd, env, detached: mac, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
515
540
|
let stdout = "", stderr = "", finished = false, terminalError = null;
|
|
516
541
|
const timer = setTimeout(() => terminate(failure("installer_timeout")), timeoutMs);
|
|
517
542
|
function finish(error, value) { if (finished) return; finished = true; clearTimeout(timer); error ? reject(error) : resolve(value); }
|
|
@@ -529,6 +554,9 @@ async function runInstaller({ executable, args, cwd, root, timeoutMs }) {
|
|
|
529
554
|
killer.on("error", () => { clearTimeout(killTimer); if (!finished) killChild(); });
|
|
530
555
|
killer.once("close", () => { clearTimeout(killTimer); if (!finished) killChild(); });
|
|
531
556
|
} catch { killChild(); }
|
|
557
|
+
} else if (mac && child.pid) {
|
|
558
|
+
// The installer owns this process group, including a synchronous Codex CLI.
|
|
559
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch { killChild(); }
|
|
532
560
|
} else killChild();
|
|
533
561
|
}
|
|
534
562
|
child.on("error", () => {
|
|
@@ -580,7 +608,8 @@ function damaged(error) { return error instanceof SyntaxError || error?.code ===
|
|
|
580
608
|
// Windows paths compare case-insensitively and may differ by a link or short name.
|
|
581
609
|
async function samePath(left, right) {
|
|
582
610
|
if (typeof left !== "string" || typeof right !== "string" || !path.isAbsolute(left) || !path.isAbsolute(right)) return false;
|
|
583
|
-
const
|
|
611
|
+
const canonical = (value) => process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
612
|
+
const same = (a, b) => canonical(a) === canonical(b);
|
|
584
613
|
if (same(left, right)) return true;
|
|
585
614
|
try { return same(await fs.realpath(left), await fs.realpath(right)); } catch { return false; }
|
|
586
615
|
}
|