feat: support custom gateway origins - #9845
Closed
pentreathm wants to merge 12 commits into
Closed
Conversation
Adds `DecentralandEnvironment.Custom`, selected by the new `--base-domain` app
arg, so the client can target a deployment served under a base domain other than
decentraland.{org,zone,today} (e.g. interconnected.online).
The url table no longer carries a domain per entry. `RawUrl` composes each host
from a `hostDomain` field (`$"https://peer.{hostDomain}/about"`), replacing the
85 `decentraland.{ENV}` templates and the `Replace` pass that resolved them:
`ENV`, `DOMAIN_TOKEN`, `ResolveDomain` and `decentralandDomain` are gone, `Url()`
only caches what `RawUrl` produced, and `Probe()` is a single call. A custom
domain is no longer a separate substitution path, just a different value in that
field.
`DecentralandUrlsSource.ResolveBaseDomain` is the single place the domain is
decided and `IDecentralandUrlsSource.BaseDomain` the single place to read it, so
nothing restates a literal. A base domain that is not a bare domain, a `Custom`
environment without one, or one paired with another environment all throw rather
than being ignored. `hostDomain` equals `BaseDomain` except in the today
environment, which resolves its `.today` hosts in the constructor and then flips
the field to org — the one reason urls must stay lazily resolved, now an explicit
mutable field instead of an emergent property of token substitution.
`GatewayUrlsSource.IsGatewayTransformable` matched a hardcoded `.decentraland.`
marker, which only worked while it saw unresolved templates; it now accepts
exactly a single-label subdomain under this client's own `BaseDomain`. That is
strictly tighter and domain-agnostic, and it removes an accidental asymmetry
where `LocalGateKeeperSceneAdapter` escaped the gateway on a custom domain while
the same url routed through it on org.
Following the resolved domain: the pre-login feature-flag host, the catalyst
server list (RealmNamesMap), the smart-wearable content fallback
(SmartWearableCache), gateway routing, teleport validation
(ChatEnvironmentValidator), deep-link realm trust
(DeepLinkAllowlist.SetTrustedBaseDomain) and the main-realm comms hostname
(RealmController). `--gatekeeper-url` still outranks all of it. The default
"Empty place" no longer carries a peer.decentraland.org thumbnail hash a custom
deployment cannot serve; its views pass the placeholder sprite their prefab
already ships.
Because the base domain decides which realm hosts are trusted, it is read while
the launch deep link is still deferred: only the command line can set it, and it
outranks a deep-link `--dclenv`.
Every other environment-dependent decision gets an explicit `Custom` arm instead
of falling into a default nobody chose: ethereum network (ChainUtils, now one
`IsMainnet` predicate instead of four copies), marketplace credits, donations,
the stored identity slot, the community-message router identity and the Genesis
City manifest. `Custom` is treated as a non-production stack throughout —
sepolia/Amoy, the sepolia identity slot, `message-router-dev-0` — so an
unverified deployment can never be handed mainnet contracts.
Characterization tests walk every DecentralandUrl and fail if one still resolves
to a decentraland host; unit tests cover base domain validation, gateway routing,
teleport validation and deep-link realm trust under a custom domain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The startup trusted-realm gate fast-passed a hardcoded list of decentraland hosts and otherwise consulted the deployment's catalyst server list. That list enumerates catalysts, not worlds servers -- which is exactly why decentraland's worlds-content-server is hardcoded there -- so a --base-domain deployment's world realm matched neither and raised the untrusted-realm confirmation on every launch. Every host under the resolved base domain is now trusted, so a catalyst or a world on that deployment connects without it. Safe because --base-domain is command-line only and never accepted from a deep link: reaching this gate already required the operator to point the client at that deployment. The deep-link gate stays deliberately stricter -- subdomains only, and still gated on the flag-whitelisted world name -- because the realm it sees is attacker-supplied. The '.'-boundary check those gates share moves to IDecentralandUrlsSource.IsSubdomainOf / IsHostWithinDomain, replacing the private copy in DeepLinkAllowlist with identical semantics, so the rule that rejects "interconnected.online.attacker.com" and "evil-interconnected.online" lives in one place and is covered by tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
base-domain is denied by DeepLinkAllowlist, so a link carrying it reaches the denied-params consent dialog like gatekeeper-url or feature-flags-url. Accepting it there did nothing: ApplyBaseDomainArg reads the arg before the deep link is processed and nothing reads it again, so the value sat in the logged arguments looking applied while the client stayed on decentraland. WarnIfBaseDomainCameFromTheDeepLink now reports that, and the comments and docs state the actual reason the arg is command-line only -- an ordering constraint, not a trust boundary. The domain gates which realms DeepLinkAllowlist trusts, so it has to be registered before InitializeDeepLinks() evaluates a pending link's whitelisted-realm params; a domain arriving in that same link could not be, because the link's own params would need gating against a domain it has not supplied yet. Also restates why trusting every host under the base domain costs nothing: whoever sets it has already redirected every backend host, so realm trust adds no capability on top -- an argument that does not depend on how the value arrived. Brings docs/custom-base-domain.md back in line with the code: it still described the hostDomain field that was merged into BaseDomain, and still claimed a custom world realm costs one extra request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two areas a --base-domain deployment has to reason about, neither of which the flag itself decides. Manifest and roads: RealmKind comes from the realm's own /about, so a custom catalyst realm classifies as GenesisCity. RoadsPresence switches instanced road rendering on that alone and the geometry is a local Addressable baked from decentraland's Genesis City layout, so a custom genesis realm gets decentraland's roads drawn over its parcels. Skipping the genesis manifest also leaves WorldManifest.IsEmpty, which disables the occupied-parcel filter in LoadPointersByIncreasingRadiusSystem and the manifest-derived terrain and spawn data -- an optimisation lost rather than a break, and still better than applying decentraland's parcel set to a foreign realm. Per-deployment world manifests already work through the asset-bundle-registry; only the genesis one has no per-deployment source. Authentication: the auth api and the signing web app follow the base domain, so a deployment must serve both for login to complete. The chain RPC is split -- the external-wallet dapp path resolves ApiRpc and moves to the base domain, while ThirdWebAuthenticator keeps its hardcoded rpc.decentraland.org map -- and the sepolia identity slot is shared with zone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SDK6 hybrid scenes and LocalIpfsRealm were listed as gaps but neither asks anything of a reader: the SDK6 path is legacy, and LocalIpfsRealm is only ever constructed by IRealmData.Fake so it never runs in a shipped client. The ThirdWeb chain RPC is already covered where it matters, in the authentication section. What is left is the one thing a reader has to know: which links stay on decentraland, and that the marketplace/shop/docs ones do not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The manifest path and RealmKind were described as one classification, which hid that they key off different things. The manifest branch is chosen by matching configurations.realmName against WorldManifestProvider's hardcoded list of decentraland's genesis realm names, so a custom deployment calling its main realm "main" collides with that list; RealmKind ignores the name entirely and comes from whether /about lists fixed scene URNs. The distinction is what a reader needs: renaming a realm away from "main" changes which manifest branch it takes but not that it classifies as Genesis City, so it does not turn roads off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chain was derived from DecentralandEnvironment in four independent places - the ethereum ids, marketplace credits, donations and the stored identity slot. An override applied to one of them would have let them disagree, so they all take the resolved EthereumNetwork now and nothing re-derives a chain of its own. Each ethereum network carries exactly one polygon network, because a deployment is on both or neither. ChainUtils.ResolveNetwork is the single decision. PinnedNetworkOf fixes org and today to mainnet and zone to sepolia, and no argument moves them: --eth-network paired with one of those is reported and dropped. Only a --base-domain deployment reads the value, defaulting to mainnet, which is what brings a deployment mirroring mainnet into scope for that flag. The value is captured while the deep link is still deferred, as --base-domain is, so a link cannot pick the chain - not even through the denied-params dialog. Where the value is read, one naming no known network ends the launch instead of falling back: reaching mainnet by mistyping the flag is what would put real contracts and the production identity slot behind an operator who asked for a test chain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The embedded wallet reached decentraland's RPC proxy from a hardcoded
table while the external-wallet path resolved ApiRpc, so the two disagreed
about where a chain lives on a --base-domain deployment. They are one
dependency configured twice: the same host over websocket and https.
The map is now built per instance from the new DecentralandUrl.ChainRpc,
so every chain the client may transact on follows the base domain.
Probed rather than resolved through Url: rpc is a single-label subdomain,
so gateway routing would rewrite it to gateway.{domain}/rpc once that flag
is on - a change of route for every chain call, on the default
environments too, and a separate decision from this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
🚦 CI StatusWindows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below. Waiting for lint to start… Waiting for tests to start… |
pentreathm
force-pushed
the
fix/fixture-gateway-url
branch
from
August 24, 2026 13:25
c3d18fa to
7045536
Compare
pentreathm
force-pushed
the
fix/fixture-gateway-url
branch
from
August 24, 2026 13:47
7045536 to
2d3661a
Compare
This comment has been minimized.
This comment has been minimized.
Contributor
|
PR #9845, run #32766857263 Builds: Windows change, Windows baseline, macOS change, macOS baseline How to read this table
Intel Core i5
Exception breakdown
Apple M1
|
mikhail-dcl
added a commit
that referenced
this pull request
Aug 25, 2026
--base-domain (#9826) landed in dev and overlaps every file --gateway touches. Five conflicts, resolved as follows. GatewayUrlsSource: dev moved the gateway host off the hardcoded env domain onto gateway.{BaseDomain}, so --gateway now overrides that resolved prefix rather than a decentraland-shaped one. gatewayPrefix became the single origin both paths build from, which removes the second branch the transform carried and drops the domain-derivation the default path used to do -- dev's IsGatewayTransformable already restricts transformable hosts to this deployment's own base domain, so nothing else can reach it. The shape follows #9845, which implements the same override on top of the same dev change. The value is now validated and normalized (NormalizeGatewayPrefix): an absolute http(s) url with a host and no query or fragment, reduced to a prefix ending in '/'. Anything else ends the launch instead of being coerced, because a mistyped gateway silently routes every supported service somewhere unintended. MainSceneLoader: kept TrustedRealms.IsTrusted over dev's re-inlined host list, and kept dev's --base-domain arm alongside it. --gateway is now captured while the deep link is still deferred and reported through WarnIfCommandLineOnlyArgCameFromTheDeepLink, matching --base-domain and --eth-network: reading it later meant accepting it in the denied-params dialog would have applied it, which is not what "command line only" means. AppArgsFlags, DeepLinkAllowlist and docs/app-arguments.md: both sides kept, with gateway named in the same never-permitted tier as the other infrastructure- pointing params. Verified in Unity batch EditMode: 203/203 across AppArgsTest, DecentralandUrlsSourceShould, TrustedRealmsShould, ChatEnvironmentValidatorShould and RealmLaunchSettingsShould -- dev's base-domain cases included -- no compile errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member
Author
|
Superseded by #9822, which provides the required argument and custom realm support. The automation now uses ; no separate binary from this PR is required. |
Member
Author
|
Correction: superseded by #9822, which provides the required --gateway argument and custom realm support. The automation now uses --gateway; no separate binary from this PR is required. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
--gateway-urlto override the origin used by gateway-routed requestsThis is needed by the Explorer E2E fixture, whose gateway is served on the realm origin because ephemeral fixture subdomains do not have per-service TLS certificates.