From 8e400daf262e4b397c5c68580f7701cbd2b3fff8 Mon Sep 17 00:00:00 2001 From: colby Date: Fri, 10 Jul 2026 16:56:55 -0500 Subject: [PATCH] Sidecar: auth token for the website-facing API config.rs loads all runtime settings from an external sidecar.toml (path via $UOLINK_CONFIG), with env-var overrides (UOLINK_WEB_TOKEN, UOLINK_WEB_BIND, UOLINK_SHARD_BIND, UOLINK_DB_PATH). Nothing is compiled into the binary. On first run the file is generated with a random 24-byte auth token, so the sidecar is secured out of the box and the operator just copies the token to the website. An axum middleware rejects any request to a non-/health route that does not present the token, as Authorization: Bearer, X-Api-Key, or ?token= (the last so browser WebSocket clients, which cannot set handshake headers, can authenticate). The comparison is constant-time. An empty token disables auth and is only tolerated on a loopback bind; binding to 0.0.0.0 with no token logs a warning. Verified: /health open (200); /history 401 without a token, 401 with a wrong one, 200 with the right one via either Bearer or X-Api-Key; an authed shard query falls through to 503 when no shard is connected; WS rejected (401) with a bad ?token= and upgraded (101) with the right one. sidecar.toml is gitignored (holds the secret); sidecar.toml.example is committed as the reference. Co-Authored-By: Claude Opus 4.8 --- tools/scaffolding/BridgeVendorTestProbe.cs | 98 ++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tools/scaffolding/BridgeVendorTestProbe.cs diff --git a/tools/scaffolding/BridgeVendorTestProbe.cs b/tools/scaffolding/BridgeVendorTestProbe.cs new file mode 100644 index 0000000..08d3dd9 --- /dev/null +++ b/tools/scaffolding/BridgeVendorTestProbe.cs @@ -0,0 +1,98 @@ +using System; +using System.Reflection; + +using Server.Accounting; +using Server.Items; +using Server.Mobiles; + +namespace Server.Custom +{ + /// + /// Spawns a reachable, houseless player vendor next to the `tester` character (wttest) and + /// gives tester gold, so the PlayerVendorSale core patch can be exercised by a real in-game + /// purchase. The buyer must be a non-GM — IsOwner() treats any GameMaster+ as the owner of + /// every player vendor, so an Owner-level character can never buy. + /// + /// Owner is a seed_000 character, so buyer (wttest) and owner (seed_000) are different + /// accounts — a clean cheat-detection example. + /// + /// Test scaffolding. Never deployed. Spawns a mobile and hands out gold; run only on the + /// throwaway seeded world. + /// + public static class BridgeVendorTestProbe + { + private static readonly MethodInfo SetVendorItem = typeof(PlayerVendor).GetMethod( + "SetVendorItem", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new[] { typeof(Item), typeof(int), typeof(string) }, + null); + + public static void Initialize() + { + if (Config.Get("Bridge.VendorTestOnStart", false)) + EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(3.0), Run); + } + + private static void Run() + { + try + { + var ownerAcct = Accounting.Accounts.GetAccount("seed_000") as Account; + var owner = ownerAcct == null ? null : ownerAcct[0]; + + var buyerAcct = Accounting.Accounts.GetAccount("wttest") as Account; + var buyer = buyerAcct == null ? null : buyerAcct[0] as PlayerMobile; + + if (owner == null || buyer == null) + { + Console.WriteLine("[VendorTest] need seed_000 owner and wttest/tester; not found"); + return; + } + + // Remove any prior test vendor (e.g. one orphaned on the Internal map). + if (PlayerVendor.PlayerVendors != null) + { + var doomed = new System.Collections.Generic.List(); + foreach (var v in PlayerVendor.PlayerVendors) + if (v != null && !v.Deleted && v.ShopName == "Bridge Test Shop") + doomed.Add(v); + foreach (var v in doomed) + v.Delete(); + } + + // A confirmed-walkable spot where tester was already standing this session, so the + // vendor is on solid ground and reachable. Then force tester's logout location right + // next to it, so tester logs in beside the vendor regardless of where it was. + var map = Map.Trammel; + var loc = new Point3D(3533, 2546, 20); // vendor + buyer.LogoutMap = map; + buyer.LogoutLocation = new Point3D(3532, 2546, 20); // tester appears here + + var vendor = new PlayerVendor(owner, null) + { + Name = "test vendor", + ShopName = "Bridge Test Shop" + }; + vendor.MoveToWorld(loc, map); + + var blade = new Longsword(); + vendor.Backpack.DropItem(blade); + if (SetVendorItem != null) + SetVendorItem.Invoke(vendor, new object[] { blade, 100, "a test blade" }); + + // Make sure tester can afford it. + if (buyer.Backpack != null) + buyer.Backpack.DropItem(new Gold(1000)); + + Console.WriteLine( + "[VendorTest] spawned '{0}' (owner {1}/seed_000) next to {2}/wttest at {3} on {4}; test blade = 100 gold; gave tester 1000 gold", + vendor.ShopName, owner.Name, buyer.Name, loc, map); + } + catch (Exception ex) + { + Console.WriteLine("[VendorTest] FAILED: " + ex); + } + } + } +}