diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs
new file mode 100644
index 0000000..83de71a
--- /dev/null
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs
@@ -0,0 +1,368 @@
+// AssemblyLoader.cs
+// SPDX-License-Identifier: GPL-3.0-or-later
+// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
+// https://github.com/gnh1201/welsonjs
+//
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+namespace WelsonJS.Launcher
+{
+ ///
+ /// Network-aware loader for managed (.NET) and native (C/C++) binaries.
+ /// - Managed assemblies resolve via AssemblyResolve
+ /// - Native modules explicitly loaded via LoadNativeModules(...)
+ /// - All DLLs must have valid Authenticode signatures
+ /// - Cached at: %APPDATA%\WelsonJS\assembly\{Name}\{Version}\
+ /// - BaseUrl must be set by Main() before calling Register()
+ ///
+ public static class AssemblyLoader
+ {
+ ///
+ /// Base URL for downloading managed/native binaries.
+ /// Example: https://catswords.blob.core.windows.net/welsonjs/packages
+ /// Must be set before Register() or LoadNativeModules().
+ ///
+ public static string BaseUrl { get; set; } = null;
+
+ private static readonly object SyncRoot = new object();
+ private static bool _registered;
+
+ private static readonly string LoaderNamespace = typeof(AssemblyLoader).Namespace ?? "WelsonJS.Launcher";
+ private static readonly HttpClient Http = new HttpClient();
+ private static readonly ICompatibleLogger Logger = new TraceLogger();
+
+ // -------------------- kernel32 native loading --------------------
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern IntPtr LoadLibrary(string lpFileName);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool SetDllDirectory(string lpPathName);
+
+ // -------------------- WinVerifyTrust (signature verification) --------------------
+
+ private const uint ERROR_SUCCESS = 0x00000000;
+ private const uint TRUST_E_NOSIGNATURE = 0x800B0100;
+ private const uint TRUST_E_EXPLICIT_DISTRUST = 0x800B0111;
+ private const uint TRUST_E_SUBJECT_NOT_TRUSTED = 0x800B0004;
+ private const uint CRYPT_E_SECURITY_SETTINGS = 0x80092026;
+
+ private static readonly Guid WINTRUST_ACTION =
+ new Guid("00aac56b-cd44-11d0-8cc2-00c04fc295ee");
+
+ [DllImport("wintrust.dll", CharSet = CharSet.Unicode)]
+ private static extern uint WinVerifyTrust(
+ IntPtr hwnd,
+ [MarshalAs(UnmanagedType.LPStruct)] Guid pgActionID,
+ ref WINTRUST_DATA pWVTData);
+
+
+ private enum FileSignatureStatus { Valid, NoSignature, Invalid }
+
+
+ // -------------------- WinTrust Structures --------------------
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct WINTRUST_FILE_INFO
+ {
+ public uint cbStruct;
+ [MarshalAs(UnmanagedType.LPWStr)] public string pcwszFilePath;
+ public IntPtr hFile;
+ public IntPtr pgKnownSubject;
+
+ public WINTRUST_FILE_INFO(string filePath)
+ {
+ cbStruct = (uint)Marshal.SizeOf(typeof(WINTRUST_FILE_INFO));
+ pcwszFilePath = filePath;
+ hFile = IntPtr.Zero;
+ pgKnownSubject = IntPtr.Zero;
+ }
+ }
+
+ private enum WinTrustDataUIChoice : uint { None = 2 }
+ private enum WinTrustDataRevocationChecks : uint { None = 0 }
+ private enum WinTrustDataChoice : uint { File = 1 }
+ private enum WinTrustDataStateAction : uint { Ignore = 0 }
+ private enum WinTrustDataUIContext : uint { Execute = 0 }
+ [Flags]
+ private enum WinTrustDataProvFlags : uint
+ {
+ RevocationCheckNone = 0x00000010,
+ DisableMD2andMD4 = 0x00002000
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct WINTRUST_DATA
+ {
+ public uint cbStruct;
+ public IntPtr pPolicyCallbackData;
+ public IntPtr pSIPClientData;
+ public WinTrustDataUIChoice dwUIChoice;
+ public WinTrustDataRevocationChecks dwRevocationChecks;
+ public WinTrustDataChoice dwUnionChoice;
+ public IntPtr pFile;
+ public WinTrustDataStateAction dwStateAction;
+ public IntPtr hWVTStateData;
+ public string pwszURLReference;
+ public WinTrustDataProvFlags dwProvFlags;
+ public WinTrustDataUIContext dwUIContext;
+
+ public WINTRUST_DATA(IntPtr pFileInfo)
+ {
+ cbStruct = (uint)Marshal.SizeOf(typeof(WINTRUST_DATA));
+ pPolicyCallbackData = IntPtr.Zero;
+ pSIPClientData = IntPtr.Zero;
+ dwUIChoice = WinTrustDataUIChoice.None;
+ dwRevocationChecks = WinTrustDataRevocationChecks.None;
+ dwUnionChoice = WinTrustDataChoice.File;
+ pFile = pFileInfo;
+ dwStateAction = WinTrustDataStateAction.Ignore;
+ hWVTStateData = IntPtr.Zero;
+ pwszURLReference = null;
+ dwProvFlags = WinTrustDataProvFlags.RevocationCheckNone |
+ WinTrustDataProvFlags.DisableMD2andMD4;
+ dwUIContext = WinTrustDataUIContext.Execute;
+ }
+ }
+
+
+ // ========================================================================
+ // PUBLIC API
+ // ========================================================================
+
+ ///
+ /// Registers AssemblyResolve to download and validate .NET assemblies.
+ ///
+ public static void Register()
+ {
+ if (_registered)
+ return;
+
+ if (string.IsNullOrWhiteSpace(BaseUrl))
+ {
+ Logger.Error("AssemblyLoader.Register() called but BaseUrl is not set.");
+ throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register().");
+ }
+
+ AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
+ _registered = true;
+
+ Logger.Info("AssemblyLoader: AssemblyResolve handler registered.");
+ }
+
+
+ ///
+ /// Loads native modules associated with an assembly (explicit).
+ ///
+ public static void LoadNativeModules(string ownerAssemblyName, Version version, IList fileNames)
+ {
+ if (string.IsNullOrWhiteSpace(BaseUrl))
+ throw new InvalidOperationException("AssemblyLoader.BaseUrl must be set before loading native modules.");
+
+ if (ownerAssemblyName == null) throw new ArgumentNullException("ownerAssemblyName");
+ if (version == null) throw new ArgumentNullException("version");
+ if (fileNames == null) throw new ArgumentNullException("fileNames");
+
+ string versionString = version.ToString();
+
+ lock (SyncRoot)
+ {
+ string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", ownerAssemblyName, versionString);
+ Directory.CreateDirectory(cacheDir);
+
+ try { SetDllDirectory(cacheDir); }
+ catch { }
+
+ foreach (string raw in fileNames)
+ {
+ if (string.IsNullOrWhiteSpace(raw))
+ continue;
+
+ string fileName = raw.Trim();
+ string localPath = Path.Combine(cacheDir, fileName);
+
+ if (!File.Exists(localPath))
+ {
+ string url = $"{BaseUrl.TrimEnd('/')}/native/{ownerAssemblyName}/{versionString}/{fileName}";
+ DownloadFile(url, localPath);
+ Logger.Info("Downloaded native module: {0}", fileName);
+ }
+ else
+ {
+ Logger.Info("Using cached native module: {0}", localPath);
+ }
+
+ EnsureSignedFileOrThrow(localPath, fileName);
+
+ IntPtr h = LoadLibrary(localPath);
+ if (h == IntPtr.Zero)
+ {
+ Logger.Error("LoadLibrary failed for {0}", localPath);
+ }
+ else
+ {
+ Logger.Info("Loaded native module: {0}", fileName);
+ }
+ }
+ }
+ }
+
+
+ public static void LoadNativeModules(Assembly asm, IList fileNames)
+ {
+ AssemblyName an = asm.GetName();
+ LoadNativeModules(an.Name, an.Version, fileNames);
+ }
+
+
+ // ========================================================================
+ // ASSEMBLY RESOLVE HANDLER (MANAGED)
+ // ========================================================================
+
+ private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
+ {
+ Logger.Info("AssemblyResolve: {0}", args.Name);
+
+ AssemblyName req = new AssemblyName(args.Name);
+ string simpleName = req.Name;
+ if (IsFrameworkAssembly(simpleName))
+ return null;
+
+ var entry = Assembly.GetEntryAssembly();
+ if (entry != null)
+ {
+ var entryName = entry.GetName().Name;
+ if (string.Equals(simpleName, entryName, StringComparison.OrdinalIgnoreCase))
+ {
+ Logger.Info("AssemblyResolve: skipping entry assembly {0}", simpleName);
+ return null;
+ }
+ }
+
+ Version version = req.Version ?? new Version(0, 0, 0, 0);
+ string versionStr = version.ToString();
+
+ lock (SyncRoot)
+ {
+ string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", simpleName, versionStr);
+ string dllPath = Path.Combine(cacheDir, simpleName + ".dll");
+
+ Directory.CreateDirectory(cacheDir);
+
+ if (!File.Exists(dllPath))
+ {
+ string url = $"{BaseUrl.TrimEnd('/')}/managed/{simpleName}/{versionStr}/{simpleName}.dll";
+ DownloadFile(url, dllPath);
+ Logger.Info("Downloaded managed assembly: {0}", simpleName);
+ }
+ else
+ {
+ Logger.Info("Using cached managed assembly: {0}", dllPath);
+ }
+
+ if (!File.Exists(dllPath))
+ {
+ Logger.Warn("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName);
+ return null;
+ }
+
+ EnsureSignedFileOrThrow(dllPath, simpleName);
+ return Assembly.LoadFrom(dllPath);
+ }
+ }
+
+
+ // ========================================================================
+ // HELPERS
+ // ========================================================================
+
+ private static void DownloadFile(string url, string dest)
+ {
+ HttpResponseMessage res = null;
+
+ try
+ {
+ res = Http.GetAsync(url).GetAwaiter().GetResult();
+ if (res.StatusCode == HttpStatusCode.NotFound)
+ {
+ Logger.Warn("DownloadFile: 404 Not Found for {0}", url);
+ return;
+ }
+
+ res.EnsureSuccessStatusCode();
+
+ using (Stream s = res.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
+ using (FileStream fs = new FileStream(dest, FileMode.Create, FileAccess.Write))
+ {
+ s.CopyTo(fs);
+ }
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.Error("DownloadFile: HTTP error for {0}: {1}", url, ex.Message);
+ throw;
+ }
+ }
+
+
+ private static bool IsFrameworkAssembly(string name)
+ {
+ return name.StartsWith("System", StringComparison.OrdinalIgnoreCase) ||
+ name.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) ||
+ name == "mscorlib" ||
+ name == "netstandard" ||
+ name == "WindowsBase" ||
+ name == "PresentationCore" ||
+ name == "PresentationFramework" ||
+ name.StartsWith(LoaderNamespace);
+ }
+
+
+ private static void EnsureSignedFileOrThrow(string path, string logicalName)
+ {
+ FileSignatureStatus status = VerifySignature(path);
+
+ if (status == FileSignatureStatus.Valid)
+ {
+ Logger.Info("Signature OK: {0}", logicalName);
+ return;
+ }
+
+ if (status == FileSignatureStatus.NoSignature)
+ {
+ Logger.Error("BLOCKED unsigned binary: {0}", logicalName);
+ throw new InvalidOperationException("Unsigned binary blocked: " + logicalName);
+ }
+
+ Logger.Error("BLOCKED invalid signature: {0}", logicalName);
+ throw new InvalidOperationException("Invalid signature: " + logicalName);
+ }
+
+
+ private static FileSignatureStatus VerifySignature(string file)
+ {
+ WINTRUST_FILE_INFO fileInfo = new WINTRUST_FILE_INFO(file);
+ IntPtr pFile = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(WINTRUST_FILE_INFO)));
+
+ Marshal.StructureToPtr(fileInfo, pFile, false);
+ WINTRUST_DATA data = new WINTRUST_DATA(pFile);
+
+ uint result = WinVerifyTrust(IntPtr.Zero, WINTRUST_ACTION, ref data);
+
+ Marshal.FreeCoTaskMem(pFile);
+
+ if (result == ERROR_SUCCESS) return FileSignatureStatus.Valid;
+ if (result == TRUST_E_NOSIGNATURE) return FileSignatureStatus.NoSignature;
+
+ return FileSignatureStatus.Invalid;
+ }
+ }
+}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs
deleted file mode 100644
index 609169b..0000000
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs
+++ /dev/null
@@ -1,244 +0,0 @@
-// NativeBootstrap.cs
-// SPDX-License-Identifier: GPL-3.0-or-later
-// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
-// https://github.com/gnh1201/welsonjs
-//
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Security.Cryptography.X509Certificates;
-
-namespace WelsonJS.Launcher
-{
- public static class NativeBootstrap
- {
- // Win32 APIs
- [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
- private static extern IntPtr LoadLibrary(string lpFileName);
-
- [DllImport("kernel32", SetLastError = true)]
- private static extern bool SetDefaultDllDirectories(int DirectoryFlags);
-
- [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "AddDllDirectory")]
- private static extern IntPtr AddDllDirectory(string newDirectory);
-
- [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
- private static extern bool SetDllDirectory(string lpPathName);
-
- private const int LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000;
-
- ///
- /// Tries to load native libraries in the following order:
- /// 1) %APPDATA%\{appDataSubdirectory}\{dllName}
- /// 2) Application base directory\{dllName}
- ///
- /// Signatures:
- /// - requireSigned = true : Only loads DLLs with a valid Authenticode chain.
- /// - certValidator != null : Additional custom validation (e.g., pinning).
- ///
- /// Must be called before any P/Invoke usage.
- ///
- public static void Init(
- IEnumerable dllNames,
- string appDataSubdirectory,
- ICompatibleLogger logger,
- bool requireSigned = false,
- Func certValidator = null)
- {
- if (dllNames == null) throw new ArgumentNullException(nameof(dllNames));
- if (logger == null) throw new ArgumentNullException(nameof(logger));
-
- string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
- string appDataPath = string.IsNullOrEmpty(appDataSubdirectory)
- ? appData
- : Path.Combine(appData, appDataSubdirectory);
-
- string asmLocation = Assembly.GetEntryAssembly()?.Location
- ?? Assembly.GetExecutingAssembly().Location
- ?? AppContext.BaseDirectory;
- string appBaseDirectory = Path.GetDirectoryName(asmLocation) ?? AppContext.BaseDirectory;
-
- var triedPaths = new List();
-
- foreach (string dllName in dllNames)
- {
- if (string.IsNullOrWhiteSpace(dllName))
- continue;
-
- // 1) %APPDATA% subdirectory
- string candidate1 = Path.Combine(appDataPath, dllName);
- triedPaths.Add(candidate1);
- if (TryLoad(candidate1, logger, requireSigned, certValidator)) return;
-
- // 2) Application base directory
- string candidate2 = Path.Combine(appBaseDirectory, dllName);
- triedPaths.Add(candidate2);
- if (TryLoad(candidate2, logger, requireSigned, certValidator)) return;
- }
-
- string message = "Failed to load requested native libraries.\n" +
- "Tried:\n " + string.Join("\n ", triedPaths);
- logger.Error(message);
- throw new FileNotFoundException(message);
- }
-
- private static bool TryLoad(
- string fullPath,
- ICompatibleLogger logger,
- bool requireSigned,
- Func certValidator)
- {
- try
- {
- if (!File.Exists(fullPath))
- {
- logger.Info($"Not found: {fullPath}");
- return false;
- }
-
- // Optional signature validation
- if (!ValidateSignatureIfRequired(fullPath, requireSigned, certValidator, logger))
- {
- // If requireSigned=false we never reach here (it would return true).
- logger.Warn($"Signature validation failed: {fullPath}");
- return false;
- }
-
- string directoryPath = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory;
- if (!TryRegisterSearchDirectory(directoryPath, logger))
- {
- logger.Warn($"Could not register search directory: {directoryPath}");
- }
-
- logger.Info($"Loading: {fullPath}");
- IntPtr handle = LoadLibrary(fullPath);
- if (handle == IntPtr.Zero)
- {
- int err = Marshal.GetLastWin32Error();
- logger.Warn($"LoadLibrary failed for {fullPath} (Win32Error={err}).");
- return false;
- }
-
- logger.Info($"Successfully loaded: {fullPath}");
- return true;
- }
- catch (Exception ex)
- {
- logger.Warn($"Exception while loading {fullPath}: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// If requireSigned=false, returns true (no check).
- /// If requireSigned=true, verifies Authenticode chain and optional custom validator.
- ///
- private static bool ValidateSignatureIfRequired(
- string path,
- bool requireSigned,
- Func certValidator,
- ICompatibleLogger logger)
- {
- if (!requireSigned)
- {
- // No signature requirement: allow loading regardless of signature.
- return true;
- }
-
- try
- {
- // Throws on unsigned files.
- var baseCert = X509Certificate.CreateFromSignedFile(path);
- if (baseCert == null)
- {
- logger.Warn("No certificate extracted from file.");
- return false;
- }
-
- var cert = new X509Certificate2(baseCert);
-
- var chain = new X509Chain
- {
- ChainPolicy =
- {
- RevocationMode = X509RevocationMode.Online,
- RevocationFlag = X509RevocationFlag.ExcludeRoot,
- VerificationFlags = X509VerificationFlags.NoFlag,
- VerificationTime = DateTime.UtcNow
- }
- };
-
- bool chainOk = chain.Build(cert);
- if (!chainOk)
- {
- foreach (var status in chain.ChainStatus)
- logger.Warn($"Cert chain status: {status.Status} - {status.StatusInformation?.Trim()}");
- return false;
- }
-
- // Optional extra validation, e.g. thumbprint or subject pinning.
- if (certValidator != null)
- {
- bool ok = false;
- try { ok = certValidator(cert); }
- catch (Exception ex)
- {
- logger.Warn($"Custom certificate validator threw: {ex.Message}");
- return false;
- }
-
- if (!ok)
- {
- logger.Warn("Custom certificate validator rejected the certificate.");
- return false;
- }
- }
-
- logger.Info($"Signature validated. Subject='{cert.Subject}', Thumbprint={cert.Thumbprint}");
- return true;
- }
- catch (Exception ex)
- {
- logger.Warn($"Signature check failed: {ex.Message}");
- return false;
- }
- }
-
- private static bool TryRegisterSearchDirectory(string directoryPath, ICompatibleLogger logger)
- {
- try
- {
- bool ok = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
- if (!ok)
- {
- int e = Marshal.GetLastWin32Error();
- logger.Warn($"SetDefaultDllDirectories failed (Win32Error={e}), fallback to SetDllDirectory.");
- return SetDllDirectory(directoryPath);
- }
-
- IntPtr cookie = AddDllDirectory(directoryPath);
- if (cookie == IntPtr.Zero)
- {
- int e = Marshal.GetLastWin32Error();
- logger.Warn($"AddDllDirectory failed (Win32Error={e}), fallback to SetDllDirectory.");
- return SetDllDirectory(directoryPath);
- }
-
- logger.Info($"Registered native DLL search directory: {directoryPath}");
- return true;
- }
- catch (EntryPointNotFoundException)
- {
- logger.Warn("DefaultDllDirectories API not available. Using SetDllDirectory fallback.");
- return SetDllDirectory(directoryPath);
- }
- catch (Exception ex)
- {
- logger.Warn($"Register search directory failed for {directoryPath}: {ex.Message}");
- return false;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs
index 2ed4b5d..77fc01f 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs
@@ -32,19 +32,10 @@ namespace WelsonJS.Launcher
// set up logger
_logger = new TraceLogger();
- // load native libraries
- string appDataSubDirectory = "WelsonJS";
- bool requireSigned = string.Equals(
- GetAppConfig("NativeRequireSigned"),
- "true",
- StringComparison.OrdinalIgnoreCase);
-
- NativeBootstrap.Init(
- dllNames: new[] { "ChakraCore.dll" },
- appDataSubdirectory: appDataSubDirectory,
- logger: _logger,
- requireSigned: requireSigned
- );
+ // load external assemblies
+ AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl");
+ AssemblyLoader.Register();
+ AssemblyLoader.LoadNativeModules("ChakraCore", new Version(1, 13, 0, 0), new[] { "ChakraCore.dll" });
// telemetry
try
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs
index 6ef92bc..0c6cea4 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs
@@ -60,6 +60,15 @@ namespace WelsonJS.Launcher.Properties {
}
}
+ ///
+ /// https://catswords.blob.core.windows.net/welsonjs/packages과(와) 유사한 지역화된 문자열을 찾습니다.
+ ///
+ internal static string AssemblyBaseUrl {
+ get {
+ return ResourceManager.GetString("AssemblyBaseUrl", resourceCulture);
+ }
+ }
+
///
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
///
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx
index 0f32638..d0d2661 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx
@@ -241,4 +241,7 @@
true
+
+ https://catswords.blob.core.windows.net/welsonjs/packages
+
\ No newline at end of file
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
index a656599..a1a876a 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
@@ -88,12 +88,12 @@
+
-
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config
index 617e7c7..59b2fa6 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config
@@ -30,6 +30,7 @@
+