@apilift/cli 1.4.28 → 1.4.29

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.
@@ -21,7 +21,7 @@ function dataRoot(env) {
21
21
  const home = homedir();
22
22
  if (platform() === "darwin") return join(home, "Library", "Application Support", "apilift");
23
23
  if (platform() === "win32") return join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "apilift");
24
- return join(env.XDG_DATA_HOME || join(home, ".local", "share"), "apilift");
24
+ return join(env.XDG_DATA_HOME && isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : join(home, ".local", "share"), "apilift");
25
25
  }
26
26
 
27
27
  // src/capture-server/server.ts
@@ -19868,7 +19868,7 @@ var extensionPageMessageSchema = external_exports.discriminatedUnion("operation"
19868
19868
 
19869
19869
  // src/capture-server/store.ts
19870
19870
  import { randomUUID, createHash } from "node:crypto";
19871
- import { mkdirSync, chmodSync } from "node:fs";
19871
+ import { mkdirSync, chmodSync, readFileSync as readFileSync2 } from "node:fs";
19872
19872
  import { join as join2 } from "node:path";
19873
19873
  import { execFileSync } from "node:child_process";
19874
19874
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
@@ -19881,28 +19881,66 @@ var CaptureError = class extends Error {
19881
19881
  var json3 = (value) => JSON.stringify(value);
19882
19882
  var readJson = (value) => JSON.parse(String(value));
19883
19883
  var scopeTtl = { inspect: 3e4, execute: 12e4, capture: 72e5 };
19884
- function ownerProcessAlive(pid, expectedStartedAt, probe = {}) {
19885
- if (!Number.isInteger(pid) || pid <= 0 || !Number.isFinite(expectedStartedAt)) return false;
19886
- const alive = probe.alive ?? ((candidate2) => {
19887
- try {
19888
- process.kill(candidate2, 0);
19889
- return true;
19890
- } catch {
19884
+ var pidAlive = (candidate2) => {
19885
+ try {
19886
+ process.kill(candidate2, 0);
19887
+ return true;
19888
+ } catch {
19889
+ return false;
19890
+ }
19891
+ };
19892
+ var inspectProcess = (command, args, timeout) => execFileSync(command, args, { encoding: "utf8", timeout, windowsHide: true, env: { ...process.env, LC_ALL: "C", TZ: "UTC" }, stdio: ["ignore", "pipe", "ignore"] }).trim();
19893
+ var linuxClockTicks;
19894
+ function createOwnerVerifier(probe = {}) {
19895
+ const cached2 = /* @__PURE__ */ new Map();
19896
+ const alive = probe.alive ?? pidAlive, now = probe.now ?? Date.now;
19897
+ return (pid, startedAt, fresh = false) => {
19898
+ if (!Number.isInteger(pid) || pid <= 0 || !Number.isFinite(startedAt) || !alive(pid)) {
19899
+ cached2.delete(pid);
19891
19900
  return false;
19892
19901
  }
19893
- });
19894
- const inspect = probe.inspect ?? ((command, args, timeout) => execFileSync(command, args, { encoding: "utf8", timeout }).trim());
19902
+ if ((probe.platform ?? process.platform) === "linux") return inspectOwner(pid, startedAt, probe);
19903
+ const prior = cached2.get(pid), time3 = now();
19904
+ if (!fresh && prior && prior.startedAt === startedAt && time3 >= prior.checkedAt && time3 - prior.checkedAt < 2e3) return true;
19905
+ cached2.delete(pid);
19906
+ if (!inspectOwner(pid, startedAt, probe)) return false;
19907
+ if (cached2.size >= 256) cached2.delete(cached2.keys().next().value);
19908
+ cached2.set(pid, { startedAt, checkedAt: now() });
19909
+ return true;
19910
+ };
19911
+ }
19912
+ var defaultOwnerVerifier = createOwnerVerifier();
19913
+ function ownerProcessAlive(pid, expectedStartedAt, probe) {
19914
+ return typeof probe === "object" ? inspectOwner(pid, expectedStartedAt, probe) : defaultOwnerVerifier(pid, expectedStartedAt, probe);
19915
+ }
19916
+ function inspectOwner(pid, expectedStartedAt, probe) {
19917
+ if (!Number.isInteger(pid) || pid <= 0 || !Number.isFinite(expectedStartedAt)) return false;
19918
+ const alive = probe.alive ?? pidAlive;
19919
+ const inspect = probe.inspect ?? inspectProcess;
19895
19920
  const platform2 = probe.platform ?? process.platform;
19896
19921
  const now = probe.now ?? Date.now;
19897
19922
  const started = now();
19898
19923
  const windows = platform2 === "win32";
19924
+ if (platform2 === "linux") {
19925
+ try {
19926
+ const read = probe.read ?? ((path) => readFileSync2(path, "utf8"));
19927
+ const stat = read(`/proc/${pid}/stat`), fields = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/u);
19928
+ if (["Z", "X"].includes(fields[0])) return false;
19929
+ const ticks = Number(fields[19]), boot = Number(/^btime (\d+)$/mu.exec(read("/proc/stat"))?.[1]);
19930
+ const hz = probe.inspect ? Number(inspect("getconf", ["CLK_TCK"], 2e3)) : linuxClockTicks ??= Number(inspect("getconf", ["CLK_TCK"], 2e3));
19931
+ const observed = boot * 1e3 + ticks * 1e3 / hz;
19932
+ return Number.isFinite(observed) && hz > 0 && Math.abs(observed - expectedStartedAt) <= 5e3;
19933
+ } catch {
19934
+ return false;
19935
+ }
19936
+ }
19899
19937
  for (let attempt = 0; attempt < (windows ? 2 : 1); attempt++) {
19900
19938
  if (!alive(pid)) return false;
19901
19939
  const remaining = windows ? 8e3 - (now() - started) : 2e3;
19902
19940
  if (remaining <= 0) return false;
19903
19941
  try {
19904
- const raw = windows ? inspect("powershell.exe", ["-NoProfile", "-Command", `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString('o')`], Math.min(4e3, remaining)) : inspect("ps", ["-o", "lstart=", "-p", String(pid)], 2e3);
19905
- const observed = Date.parse(raw.trim());
19942
+ const raw = windows ? inspect("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().ToString('o')`], Math.min(4e3, remaining)) : inspect("ps", ["-o", "lstart=", "-p", String(pid)], 2e3);
19943
+ const observed = Date.parse(windows ? raw.trim() : `${raw.trim()} UTC`);
19906
19944
  return Number.isFinite(observed) && Math.abs(observed - expectedStartedAt) <= 5e3;
19907
19945
  } catch (error62) {
19908
19946
  if (!windows || error62.code !== "ETIMEDOUT") return false;
@@ -19972,18 +20010,22 @@ var CaptureStore = class {
19972
20010
  const now = this.clock();
19973
20011
  const rows = this.db.prepare("SELECT id,owner_pid,owner_started_at,expires_at FROM scopes WHERE retained_at IS NULL").all();
19974
20012
  const stale = rows.filter((row) => row.expires_at <= now || !this.alive(row.owner_pid, row.owner_started_at));
19975
- for (const row of stale) {
19976
- const capture = this.db.prepare("SELECT kind,window_json FROM scopes WHERE id=?").get(row.id);
19977
- if (capture.kind === "capture" && !capture.window_json && this.db.prepare("SELECT 1 FROM capture_candidates WHERE scope_id=? LIMIT 1").get(row.id)) this.retainScope(row.id);
19978
- else this.db.prepare("DELETE FROM scopes WHERE id=?").run(row.id);
19979
- }
20013
+ for (const row of stale) this.releaseStaleScope(row.id);
19980
20014
  this.db.prepare("DELETE FROM jobs WHERE status='delivered' AND delivered_at < ?").run(now - 6e5);
19981
20015
  this.db.prepare("DELETE FROM bridge_demands WHERE expires_at <= ?").run(now);
19982
- if (stale.length) this.emit();
19983
20016
  }
19984
- row(id2) {
19985
- this.prune();
19986
- const row = this.db.prepare("SELECT * FROM scopes WHERE id=?").get(id2);
20017
+ releaseStaleScope(id2) {
20018
+ const capture = this.db.prepare("SELECT kind,window_json FROM scopes WHERE id=?").get(id2);
20019
+ if (capture?.kind === "capture" && !capture.window_json && this.db.prepare("SELECT 1 FROM capture_candidates WHERE scope_id=? LIMIT 1").get(id2)) this.retainScope(id2);
20020
+ else this.db.prepare("DELETE FROM scopes WHERE id=?").run(id2);
20021
+ this.emit();
20022
+ }
20023
+ row(id2, freshOwner = false) {
20024
+ let row = this.db.prepare("SELECT * FROM scopes WHERE id=?").get(id2);
20025
+ if (row && row.retained_at === null && (Number(row.expires_at) <= this.clock() || !(freshOwner ? this.alive(Number(row.owner_pid), Number(row.owner_started_at), true) : this.alive(Number(row.owner_pid), Number(row.owner_started_at))))) {
20026
+ this.releaseStaleScope(id2);
20027
+ row = this.db.prepare("SELECT * FROM scopes WHERE id=?").get(id2);
20028
+ }
19987
20029
  if (!row) throw new CaptureError("scope-expired", "Browser scope is missing or its owner exited");
19988
20030
  return row;
19989
20031
  }
@@ -20013,7 +20055,7 @@ var CaptureStore = class {
20013
20055
  throw new CaptureError("invalid-scope", "Browser tab URL must belong to the page origin");
20014
20056
  }
20015
20057
  }
20016
- if (!this.alive(input2.ownerPid, input2.ownerStartedAt)) throw new CaptureError("owner-exited", "Scope owner process is no longer running");
20058
+ if (!this.alive(input2.ownerPid, input2.ownerStartedAt, true)) throw new CaptureError("owner-exited", "Scope owner process is no longer running");
20017
20059
  this.prune();
20018
20060
  return this.transaction(() => {
20019
20061
  if (input2.kind !== "inspect") {
@@ -20067,7 +20109,7 @@ var CaptureStore = class {
20067
20109
  return row ? { seq: row.seq, entry: readJson(row.entry_json) } : null;
20068
20110
  }
20069
20111
  enqueueJob(scopeId, envelope) {
20070
- const scope2 = this.row(scopeId);
20112
+ const scope2 = this.row(scopeId, true);
20071
20113
  if (scope2.retained_at !== null) throw new CaptureError("scope-released", "Browser ownership was released; retained evidence cannot run jobs");
20072
20114
  if (scope2.kind === "inspect") throw new CaptureError("inspect-only", "Inspect scopes cannot run browser jobs");
20073
20115
  const parsed = nativeJobSchema.safeParse(envelope.job);
@@ -20201,6 +20243,7 @@ var CaptureStore = class {
20201
20243
  case "jobs.poll": {
20202
20244
  const job = this.db.prepare("SELECT id,envelope_json FROM jobs WHERE scope_id=? AND status='queued' ORDER BY created_at LIMIT 1").get(scopeId);
20203
20245
  if (!job) return {};
20246
+ this.row(scopeId, true);
20204
20247
  this.db.prepare("UPDATE jobs SET status='dispatched',dispatched_at=? WHERE id=?").run(now, job.id);
20205
20248
  return readJson(job.envelope_json);
20206
20249
  }
@@ -20371,7 +20414,7 @@ async function startCaptureServer(options) {
20371
20414
  });
20372
20415
  const address = server.address();
20373
20416
  if (!address || typeof address === "string") throw new Error("Capture server did not bind a TCP port");
20374
- const receipt = { pid: process.pid, port: address.port, token, version: options.version, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
20417
+ const receipt = { pid: process.pid, port: address.port, token, version: options.version, startedAt: new Date(Date.now() - process.uptime() * 1e3).toISOString() };
20375
20418
  const receiptPath = join3(root, "server.json");
20376
20419
  const temp = join3(root, `.server-${process.pid}-${randomBytes(4).toString("hex")}.json`);
20377
20420
  await writeFile(temp, JSON.stringify(receipt), { mode: 384 });
@@ -7182,7 +7182,7 @@ function dataRoot(env) {
7182
7182
  const home = homedir();
7183
7183
  if (platform() === "darwin") return join(home, "Library", "Application Support", "apilift");
7184
7184
  if (platform() === "win32") return join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "apilift");
7185
- return join(env.XDG_DATA_HOME || join(home, ".local", "share"), "apilift");
7185
+ return join(env.XDG_DATA_HOME && isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : join(home, ".local", "share"), "apilift");
7186
7186
  }
7187
7187
  var LocalState = class {
7188
7188
  db;
@@ -27558,27 +27558,38 @@ function readable(v2) {
27558
27558
  }
27559
27559
  function commandIdentity(command) {
27560
27560
  let source = command;
27561
+ let shell = null;
27561
27562
  try {
27562
27563
  const args = obj(JSON.parse(command));
27563
27564
  source = text(args.cmd) || text(args.command) || command;
27565
+ if (/(?:^|[\\/])(?:powershell|pwsh|cmd)(?:\.exe)?$/iu.test(text(args.shell))) shell = "windows";
27566
+ else if (/(?:^|[\\/])(?:bash|zsh|sh)(?:\.exe)?$/iu.test(text(args.shell))) shell = "posix";
27564
27567
  } catch {
27565
27568
  }
27566
27569
  source = source.replace(/\s+2>&1\s*$/u, "");
27570
+ const windows = shell ? shell === "windows" : process.platform === "win32" || /^\s*&\s+/u.test(source) || /^\s*(?:["']?(?:[A-Za-z]:\\|\\\\)|node\.exe\s+|node\s+["']?[A-Za-z]:\\|apilift\.(?:cmd|exe|ps1)\b)/iu.test(source);
27571
+ if (windows) source = source.replace(/^\s*&\s+/u, "");
27567
27572
  const words = [];
27568
27573
  let word = "", quote = "", escaped = false, started = false;
27569
- for (const char of source) {
27574
+ for (let offset = 0; offset < source.length; offset++) {
27575
+ const char = source[offset];
27570
27576
  if (escaped) {
27571
27577
  word += char;
27572
27578
  escaped = false;
27573
27579
  continue;
27574
27580
  }
27575
- if (char === "\\" && quote !== "'") {
27581
+ if (char === "\\" && quote !== "'" && !windows) {
27576
27582
  escaped = true;
27577
27583
  started = true;
27578
27584
  continue;
27579
27585
  }
27580
27586
  if (quote) {
27581
- if (quote === '"' && (char === "$" || char === "`")) return null;
27587
+ if (windows && quote === "'" && char === "'" && source[offset + 1] === "'") {
27588
+ word += "'";
27589
+ offset++;
27590
+ continue;
27591
+ }
27592
+ if (quote === '"' && (char === "$" || char === "`" || windows && (char === "%" || char === "!"))) return null;
27582
27593
  if (char === quote) quote = "";
27583
27594
  else word += char;
27584
27595
  continue;
@@ -27589,6 +27600,7 @@ function commandIdentity(command) {
27589
27600
  continue;
27590
27601
  }
27591
27602
  if (/[;&|`$()]/u.test(char)) return null;
27603
+ if (windows && /[%!^<>]/u.test(char)) return null;
27592
27604
  if (/\s/u.test(char)) {
27593
27605
  if (started) {
27594
27606
  words.push(word);
@@ -27605,9 +27617,9 @@ function commandIdentity(command) {
27605
27617
  let index = 0;
27606
27618
  if (/(?:^|\/)env$/u.test(words[index] ?? "")) index++;
27607
27619
  while (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(words[index] ?? "")) index++;
27608
- if (/(?:^|\/)node(?:\.exe)?$/u.test(words[index] ?? "")) index++;
27620
+ if (/(?:^|[\\/])node(?:\.exe)?$/iu.test(words[index] ?? "")) index++;
27609
27621
  if (words[index] === "./dev" && words[index + 1] === "apilift") index++;
27610
- if (!/(?:^|\/)apilift(?:\.js)?$/u.test(words[index] ?? "")) return null;
27622
+ if (!/(?:^|[\\/])apilift(?:\.(?:js|cmd|exe|ps1))?$/iu.test(words[index] ?? "")) return null;
27611
27623
  let argv = words.slice(index + 1);
27612
27624
  if (argv[0] === "--session") argv = argv.slice(2);
27613
27625
  if (!argv.length) return null;
@@ -9185,7 +9185,7 @@ function dataRoot(env) {
9185
9185
  const home = homedir();
9186
9186
  if (platform() === "darwin") return join(home, "Library", "Application Support", "apilift");
9187
9187
  if (platform() === "win32") return join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "apilift");
9188
- return join(env.XDG_DATA_HOME || join(home, ".local", "share"), "apilift");
9188
+ return join(env.XDG_DATA_HOME && isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : join(home, ".local", "share"), "apilift");
9189
9189
  }
9190
9190
  var LocalState = class {
9191
9191
  db;
@@ -31740,12 +31740,13 @@ function probeState(root, now = Date.now()) {
31740
31740
  // src/native-host/registration.ts
31741
31741
  import { homedir as homedir2, platform as platform2 } from "node:os";
31742
31742
  import { dirname as dirname2, isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "node:path";
31743
- function manifestDirectory(env = process.env) {
31743
+ var absoluteEnv = (value) => value && isAbsolute2(value) ? value : void 0;
31744
+ function manifestDirectory(env = process.env, system = { platform: platform2(), home: homedir2() }) {
31744
31745
  if (env.APILIFT_NATIVE_MESSAGING_DIR) return resolve2(env.APILIFT_NATIVE_MESSAGING_DIR);
31745
- const home = homedir2();
31746
- if (platform2() === "darwin") return join5(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
31747
- if (platform2() === "win32") throw new Error("Windows native-host registration needs a data-root manifest and HKCU registry entry");
31748
- return join5(home, ".config", "google-chrome", "NativeMessagingHosts");
31746
+ const home = system.home;
31747
+ if (system.platform === "darwin") return join5(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
31748
+ if (system.platform === "win32") throw new Error("Windows native-host registration needs a data-root manifest and HKCU registry entry");
31749
+ return join5(absoluteEnv(env.CHROME_CONFIG_HOME) || absoluteEnv(env.XDG_CONFIG_HOME) || join5(home, ".config"), "google-chrome", "NativeMessagingHosts");
31749
31750
  }
31750
31751
 
31751
31752
  // src/bin/native-host.ts
@@ -7,20 +7,84 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
7
7
  import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
8
8
 
9
9
  // src/native-host/registration.ts
10
- import { createHash } from "node:crypto";
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import { execFileSync } from "node:child_process";
12
+ import { mkdirSync, writeFileSync } from "node:fs";
11
13
  import { homedir, platform } from "node:os";
12
14
  import { dirname, isAbsolute, join, resolve } from "node:path";
13
- import { chmod, mkdir, readFile, rename, writeFile, copyFile } from "node:fs/promises";
15
+ import { chmod, mkdir, readFile, rename, writeFile, copyFile, rm, stat } from "node:fs/promises";
14
16
  var fingerprint = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
17
  var hostNames = /* @__PURE__ */ new Set(["com.apilift.browser", "com.apilift.browser.dev"]);
16
18
  var validId = /^[a-p]{32}$/;
17
19
  var posixQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
18
- function manifestDirectory(env = process.env) {
20
+ var absoluteEnv = (value) => value && isAbsolute(value) ? value : void 0;
21
+ function batchLiteral(value) {
22
+ if (/["\r\n\0]/u.test(value)) throw new Error("Native host launcher values contain unsupported control characters");
23
+ return value.replaceAll("%", "%%");
24
+ }
25
+ function nativeLauncher(values, windows) {
26
+ const environment = { APILIFT_HOME: values.root, APILIFT_NATIVE_HOST_NAME: values.hostName, APILIFT_NATIVE_MESSAGING_DIR: values.manifestRoot, ...values.site ? { APILIFT_URL: values.site } : {} };
27
+ if (windows) return `@echo off\r
28
+ chcp 65001 >nul 2>&1\r
29
+ setlocal DisableDelayedExpansion\r
30
+ ${Object.entries(environment).map(([key, value]) => `set "${key}=${batchLiteral(value)}"\r
31
+ `).join("")}"${batchLiteral(values.nodePath)}" "${batchLiteral(values.hostScript)}" %*\r
32
+ exit /b %errorlevel%\r
33
+ `;
34
+ return `#!/bin/sh
35
+ ${Object.entries(environment).map(([key, value]) => `${key}=${posixQuote(value)}`).join(" ")} exec ${posixQuote(values.nodePath)} ${posixQuote(values.hostScript)} "$@"
36
+ `;
37
+ }
38
+ function readWindowsHostValues(hostName, run = execFileSync) {
39
+ if (!/^[a-z0-9_]+(?:\.[a-z0-9_]+)*$/u.test(hostName)) throw new Error("Invalid native host name");
40
+ const script = `$ErrorActionPreference='Stop'; $results=@(); foreach($view in @('32','64')) {
41
+ $base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::CurrentUser,[Microsoft.Win32.RegistryView]('Registry'+$view));
42
+ try {
43
+ $key=$base.OpenSubKey('Software\\Google\\Chrome\\NativeMessagingHosts\\${hostName}');
44
+ $kind=$null; $data=$null;
45
+ if($null -ne $key) { try {
46
+ $value=$key.GetValue('',$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames);
47
+ if($null -ne $value) { $kind=$key.GetValueKind('').ToString(); $data=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject $value -Compress))); }
48
+ } finally { $key.Dispose(); } }
49
+ $results+=@{view=$view;present=($null -ne $key);kind=$kind;data=$data};
50
+ } finally { $base.Dispose(); }
51
+ }; ConvertTo-Json -InputObject @($results) -Compress`;
52
+ try {
53
+ const output = run("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { encoding: "utf8", timeout: 1e4, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
54
+ const values = JSON.parse(String(output));
55
+ if (!Array.isArray(values) || values.length !== 2) throw new Error("Invalid registry snapshot");
56
+ return ["32", "64"].map((view) => {
57
+ const items = values.filter((item2) => item2?.view === view);
58
+ const item = items[0];
59
+ if (items.length !== 1 || typeof item.present !== "boolean" || !(item.kind === null || typeof item.kind === "string") || !(item.data === null || typeof item.data === "string")) throw new Error("Invalid registry value");
60
+ return { view, present: item.present, kind: item.kind, value: item.data === null ? null : JSON.parse(Buffer.from(item.data, "base64").toString("utf8")) };
61
+ });
62
+ } catch {
63
+ throw new Error("Could not inspect the current user's Chrome native host registration");
64
+ }
65
+ }
66
+ function registerWindowsHost(hostName, manifest, owned, run = execFileSync, write = true, backup) {
67
+ const key = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${hostName}`;
68
+ const options2 = { encoding: "utf8", timeout: 1e4, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] };
69
+ const present = /* @__PURE__ */ new Set();
70
+ for (const value of readWindowsHostValues(hostName, run)) {
71
+ if (!value.present) continue;
72
+ const { view } = value;
73
+ present.add(view);
74
+ const current = value.kind === "String" && typeof value.value === "string" ? value.value : null;
75
+ if (!owned || current?.toLowerCase() !== manifest.toLowerCase()) {
76
+ if (!backup) throw new Error(`Native host registration is not owned by Apilift: ${key}`);
77
+ backup(view, JSON.stringify(value));
78
+ }
79
+ }
80
+ if (write) for (const view of /* @__PURE__ */ new Set(["32", ...present])) run("reg.exe", ["add", key, "/ve", "/t", "REG_SZ", "/d", manifest, "/f", `/reg:${view}`], options2);
81
+ }
82
+ function manifestDirectory(env = process.env, system = { platform: platform(), home: homedir() }) {
19
83
  if (env.APILIFT_NATIVE_MESSAGING_DIR) return resolve(env.APILIFT_NATIVE_MESSAGING_DIR);
20
- const home = homedir();
21
- if (platform() === "darwin") return join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
22
- if (platform() === "win32") throw new Error("Windows native-host registration needs a data-root manifest and HKCU registry entry");
23
- return join(home, ".config", "google-chrome", "NativeMessagingHosts");
84
+ const home = system.home;
85
+ if (system.platform === "darwin") return join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
86
+ if (system.platform === "win32") throw new Error("Windows native-host registration needs a data-root manifest and HKCU registry entry");
87
+ return join(absoluteEnv(env.CHROME_CONFIG_HOME) || absoluteEnv(env.XDG_CONFIG_HOME) || join(home, ".config"), "google-chrome", "NativeMessagingHosts");
24
88
  }
25
89
  async function registerNativeHost(options2) {
26
90
  const env = options2.env ?? process.env;
@@ -35,7 +99,24 @@ async function registerNativeHost(options2) {
35
99
  const manifestRoot = env.APILIFT_NATIVE_MESSAGING_DIR ? resolve(env.APILIFT_NATIVE_MESSAGING_DIR) : platform() === "win32" ? nativeRoot : manifestDirectory(env);
36
100
  const launcher = join(nativeRoot, platform() === "win32" ? "launcher.cmd" : "launcher.sh");
37
101
  const manifest = join(manifestRoot, `${hostName}.json`);
102
+ const manifests = [manifest];
103
+ if (platform() === "linux" && !env.APILIFT_NATIVE_MESSAGING_DIR) {
104
+ for (const browser of ["google-chrome-beta", "google-chrome-unstable", "chromium"]) {
105
+ const profile = join(dirname(dirname(manifestRoot)), browser);
106
+ if (await stat(profile).then((value) => value.isDirectory()).catch(() => false)) manifests.push(join(profile, "NativeMessagingHosts", `${hostName}.json`));
107
+ }
108
+ }
38
109
  const receiptPath = join(nativeRoot, "registration.json");
110
+ const registryBackup = options2.allowReplaceUnowned ? (view, value) => {
111
+ const directory = join(nativeRoot, "backup");
112
+ mkdirSync(directory, { recursive: true, mode: 448 });
113
+ const path = join(directory, `${hostName}-registry-${view}-${fingerprint(value)}.json`);
114
+ try {
115
+ writeFileSync(path, JSON.stringify({ hostName, view, query: value }), { flag: "wx", mode: 384 });
116
+ } catch (error) {
117
+ if (error.code !== "EEXIST") throw error;
118
+ }
119
+ } : void 0;
39
120
  const previous = await readFile(receiptPath, "utf8").then((value) => JSON.parse(value)).catch(() => ({}));
40
121
  const guard = async (path) => {
41
122
  const bytes = await readFile(path).catch(() => null);
@@ -45,33 +126,33 @@ async function registerNativeHost(options2) {
45
126
  await copyFile(path, join(nativeRoot, "backup", `${fingerprint(bytes)}-${Date.now()}`));
46
127
  };
47
128
  await guard(launcher);
48
- await guard(manifest);
129
+ for (const path of manifests) await guard(path);
130
+ if (platform() === "win32" && !env.APILIFT_NATIVE_MESSAGING_DIR) {
131
+ const bytes = await readFile(manifest).catch(() => null);
132
+ registerWindowsHost(hostName, manifest, Boolean(bytes && previous[manifest] === fingerprint(bytes)), execFileSync, false, registryBackup);
133
+ }
49
134
  await mkdir(nativeRoot, { recursive: true, mode: 448 });
50
135
  await mkdir(manifestRoot, { recursive: true, mode: 448 });
51
- const launcherText = platform() === "win32" ? `@echo off\r
52
- set "APILIFT_HOME=${root}"\r
53
- set "APILIFT_NATIVE_HOST_NAME=${hostName}"\r
54
- ${siteUrl ? `set "APILIFT_URL=${siteUrl.origin}"\r
55
- ` : ""}${env.APILIFT_NATIVE_MESSAGING_DIR ? `set "APILIFT_NATIVE_MESSAGING_DIR=${manifestRoot}"\r
56
- ` : ""}"${nodePath}" "${hostScript}" %*\r
57
- ` : `#!/bin/sh
58
- APILIFT_HOME=${posixQuote(root)} APILIFT_NATIVE_HOST_NAME=${posixQuote(hostName)} ${siteUrl ? `APILIFT_URL=${posixQuote(siteUrl.origin)} ` : ""}${env.APILIFT_NATIVE_MESSAGING_DIR ? `APILIFT_NATIVE_MESSAGING_DIR=${posixQuote(manifestRoot)} ` : ""}exec ${posixQuote(nodePath)} ${posixQuote(hostScript)} "$@"
59
- `;
136
+ const launcherText = nativeLauncher({ root, hostName, nodePath, hostScript, manifestRoot, site: siteUrl?.origin }, platform() === "win32");
60
137
  const manifestText = JSON.stringify({ name: hostName, description: "Apilift browser bridge", path: launcher, type: "stdio", allowed_origins: [`chrome-extension://${options2.extensionId}/`] }, null, 2) + "\n";
61
138
  const atomicWrite = async (path, bytes, mode) => {
62
- const temp = join(dirname(path), `.${hostName}-${process.pid}-${Date.now()}.tmp`);
63
- await writeFile(temp, bytes, { mode });
64
- await chmod(temp, mode);
65
- await rename(temp, path);
139
+ const temp = join(dirname(path), `.${hostName}-${randomUUID()}.tmp`);
140
+ try {
141
+ await writeFile(temp, bytes, { mode, flag: "wx" });
142
+ await chmod(temp, mode);
143
+ await rename(temp, path);
144
+ } finally {
145
+ await rm(temp, { force: true });
146
+ }
66
147
  };
67
148
  await atomicWrite(launcher, launcherText, platform() === "win32" ? 384 : 448);
68
- await atomicWrite(manifest, manifestText, 384);
69
- const receipt = { ...previous, [launcher]: fingerprint(launcherText), [manifest]: fingerprint(manifestText) };
70
- await atomicWrite(receiptPath, JSON.stringify(receipt, null, 2) + "\n", 384);
71
- if (platform() === "win32" && !env.APILIFT_NATIVE_MESSAGING_DIR) {
72
- const { execFileSync } = await import("node:child_process");
73
- execFileSync("reg.exe", ["add", `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${hostName}`, "/ve", "/t", "REG_SZ", "/d", manifest, "/f"], { stdio: "ignore" });
149
+ for (const path of manifests) {
150
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
151
+ await atomicWrite(path, manifestText, 384);
74
152
  }
153
+ const receipt = { ...previous, [launcher]: fingerprint(launcherText), ...Object.fromEntries(manifests.map((path) => [path, fingerprint(manifestText)])) };
154
+ await atomicWrite(receiptPath, JSON.stringify(receipt, null, 2) + "\n", 384);
155
+ if (platform() === "win32" && !env.APILIFT_NATIVE_MESSAGING_DIR) registerWindowsHost(hostName, manifest, true, execFileSync, true, registryBackup);
75
156
  return { launcher, manifest, hostName };
76
157
  }
77
158
 
@@ -31861,7 +31861,7 @@ function dataRoot(env) {
31861
31861
  const home = homedir();
31862
31862
  if (platform() === "darwin") return join(home, "Library", "Application Support", "apilift");
31863
31863
  if (platform() === "win32") return join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "apilift");
31864
- return join(env.XDG_DATA_HOME || join(home, ".local", "share"), "apilift");
31864
+ return join(env.XDG_DATA_HOME && isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : join(home, ".local", "share"), "apilift");
31865
31865
  }
31866
31866
 
31867
31867
  // src/usage/measurement.ts
@@ -134465,27 +134465,38 @@ function readable(v2) {
134465
134465
  }
134466
134466
  function commandIdentity(command) {
134467
134467
  let source = command;
134468
+ let shell = null;
134468
134469
  try {
134469
134470
  const args = obj(JSON.parse(command));
134470
134471
  source = text(args.cmd) || text(args.command) || command;
134472
+ if (/(?:^|[\\/])(?:powershell|pwsh|cmd)(?:\.exe)?$/iu.test(text(args.shell))) shell = "windows";
134473
+ else if (/(?:^|[\\/])(?:bash|zsh|sh)(?:\.exe)?$/iu.test(text(args.shell))) shell = "posix";
134471
134474
  } catch {
134472
134475
  }
134473
134476
  source = source.replace(/\s+2>&1\s*$/u, "");
134477
+ const windows = shell ? shell === "windows" : process.platform === "win32" || /^\s*&\s+/u.test(source) || /^\s*(?:["']?(?:[A-Za-z]:\\|\\\\)|node\.exe\s+|node\s+["']?[A-Za-z]:\\|apilift\.(?:cmd|exe|ps1)\b)/iu.test(source);
134478
+ if (windows) source = source.replace(/^\s*&\s+/u, "");
134474
134479
  const words = [];
134475
134480
  let word = "", quote = "", escaped = false, started = false;
134476
- for (const char of source) {
134481
+ for (let offset = 0; offset < source.length; offset++) {
134482
+ const char = source[offset];
134477
134483
  if (escaped) {
134478
134484
  word += char;
134479
134485
  escaped = false;
134480
134486
  continue;
134481
134487
  }
134482
- if (char === "\\" && quote !== "'") {
134488
+ if (char === "\\" && quote !== "'" && !windows) {
134483
134489
  escaped = true;
134484
134490
  started = true;
134485
134491
  continue;
134486
134492
  }
134487
134493
  if (quote) {
134488
- if (quote === '"' && (char === "$" || char === "`")) return null;
134494
+ if (windows && quote === "'" && char === "'" && source[offset + 1] === "'") {
134495
+ word += "'";
134496
+ offset++;
134497
+ continue;
134498
+ }
134499
+ if (quote === '"' && (char === "$" || char === "`" || windows && (char === "%" || char === "!"))) return null;
134489
134500
  if (char === quote) quote = "";
134490
134501
  else word += char;
134491
134502
  continue;
@@ -134496,6 +134507,7 @@ function commandIdentity(command) {
134496
134507
  continue;
134497
134508
  }
134498
134509
  if (/[;&|`$()]/u.test(char)) return null;
134510
+ if (windows && /[%!^<>]/u.test(char)) return null;
134499
134511
  if (/\s/u.test(char)) {
134500
134512
  if (started) {
134501
134513
  words.push(word);
@@ -134512,9 +134524,9 @@ function commandIdentity(command) {
134512
134524
  let index = 0;
134513
134525
  if (/(?:^|\/)env$/u.test(words[index] ?? "")) index++;
134514
134526
  while (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(words[index] ?? "")) index++;
134515
- if (/(?:^|\/)node(?:\.exe)?$/u.test(words[index] ?? "")) index++;
134527
+ if (/(?:^|[\\/])node(?:\.exe)?$/iu.test(words[index] ?? "")) index++;
134516
134528
  if (words[index] === "./dev" && words[index + 1] === "apilift") index++;
134517
- if (!/(?:^|\/)apilift(?:\.js)?$/u.test(words[index] ?? "")) return null;
134529
+ if (!/(?:^|[\\/])apilift(?:\.(?:js|cmd|exe|ps1))?$/iu.test(words[index] ?? "")) return null;
134518
134530
  let argv = words.slice(index + 1);
134519
134531
  if (argv[0] === "--session") argv = argv.slice(2);
134520
134532
  if (!argv.length) return null;