@testsmith/api-spector 0.5.2 → 0.5.4

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.
@@ -4,6 +4,10 @@ const IPC = {
4
4
  file: {
5
5
  openWorkspace: "file:openWorkspace",
6
6
  openWorkspacePath: "file:openWorkspacePath",
7
+ openFromGit: "file:openFromGit",
8
+ /** Event (main→renderer): a `spector://open-from-git?url=…` deep link was
9
+ * received; the renderer runs the same open-from-Git flow as the button. */
10
+ openFromGitDeepLink: "file:openFromGitDeepLink",
7
11
  getLastWorkspace: "file:getLastWorkspace",
8
12
  getRecentWorkspaces: "file:getRecentWorkspaces",
9
13
  saveWorkspace: "file:saveWorkspace",
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const uuid = require("uuid");
4
- const soapHandler = require("./soap-handler-BYkE6MyE.js");
5
- require("./handle-b6bp3fb0.js");
4
+ const soapHandler = require("./soap-handler-DCCE344x.js");
5
+ require("./handle-BFaeFA0A.js");
6
6
  require("https");
7
7
  require("http");
8
8
  require("@xmldom/xmldom");
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  const undici = require("undici");
25
25
  const promises = require("fs/promises");
26
26
  const nodeTls = require("tls");
27
- const handle = require("./handle-b6bp3fb0.js");
27
+ const handle = require("./handle-BFaeFA0A.js");
28
28
  const fs = require("node:fs");
29
29
  const os = require("node:os");
30
30
  const path$1 = require("node:path");
@@ -2252,6 +2252,7 @@ exports.buildSchemaTestResults = buildSchemaTestResults;
2252
2252
  exports.buildUrl = buildUrl;
2253
2253
  exports.deriveRunStatus = deriveRunStatus;
2254
2254
  exports.executeRunnerRequest = executeRunnerRequest;
2255
+ exports.fetchOAuth2Token = fetchOAuth2Token;
2255
2256
  exports.getGlobals = getGlobals;
2256
2257
  exports.getSecret = getSecret;
2257
2258
  exports.hasSecretScheme = hasSecretScheme;
@@ -3,7 +3,7 @@ const promises = require("fs/promises");
3
3
  const path = require("path");
4
4
  const crypto = require("crypto");
5
5
  const undici = require("undici");
6
- const requestExec = require("./request-exec-D_xyyt3_.js");
6
+ const requestExec = require("./request-exec-Dg-EHFX0.js");
7
7
  const Ajv = require("ajv");
8
8
  const jsYaml = require("js-yaml");
9
9
  require("http");
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const handle = require("./handle-b6bp3fb0.js");
2
+ const handle = require("./handle-BFaeFA0A.js");
3
3
  const https = require("https");
4
4
  const http = require("http");
5
5
  const xmldom = require("@xmldom/xmldom");
@@ -315,7 +315,7 @@ function registerSoapHandlers(ipc) {
315
315
  handle.handleIpc(ipc, handle.IPC.wsdl.import, async (_event, opts) => {
316
316
  const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-k6KI8adf.js"));
317
317
  validateWsdlImport(opts);
318
- const { importWsdl } = await Promise.resolve().then(() => require("./import-BeY7gjWL.js"));
318
+ const { importWsdl } = await Promise.resolve().then(() => require("./import-BBr8q5eT.js"));
319
319
  const wsdlText = opts.xml ?? (opts.url ? await fetchUrl(opts.url) : "");
320
320
  if (!wsdlText) throw new Error("wsdl:import requires either `url` or `xml`");
321
321
  return importWsdl(wsdlText, { name: opts.name, existingMockPorts: opts.existingMockPorts });
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const snapshots = require("./chunks/snapshots-BJiZNuRN.js");
5
+ const snapshots = require("./chunks/snapshots-CN8g7lqA.js");
6
6
  const crypto = require("crypto");
7
7
  const undici = require("undici");
8
8
  const os = require("os");
@@ -10,9 +10,9 @@ const cliCommon = require("./chunks/cli-common-BKYgsbGB.js");
10
10
  const child_process = require("child_process");
11
11
  const jsYaml = require("js-yaml");
12
12
  const environments = require("./chunks/environments-iM3SUM-4.js");
13
- require("./chunks/request-exec-D_xyyt3_.js");
13
+ require("./chunks/request-exec-Dg-EHFX0.js");
14
14
  require("tls");
15
- require("./chunks/handle-b6bp3fb0.js");
15
+ require("./chunks/handle-BFaeFA0A.js");
16
16
  require("node:fs");
17
17
  require("node:os");
18
18
  require("node:path");
package/out/main/index.js CHANGED
@@ -22,17 +22,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  const electron = require("electron");
25
- const handle = require("./chunks/handle-b6bp3fb0.js");
25
+ const handle = require("./chunks/handle-BFaeFA0A.js");
26
26
  const path = require("path");
27
27
  const fs$1 = require("fs");
28
28
  const promises = require("fs/promises");
29
29
  const crypto = require("crypto");
30
- const requestExec = require("./chunks/request-exec-D_xyyt3_.js");
30
+ const JSZip = require("jszip");
31
+ const requestExec = require("./chunks/request-exec-Dg-EHFX0.js");
31
32
  const ipcValidate = require("./chunks/ipc-validate-k6KI8adf.js");
32
33
  const uuid = require("uuid");
33
34
  const jsYaml = require("js-yaml");
34
35
  const undici = require("undici");
35
- const JSZip = require("jszip");
36
36
  const requestCollection = require("./chunks/request-collection-CJoXpvOw.js");
37
37
  const mockServer = require("./chunks/mock-server-BxiXhP1m.js");
38
38
  const http = require("http");
@@ -42,10 +42,10 @@ const os = require("node:os");
42
42
  const path$1 = require("node:path");
43
43
  const grpc = require("@grpc/grpc-js");
44
44
  const protoLoader = require("@grpc/proto-loader");
45
- const soapHandler = require("./chunks/soap-handler-BYkE6MyE.js");
45
+ const soapHandler = require("./chunks/soap-handler-DCCE344x.js");
46
46
  const promises$1 = require("node:fs/promises");
47
47
  const os$1 = require("os");
48
- const snapshots = require("./chunks/snapshots-BJiZNuRN.js");
48
+ const snapshots = require("./chunks/snapshots-CN8g7lqA.js");
49
49
  const simpleGit = require("simple-git");
50
50
  const recorder = require("./chunks/recorder-0Ij921El.js");
51
51
  require("tls");
@@ -461,6 +461,40 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
461
461
  requestExec.setSecretsConfig(parsed.settings?.secrets);
462
462
  return { workspace: parsed, workspacePath: wsPath };
463
463
  });
464
+ handle.handleIpc(ipc, handle.IPC.file.openFromGit, async (_e, repoUrl) => {
465
+ const archiveUrl = await resolveArchiveUrl(repoUrl);
466
+ if (!archiveUrl) {
467
+ throw new Error("unsupported URL — use a public GitHub repo URL or a direct .zip link");
468
+ }
469
+ const pick = await electron.dialog.showOpenDialog({
470
+ title: "Choose a folder to clone the repository into",
471
+ properties: ["openDirectory", "createDirectory"],
472
+ buttonLabel: "Clone here"
473
+ });
474
+ if (pick.canceled || pick.filePaths.length === 0) return null;
475
+ const parent = pick.filePaths[0];
476
+ const repoName = (repoUrl.match(/github\.com\/[^/]+\/([^/#?]+)/i)?.[1] ?? "repository").replace(/\.git$/i, "").replace(/[^a-zA-Z0-9._-]+/g, "-") || "repository";
477
+ const destDir = path.join(parent, repoName);
478
+ try {
479
+ const existing = await promises.readdir(destDir);
480
+ if (existing.length > 0) {
481
+ throw new Error(`"${repoName}" already exists in that folder and is not empty`);
482
+ }
483
+ } catch (err) {
484
+ if (err instanceof Error && err.message.includes("already exists")) throw err;
485
+ }
486
+ await promises.mkdir(destDir, { recursive: true });
487
+ const buf = await downloadArchive(archiveUrl);
488
+ await extractZip(buf, destDir);
489
+ const wsPath = await findWorkspaceSpector(destDir);
490
+ const parsed = JSON.parse(await promises.readFile(wsPath, "utf8"));
491
+ workspaceDir = path.dirname(wsPath);
492
+ workspaceFile = wsPath;
493
+ await requestExec.loadGlobals(workspaceDir);
494
+ await saveLastWorkspacePath(wsPath);
495
+ requestExec.setSecretsConfig(parsed.settings?.secrets);
496
+ return { workspace: parsed, workspacePath: wsPath };
497
+ });
464
498
  }
465
499
  async function tryOpenWorkspaceInDir(dir) {
466
500
  let entries;
@@ -492,6 +526,103 @@ function looksLikeCollection(obj) {
492
526
  const o = obj;
493
527
  return !Array.isArray(o["collections"]) && (!!o["rootFolder"] || !!o["requests"]);
494
528
  }
529
+ const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
530
+ async function resolveArchiveUrl(input) {
531
+ let url;
532
+ try {
533
+ url = new URL(input.trim());
534
+ } catch {
535
+ return null;
536
+ }
537
+ if (url.protocol !== "https:") return null;
538
+ if (url.pathname.toLowerCase().endsWith(".zip")) return url.toString();
539
+ if (url.hostname === "github.com" || url.hostname === "www.github.com") {
540
+ const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
541
+ if (parts.length < 2) return null;
542
+ const owner = parts[0];
543
+ const repo = parts[1].replace(/\.git$/i, "");
544
+ let branch = null;
545
+ if (parts[2] === "tree" && parts[3]) branch = parts.slice(3).join("/");
546
+ if (!branch) {
547
+ try {
548
+ const resp = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
549
+ headers: { "User-Agent": "API-Spector", Accept: "application/vnd.github+json" }
550
+ });
551
+ if (!resp.ok) return null;
552
+ const meta = await resp.json();
553
+ branch = meta.default_branch ?? "main";
554
+ } catch {
555
+ return null;
556
+ }
557
+ }
558
+ return `https://codeload.github.com/${owner}/${repo}/zip/refs/heads/${branch}`;
559
+ }
560
+ return null;
561
+ }
562
+ async function downloadArchive(archiveUrl) {
563
+ const resp = await fetch(archiveUrl, { headers: { "User-Agent": "API-Spector" } });
564
+ if (!resp.ok) throw new Error(`download failed (HTTP ${resp.status})`);
565
+ const buf = Buffer.from(await resp.arrayBuffer());
566
+ if (buf.byteLength > MAX_ARCHIVE_BYTES) throw new Error("archive is too large");
567
+ return buf;
568
+ }
569
+ async function extractZip(buf, destDir) {
570
+ const zip = await JSZip.loadAsync(buf);
571
+ const paths = Object.keys(zip.files);
572
+ let root = null;
573
+ for (const p of paths) {
574
+ const top = p.split("/")[0];
575
+ if (root === null) root = top;
576
+ else if (root !== top) {
577
+ root = null;
578
+ break;
579
+ }
580
+ }
581
+ const prefix = root ? `${root}/` : "";
582
+ for (const p of paths) {
583
+ const entry = zip.files[p];
584
+ const rel = prefix && p.startsWith(prefix) ? p.slice(prefix.length) : p;
585
+ if (!rel) continue;
586
+ const outPath = path.resolve(destDir, rel);
587
+ if (outPath !== destDir && !outPath.startsWith(destDir + path.sep)) {
588
+ throw new Error("archive contains an unsafe path");
589
+ }
590
+ if (entry.dir) {
591
+ await promises.mkdir(outPath, { recursive: true });
592
+ } else {
593
+ await promises.mkdir(path.dirname(outPath), { recursive: true });
594
+ await promises.writeFile(outPath, Buffer.from(await entry.async("nodebuffer")));
595
+ }
596
+ }
597
+ }
598
+ async function findWorkspaceSpector(dir) {
599
+ const found = [];
600
+ async function walk(d) {
601
+ let entries;
602
+ try {
603
+ entries = await promises.readdir(d, { withFileTypes: true });
604
+ } catch {
605
+ return;
606
+ }
607
+ for (const e of entries) {
608
+ const full = path.join(d, e.name);
609
+ if (e.isDirectory()) {
610
+ if (e.name === "node_modules" || e.name === ".git") continue;
611
+ await walk(full);
612
+ } else if (e.isFile() && e.name.endsWith(".spector")) {
613
+ try {
614
+ const parsed = JSON.parse(await promises.readFile(full, "utf8"));
615
+ if (!looksLikeCollection(parsed)) found.push(full);
616
+ } catch {
617
+ }
618
+ }
619
+ }
620
+ }
621
+ await walk(dir);
622
+ if (found.length === 0) throw new Error("no workspace (.spector) file found in the repository");
623
+ if (found.length > 1) throw new Error("multiple workspace files found — open the folder and pick one");
624
+ return found[0];
625
+ }
495
626
  async function wrapCollectionAsWorkspace(collFilePath, collection) {
496
627
  const dir = path.dirname(collFilePath);
497
628
  const base = path.basename(collFilePath).replace(/\.(spector|json)$/i, "");
@@ -4550,90 +4681,108 @@ function registerMockHandlers(ipc) {
4550
4681
  return JSON.parse(raw);
4551
4682
  });
4552
4683
  }
4553
- function registerOAuth2Handlers(ipc) {
4554
- handle.handleIpc(ipc, handle.IPC.oauth2.startFlow, async (_e, auth, vars) => {
4555
- const port = auth.oauth2RedirectPort ?? 9876;
4556
- const redirectUri = `http://localhost:${port}/callback`;
4557
- const authUrl = requestExec.interpolate(auth.oauth2AuthUrl ?? "", vars);
4558
- const tokenUrl = requestExec.interpolate(auth.oauth2TokenUrl ?? "", vars);
4559
- const clientId = requestExec.interpolate(auth.oauth2ClientId ?? "", vars);
4560
- let clientSecret = auth.oauth2ClientSecret ?? "";
4561
- if (!clientSecret && auth.oauth2ClientSecretRef) {
4562
- clientSecret = await requestExec.getSecret(auth.oauth2ClientSecretRef) ?? "";
4563
- }
4564
- clientSecret = requestExec.interpolate(clientSecret, vars);
4565
- if (!authUrl) throw new Error("OAuth 2.0: authUrl is required for authorization_code flow.");
4566
- if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for authorization_code flow.");
4567
- if (!clientId) throw new Error("OAuth 2.0: clientId is required.");
4568
- const state = Math.random().toString(36).slice(2);
4569
- const authUrlFull = new URL(authUrl);
4570
- authUrlFull.searchParams.set("response_type", "code");
4571
- authUrlFull.searchParams.set("client_id", clientId);
4572
- authUrlFull.searchParams.set("redirect_uri", redirectUri);
4573
- authUrlFull.searchParams.set("state", state);
4574
- if (auth.oauth2Scopes) authUrlFull.searchParams.set("scope", auth.oauth2Scopes);
4575
- const code = await new Promise((resolve2, reject) => {
4576
- const server = http.createServer((req, res) => {
4577
- const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
4578
- if (!reqUrl.pathname.startsWith("/callback")) {
4579
- res.writeHead(404);
4580
- res.end();
4581
- return;
4582
- }
4583
- const returnedState = reqUrl.searchParams.get("state");
4584
- const returnedCode = reqUrl.searchParams.get("code");
4585
- const error = reqUrl.searchParams.get("error");
4684
+ async function resolveClientSecret(auth, vars) {
4685
+ let secret = auth.oauth2ClientSecret ?? "";
4686
+ if (!secret && auth.oauth2ClientSecretRef) {
4687
+ secret = await requestExec.getSecret(auth.oauth2ClientSecretRef) ?? "";
4688
+ }
4689
+ return requestExec.interpolate(secret, vars);
4690
+ }
4691
+ async function browserFlow(auth, vars, flow) {
4692
+ const port = auth.oauth2RedirectPort ?? 9876;
4693
+ const redirectUri = `http://localhost:${port}/callback`;
4694
+ const authUrl = requestExec.interpolate(auth.oauth2AuthUrl ?? "", vars);
4695
+ const clientId = requestExec.interpolate(auth.oauth2ClientId ?? "", vars);
4696
+ if (!authUrl) throw new Error(`OAuth 2.0: authUrl is required for the ${flow} flow.`);
4697
+ if (!clientId) throw new Error("OAuth 2.0: clientId is required.");
4698
+ const state = Math.random().toString(36).slice(2);
4699
+ const authUrlFull = new URL(authUrl);
4700
+ authUrlFull.searchParams.set("response_type", flow === "implicit" ? "token" : "code");
4701
+ authUrlFull.searchParams.set("client_id", clientId);
4702
+ authUrlFull.searchParams.set("redirect_uri", redirectUri);
4703
+ authUrlFull.searchParams.set("state", state);
4704
+ if (auth.oauth2Scopes) authUrlFull.searchParams.set("scope", auth.oauth2Scopes);
4705
+ const captured = await new Promise((resolve2, reject) => {
4706
+ const server = http.createServer((req, res) => {
4707
+ const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
4708
+ if (flow === "implicit" && reqUrl.pathname === "/callback" && !reqUrl.searchParams.get("access_token") && !reqUrl.searchParams.get("error")) {
4586
4709
  res.writeHead(200, { "Content-Type": "text/html" });
4587
- res.end("<html><body><p>Authorization complete. You may close this tab.</p></body></html>");
4588
- server.close();
4589
- if (error) {
4590
- reject(new Error(`OAuth 2.0 authorization error: ${error}`));
4591
- return;
4592
- }
4593
- if (returnedState !== state) {
4594
- reject(new Error("OAuth 2.0: state mismatch."));
4595
- return;
4596
- }
4597
- if (!returnedCode) {
4598
- reject(new Error("OAuth 2.0: no code in callback."));
4599
- return;
4600
- }
4601
- resolve2(returnedCode);
4602
- });
4603
- server.on("error", reject);
4604
- server.listen(port, "127.0.0.1", () => {
4605
- electron.shell.openExternal(authUrlFull.toString()).catch(reject);
4606
- });
4607
- setTimeout(() => {
4608
- server.close();
4609
- reject(new Error("OAuth 2.0 authorization timed out (5 min)."));
4610
- }, 5 * 60 * 1e3);
4710
+ res.end('<!doctype html><html><body><p>Completing sign-in… you may close this tab.</p><script>location.replace("/callback/capture?" + (location.hash ? location.hash.substring(1) : ""));<\/script></body></html>');
4711
+ return;
4712
+ }
4713
+ if (reqUrl.pathname !== "/callback" && reqUrl.pathname !== "/callback/capture") {
4714
+ res.writeHead(404);
4715
+ res.end();
4716
+ return;
4717
+ }
4718
+ const params2 = Object.fromEntries(reqUrl.searchParams.entries());
4719
+ res.writeHead(200, { "Content-Type": "text/html" });
4720
+ res.end("<!doctype html><html><body><p>Authorization complete. You may close this tab.</p></body></html>");
4721
+ server.close();
4722
+ if (params2["error"]) {
4723
+ reject(new Error(`OAuth 2.0 authorization error: ${params2["error_description"] ?? params2["error"]}`));
4724
+ return;
4725
+ }
4726
+ if (params2["state"] !== state) {
4727
+ reject(new Error("OAuth 2.0: state mismatch."));
4728
+ return;
4729
+ }
4730
+ resolve2(params2);
4611
4731
  });
4612
- const { fetch: nodeFetch } = await import("undici");
4613
- const params = new URLSearchParams();
4614
- params.set("grant_type", "authorization_code");
4615
- params.set("code", code);
4616
- params.set("redirect_uri", redirectUri);
4617
- params.set("client_id", clientId);
4618
- if (clientSecret) params.set("client_secret", clientSecret);
4619
- const resp = await nodeFetch(tokenUrl, {
4620
- method: "POST",
4621
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
4622
- body: params.toString()
4732
+ server.on("error", reject);
4733
+ server.listen(port, "127.0.0.1", () => {
4734
+ electron.shell.openExternal(authUrlFull.toString()).catch(reject);
4623
4735
  });
4624
- if (!resp.ok) {
4625
- const body = await resp.text();
4626
- throw new Error(`OAuth 2.0 token exchange failed (${resp.status}): ${body}`);
4736
+ setTimeout(() => {
4737
+ server.close();
4738
+ reject(new Error("OAuth 2.0 authorization timed out (5 min)."));
4739
+ }, 5 * 60 * 1e3);
4740
+ });
4741
+ if (flow === "implicit") {
4742
+ const accessToken2 = captured["access_token"] ?? "";
4743
+ if (!accessToken2) throw new Error("OAuth 2.0: no access_token in implicit callback.");
4744
+ const expiresIn2 = Number(captured["expires_in"] ?? 3600);
4745
+ return { accessToken: accessToken2, expiresAt: Date.now() + expiresIn2 * 1e3 };
4746
+ }
4747
+ const code = captured["code"] ?? "";
4748
+ if (!code) throw new Error("OAuth 2.0: no code in callback.");
4749
+ const tokenUrl = requestExec.interpolate(auth.oauth2TokenUrl ?? "", vars);
4750
+ if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for the authorization_code flow.");
4751
+ const clientSecret = await resolveClientSecret(auth, vars);
4752
+ const { fetch: nodeFetch } = await import("undici");
4753
+ const params = new URLSearchParams();
4754
+ params.set("grant_type", "authorization_code");
4755
+ params.set("code", code);
4756
+ params.set("redirect_uri", redirectUri);
4757
+ params.set("client_id", clientId);
4758
+ if (clientSecret) params.set("client_secret", clientSecret);
4759
+ const resp = await nodeFetch(tokenUrl, {
4760
+ method: "POST",
4761
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
4762
+ body: params.toString()
4763
+ });
4764
+ if (!resp.ok) {
4765
+ const body = await resp.text();
4766
+ throw new Error(`OAuth 2.0 token exchange failed (${resp.status}): ${body}`);
4767
+ }
4768
+ const json = await resp.json();
4769
+ const accessToken = String(json["access_token"] ?? "");
4770
+ if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
4771
+ const expiresIn = Number(json["expires_in"] ?? 3600);
4772
+ return {
4773
+ accessToken,
4774
+ expiresAt: Date.now() + expiresIn * 1e3,
4775
+ refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
4776
+ };
4777
+ }
4778
+ function registerOAuth2Handlers(ipc) {
4779
+ handle.handleIpc(ipc, handle.IPC.oauth2.startFlow, async (_e, auth, vars) => {
4780
+ if (auth.type !== "oauth2") throw new Error("OAuth 2.0: auth is not an oauth2 config.");
4781
+ const flow = auth.oauth2Flow ?? "client_credentials";
4782
+ if (flow === "authorization_code" || flow === "implicit") {
4783
+ return browserFlow(auth, vars, flow);
4627
4784
  }
4628
- const json = await resp.json();
4629
- const accessToken = String(json["access_token"] ?? "");
4630
- if (!accessToken) throw new Error("OAuth 2.0: token response missing access_token.");
4631
- const expiresIn = Number(json["expires_in"] ?? 3600);
4632
- return {
4633
- accessToken,
4634
- expiresAt: Date.now() + expiresIn * 1e3,
4635
- refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
4636
- };
4785
+ return requestExec.fetchOAuth2Token(auth, vars);
4637
4786
  });
4638
4787
  handle.handleIpc(ipc, handle.IPC.oauth2.refreshToken, async (_e, auth, vars, refreshToken) => {
4639
4788
  const tokenUrl = requestExec.interpolate(auth.oauth2TokenUrl ?? "", vars);
@@ -5792,7 +5941,7 @@ function isNewer(a, b) {
5792
5941
  return a2 > b2;
5793
5942
  }
5794
5943
  async function checkForUpdate() {
5795
- const current = "0.5.2";
5944
+ const current = "0.5.4";
5796
5945
  try {
5797
5946
  const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(4e3) });
5798
5947
  if (!res.ok) return null;
@@ -5813,6 +5962,47 @@ if (process.env.ELECTRON_NO_SANDBOX === "1") {
5813
5962
  electron.app.commandLine.appendSwitch("no-sandbox");
5814
5963
  electron.app.commandLine.appendSwitch("disable-features", "RendererCodeIntegrity");
5815
5964
  }
5965
+ const PROTOCOL = "spector";
5966
+ let pendingRepoUrl = null;
5967
+ function deepLinkFromArgv(argv) {
5968
+ return argv.find((a) => a.startsWith(`${PROTOCOL}://`)) ?? null;
5969
+ }
5970
+ function parseOpenFromGit(deepLink) {
5971
+ let u;
5972
+ try {
5973
+ u = new URL(deepLink);
5974
+ } catch {
5975
+ return null;
5976
+ }
5977
+ if (u.protocol !== `${PROTOCOL}:`) return null;
5978
+ const action = u.hostname || u.pathname.replace(/^\/+/, "").split("/")[0];
5979
+ if (action !== "open-from-git") return null;
5980
+ const repo = u.searchParams.get("url");
5981
+ return repo && repo.trim() ? repo.trim() : null;
5982
+ }
5983
+ function handleDeepLink(deepLink) {
5984
+ const repo = parseOpenFromGit(deepLink);
5985
+ if (!repo) return;
5986
+ const win = electron.BrowserWindow.getAllWindows()[0];
5987
+ if (!win) {
5988
+ pendingRepoUrl = repo;
5989
+ if (electron.app.isReady()) createWindow();
5990
+ return;
5991
+ }
5992
+ if (win.isMinimized()) win.restore();
5993
+ win.focus();
5994
+ if (win.webContents.isLoading()) {
5995
+ pendingRepoUrl = repo;
5996
+ } else {
5997
+ win.webContents.send(handle.IPC.file.openFromGitDeepLink, repo);
5998
+ }
5999
+ }
6000
+ function flushDeepLink(win) {
6001
+ if (!pendingRepoUrl) return;
6002
+ const repo = pendingRepoUrl;
6003
+ pendingRepoUrl = null;
6004
+ win.webContents.send(handle.IPC.file.openFromGitDeepLink, repo);
6005
+ }
5816
6006
  function createSplashWindow() {
5817
6007
  const splash = new electron.BrowserWindow({
5818
6008
  width: 420,
@@ -5885,6 +6075,7 @@ function createWindow() {
5885
6075
  setTimeout(() => {
5886
6076
  splash.close();
5887
6077
  win.show();
6078
+ flushDeepLink(win);
5888
6079
  }, remaining);
5889
6080
  });
5890
6081
  if (process.platform === "win32") {
@@ -5898,6 +6089,31 @@ function createWindow() {
5898
6089
  if (devToolsShortcut) win.webContents.toggleDevTools();
5899
6090
  });
5900
6091
  }
6092
+ if (process.defaultApp && process.argv.length >= 2) {
6093
+ electron.app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [path.resolve(process.argv[1])]);
6094
+ } else {
6095
+ electron.app.setAsDefaultProtocolClient(PROTOCOL);
6096
+ }
6097
+ const gotLock = electron.app.requestSingleInstanceLock();
6098
+ if (!gotLock) {
6099
+ electron.app.quit();
6100
+ } else {
6101
+ electron.app.on("second-instance", (_e, argv) => {
6102
+ const url = deepLinkFromArgv(argv);
6103
+ if (url) handleDeepLink(url);
6104
+ else {
6105
+ const win = electron.BrowserWindow.getAllWindows()[0];
6106
+ if (win) {
6107
+ if (win.isMinimized()) win.restore();
6108
+ win.focus();
6109
+ }
6110
+ }
6111
+ });
6112
+ }
6113
+ electron.app.on("open-url", (event, url) => {
6114
+ event.preventDefault();
6115
+ handleDeepLink(url);
6116
+ });
5901
6117
  electron.app.whenReady().then(async () => {
5902
6118
  if (process.platform !== "darwin") electron.Menu.setApplicationMenu(null);
5903
6119
  await requestExec.initSecretStore(electron.app.getPath("userData"));
@@ -5922,6 +6138,10 @@ electron.app.whenReady().then(async () => {
5922
6138
  handle.handleIpc(electron.ipcMain, handle.IPC.shell.openExternal, (_e, url) => electron.shell.openExternal(url));
5923
6139
  handle.handleIpc(electron.ipcMain, handle.IPC.app.checkUpdate, () => checkForUpdate());
5924
6140
  createWindow();
6141
+ if (process.platform !== "darwin") {
6142
+ const url = deepLinkFromArgv(process.argv);
6143
+ if (url) handleDeepLink(url);
6144
+ }
5925
6145
  electron.app.on("activate", () => {
5926
6146
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
5927
6147
  });
package/out/main/lib.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const mockServer = require("./chunks/mock-server-BxiXhP1m.js");
4
- const requestExec = require("./chunks/request-exec-D_xyyt3_.js");
4
+ const requestExec = require("./chunks/request-exec-Dg-EHFX0.js");
5
5
  require("http");
6
6
  require("crypto");
7
7
  require("vm");
@@ -10,7 +10,7 @@ require("@xmldom/xmldom");
10
10
  require("undici");
11
11
  require("fs/promises");
12
12
  require("tls");
13
- require("./chunks/handle-b6bp3fb0.js");
13
+ require("./chunks/handle-BFaeFA0A.js");
14
14
  require("node:fs");
15
15
  require("node:os");
16
16
  require("node:path");
@@ -2,13 +2,13 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const requestExec = require("./chunks/request-exec-D_xyyt3_.js");
5
+ const requestExec = require("./chunks/request-exec-Dg-EHFX0.js");
6
6
  const requestCollection = require("./chunks/request-collection-CJoXpvOw.js");
7
7
  const environments = require("./chunks/environments-iM3SUM-4.js");
8
8
  const cliCommon = require("./chunks/cli-common-BKYgsbGB.js");
9
9
  require("undici");
10
10
  require("tls");
11
- require("./chunks/handle-b6bp3fb0.js");
11
+ require("./chunks/handle-BFaeFA0A.js");
12
12
  require("node:fs");
13
13
  require("node:os");
14
14
  require("node:path");
@@ -402,7 +402,7 @@ async function main() {
402
402
  } else if (!envName && workspace.settings?.defaultEnvironment && !env) {
403
403
  console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
404
404
  }
405
- const version = `v${"0.5.2"}`;
405
+ const version = `v${"0.5.4"}`;
406
406
  console.log("");
407
407
  console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
408
408
  console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
package/out/main/wsdl.js CHANGED
@@ -4,10 +4,10 @@ const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const https = require("https");
6
6
  const http = require("http");
7
- const soapHandler = require("./chunks/soap-handler-BYkE6MyE.js");
8
- const _import = require("./chunks/import-BeY7gjWL.js");
7
+ const soapHandler = require("./chunks/soap-handler-DCCE344x.js");
8
+ const _import = require("./chunks/import-BBr8q5eT.js");
9
9
  const cliCommon = require("./chunks/cli-common-BKYgsbGB.js");
10
- require("./chunks/handle-b6bp3fb0.js");
10
+ require("./chunks/handle-BFaeFA0A.js");
11
11
  require("@xmldom/xmldom");
12
12
  require("uuid");
13
13
  require("crypto");
@@ -5,6 +5,10 @@ const IPC = {
5
5
  file: {
6
6
  openWorkspace: "file:openWorkspace",
7
7
  openWorkspacePath: "file:openWorkspacePath",
8
+ openFromGit: "file:openFromGit",
9
+ /** Event (main→renderer): a `spector://open-from-git?url=…` deep link was
10
+ * received; the renderer runs the same open-from-Git flow as the button. */
11
+ openFromGitDeepLink: "file:openFromGitDeepLink",
8
12
  getLastWorkspace: "file:getLastWorkspace",
9
13
  getRecentWorkspaces: "file:getRecentWorkspaces",
10
14
  saveWorkspace: "file:saveWorkspace",
@@ -215,6 +219,13 @@ const api = {
215
219
  getLastWorkspace: () => electron.ipcRenderer.invoke(IPC.file.getLastWorkspace),
216
220
  getRecentWorkspaces: () => electron.ipcRenderer.invoke(IPC.file.getRecentWorkspaces),
217
221
  openWorkspacePath: (path) => electron.ipcRenderer.invoke(IPC.file.openWorkspacePath, path),
222
+ openFromGit: (repoUrl) => electron.ipcRenderer.invoke(IPC.file.openFromGit, repoUrl),
223
+ onOpenFromGit: (cb) => {
224
+ electron.ipcRenderer.on(IPC.file.openFromGitDeepLink, (_e, repoUrl) => cb(repoUrl));
225
+ },
226
+ offOpenFromGit: () => {
227
+ electron.ipcRenderer.removeAllListeners(IPC.file.openFromGitDeepLink);
228
+ },
218
229
  saveWorkspace: (ws) => electron.ipcRenderer.invoke(IPC.file.saveWorkspace, ws),
219
230
  newWorkspace: () => electron.ipcRenderer.invoke(IPC.file.newWorkspace),
220
231
  closeWorkspace: () => electron.ipcRenderer.invoke(IPC.file.closeWorkspace),