Skip to content

feat(urls): configurable base domain via --base-domain - #9728

Open
eordano wants to merge 11 commits into
devfrom
gateway
Open

feat(urls): configurable base domain via --base-domain#9728
eordano wants to merge 11 commits into
devfrom
gateway

Conversation

@eordano

@eordano eordano commented Aug 13, 2026

Copy link
Copy Markdown
Member

Target a non-decentraland.* deployment (e.g. interconnected.online) by resolving every backend host through DecentralandUrlsSource.ResolveDomain, which swaps the whole decentraland.{ENV} host token for a custom base domain supplied by the --base-domain app arg (AppArgsFlags.BASE_DOMAIN). The default path is byte-identical, swapping only the {ENV} TLD.

Consumers that follow the resolved domain: teleport validation (ChatEnvironmentValidator), realm trust (DeepLinkAllowlist), the main-realm comms fallback (RealmController), the realm-name server list (RealmNamesMap), the pre-login feature-flag whitelist URL (GetFeatureFlagsUrl), the smart-wearable content fallback (SmartWearableCache), and the local scene adapter (LocalGateKeeperSceneAdapter). Gateway routing already follows it: the transform runs on the {ENV} template and ResolveDomain rewrites the whole token, gateway host included. The default "Empty place" no longer hardcodes a peer.decentraland.org thumbnail and falls back to the built-in placeholder.

Characterization tests pin every DecentralandUrl across org/zone/custom domains and gateway routing (gatekeeper family included); unit tests cover custom-domain teleport validation and deep-link realm trust.

QA: Full automated testing should not raise any errors or changes -- this is internal only. Test --base-domain interconnected.online to see if it works on a different catalyst setup

Target a non-decentraland.* deployment (e.g. interconnected.online) by
resolving every backend host through DecentralandUrlsSource.ResolveDomain,
which swaps the whole decentraland.{ENV} host token for a custom base domain
supplied by the --base-domain app arg (AppArgsFlags.BASE_DOMAIN). The default
path is byte-identical, swapping only the {ENV} TLD.

Consumers that follow the resolved domain: teleport validation
(ChatEnvironmentValidator), realm trust (DeepLinkAllowlist), the main-realm
comms fallback (RealmController), the realm-name server list (RealmNamesMap),
the pre-login feature-flag whitelist URL (GetFeatureFlagsUrl), the
smart-wearable content fallback (SmartWearableCache), and the local scene
adapter (LocalGateKeeperSceneAdapter). Gateway routing already follows it: the
transform runs on the {ENV} template and ResolveDomain rewrites the whole
token, gateway host included. The default "Empty place" no longer hardcodes a
peer.decentraland.org thumbnail and falls back to the built-in placeholder.

Characterization tests pin every DecentralandUrl across org/zone/custom
domains and gateway routing (gatekeeper family included); unit tests cover
custom-domain teleport validation and deep-link realm trust.
@eordano
eordano requested review from a team as code owners August 13, 2026 12:16
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below.

Name Link
Commit 6b430f4
Logs https://github.com/decentraland/unity-explorer/actions/runs/31814082394
Download Windows https://github.com/decentraland/unity-explorer/suites/86309727898/artifacts/9225298918
Download Windows S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/gateway/pr-25148-6b430f4/Decentraland_windows64.zip
Download Mac https://github.com/decentraland/unity-explorer/suites/86309727898/artifacts/9225758194
Download Mac S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/gateway/pr-25148-6b430f4/Decentraland_macos.zip
Built on 2026-08-14T16:10:18Z

Lint

Warnings count reduced: 13156 => 13085

Warnings/errors in files changed by this PR (65)
Assets/DCL/Backpack/SmartWearableAuthorizationPopupController.cs:66  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/Backpack/SmartWearableAuthorizationPopupController.cs:77  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/Backpack/SmartWearableAuthorizationPopupController.cs:66  CSharpWarnings::CS8604  Possible null reference argument for parameter 'thumbnail' in 'Runtime.Wearables.SmartWearableAuthorizationPopupView.Setup'
Assets/DCL/LOD/Systems/LODContainer.cs:51  CSharpWarnings::CS8618  Non-nullable members 'roadAssetsPrefabList', 'RoadCoordinates' must contain non-null values when exiting constructor. Consider adding the 'required' modifiers or declaring the members as nullable.
Assets/DCL/LOD/Systems/LODContainer.cs:144  CSharpWarnings::CS8618  Non-nullable property 'LODSettingAsset' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/LOD/Systems/LODContainer.cs:141  CSharpWarnings::CS8618  Non-nullable property 'RoadData' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PlacesAPIService/PlacesAPIResponse.cs:144  ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract  Expression is always false according to nullable reference types' annotations
Assets/DCL/Infrastructure/Global/StaticContainer.cs:280  ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract  Expression is always true according to nullable reference types' annotations
Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs:72  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs:78  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/Roads/Systems/RoadPlugin.cs:54  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/PlacesAPIService/PlacesAPIResponse.cs:212  InconsistentNaming  Name 'IPlacesAPIResponse' does not match rule 'interfaces_should_be_pascal_case_with_i_prefix'. Suggested name is 'IPlacesApiResponse'.
Assets/DCL/Infrastructure/Global/StaticContainer.cs:141  InconsistentNaming  Name 'ISSDescriptorDiskCache' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IssDescriptorDiskCache'.
Assets/DCL/Infrastructure/Global/Tests/EditMode/RealmLaunchSettingsShould.cs:229  InconsistentNaming  Name 'IgnoreMacOSRealmInvalidation' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IgnoreMacOsRealmInvalidation'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:141  InconsistentNaming  Name 'IsMacOSVersionAcceptable' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IsMacOsVersionAcceptable'.
Assets/DCL/PlacesAPIService/PlacesAPIResponse.cs:221  InconsistentNaming  Name 'PlacesAPIGetParcelResponse' does not match rule 'members_should_be_pascal_case'. Suggested name is 'PlacesApiGetParcelResponse'.
Assets/DCL/PlacesAPIService/PlacesAPIResponse.cs:192  InconsistentNaming  Name 'PlacesAPIResponse' does not match rule 'members_should_be_pascal_case'. Suggested name is 'PlacesApiResponse'.
Assets/DCL/SmartWearables/SmartWearableCache.cs:188  InconsistentNaming  Name 'RequiresWeb3API' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RequiresWeb3Api'.
Assets/DCL/SmartWearables/SmartWearableCache.cs:86  InconsistentNaming  Name 'RequiresWeb3APIAsync' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RequiresWeb3ApiAsync'.
Assets/DCL/NetworkDefinitions/Browser/DecentralandUrlsSource.cs:167  InconsistentNaming  Name 'ResolvedBaseDomain' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'resolvedBaseDomain'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:211  InconsistentNaming  Name 'actualGBFloat' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'actualGbFloat'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:237  InconsistentNaming  Name 'always_accepted_cpus' does not match rule 'members_should_be_pascal_case'. Suggested name is 'AlwaysAcceptedCpus'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:253  InconsistentNaming  Name 'apple_silicon_supported_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'AppleSiliconSupportedRegex'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:249  InconsistentNaming  Name 'arc_gpu_supported_version_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'ArcGPUSupportedVersionRegex'.
Assets/DCL/Infrastructure/Global/Dynamic/RealmUrl/Names/RealmNamesMap.cs:73  InconsistentNaming  Name 'baseUrl' does not match rule 'members_should_be_pascal_case'. Suggested name is 'BaseUrl'.
Assets/DCL/Infrastructure/Global/Dynamic/RealmUrl/Names/RealmNamesMap.cs:81  InconsistentNaming  Name 'configurations' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Configurations'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:236  InconsistentNaming  Name 'integrated_gpu_supported_versions' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IntegratedGPUSupportedVersions'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:245  InconsistentNaming  Name 'intel_cpu_supported_version_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IntelCpuSupportedVersionRegex'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:243  InconsistentNaming  Name 'intel_supported_minimum_generation' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IntelSupportedMinimumGeneration'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:242  InconsistentNaming  Name 'intel_supported_minimum_series' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IntelSupportedMinimumSeries'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:246  InconsistentNaming  Name 'intel_ultra_cpu_supported_version_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IntelUltraCpuSupportedVersionRegex'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:244  InconsistentNaming  Name 'intel_ultra_supported_minimum_generation' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IntelUltraSupportedMinimumGeneration'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:235  InconsistentNaming  Name 'mac_supported_versions' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MacSupportedVersions'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:239  InconsistentNaming  Name 'macos_supported_version_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MacosSupportedVersionRegex'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:252  InconsistentNaming  Name 'minimum_arc_supported_version' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MinimumArcSupportedVersion'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:238  InconsistentNaming  Name 'minimum_macos_major_version' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MinimumMacosMajorVersion'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:250  InconsistentNaming  Name 'minimum_rtx_supported_version' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MinimumRtxSupportedVersion'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:251  InconsistentNaming  Name 'minimum_rx_supported_version' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MinimumRxSupportedVersion'.
Assets/DCL/Infrastructure/Global/Dynamic/RealmUrl/Names/RealmNamesMap.cs:90  InconsistentNaming  Name 'realmName' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RealmName'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:208  InconsistentNaming  Name 'requiredGB' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'requiredGb'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:215  InconsistentNaming  Name 'roundedActualGB' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'roundedActualGb'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:247  InconsistentNaming  Name 'rtx_gpu_supported_version_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RtxGPUSupportedVersionRegex'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:248  InconsistentNaming  Name 'rx_gpu_supported_version_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RxGPUSupportedVersionRegex'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:240  InconsistentNaming  Name 'ryzen_supported_cpu_regex' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RyzenSupportedCpuRegex'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:241  InconsistentNaming  Name 'ryzen_supported_minimum_series' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RyzenSupportedMinimumSeries'.
Assets/DCL/ApplicationGuards/ApplicationMinimumSpecsGuard/Logic/Utils/SystemSpecUtils.cs:234  InconsistentNaming  Name 'windows_supported_versions' does not match rule 'members_should_be_pascal_case'. Suggested name is 'WindowsSupportedVersions'.
Assets/DCL/Infrastructure/Global/Dynamic/RealmController.cs:314  ParameterHidesMember  Parameter 'realmEntity' hides field 'Global.Dynamic.RealmController.realmEntity'
Assets/DCL/Infrastructure/Global/Dynamic/RealmController.cs:319  ParameterHidesMember  Parameter 'realmEntity' hides field 'Global.Dynamic.RealmController.realmEntity'
Assets/DCL/Backpack/SmartWearableAuthorizationPopupController.cs:62  RedundantCast  Type cast is redundant
Assets/DCL/Backpack/SmartWearableAuthorizationPopupController.cs:22  RedundantNullnessAttributeWithNullableReferenceTypes  Type of the parameter is already not-nullable

…and 15 more (see the csharp-lint-reports artifact).

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 25239 0 13
PlayMode ✅ Passed 236 0 36

@github-actions
github-actions Bot requested review from NickKhalow and dalkia August 13, 2026 12:16
@decentraland-bot decentraland-bot added the ext-contribution Identifies a contribution which was not initiated by a Unity Developer label Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Slack notification sent to #explorer-ext-contributions for external review.
To re-send, delete this comment and re-add the ext-contribution label.

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(urls): configurable base domain via --base-domain

STEP 2 — Root-cause check: PASS ✅

The PR introduces a --base-domain CLI argument so the client can target a non-decentraland.* deployment (e.g. interconnected.online). The approach is sound: DecentralandUrlsSource.ResolveDomain() centralizes domain substitution. The default path replaces only {ENV} (byte-identical to before, today-mixture safe); a custom base domain replaces the whole decentraland.{ENV} token, moving every backend host. This is a feature addition, not a symptom fix.

STEP 3 — Design & integration: PASS ✅

No new long-lived units — all changes extend existing classes. The design introduces:

  • A customBaseDomain field on DecentralandUrlsSource (immutable after construction)
  • A static customBaseDomain on DeepLinkAllowlist (follows existing whitelistedWorlds pattern, set once during initialization, reset in test teardown)
  • New constructor parameters on SmartWearableCache, RealmNamesMap, ChatEnvironmentValidator

Owner search results:

  • DecentralandUrlsSource already owns all URL resolution — ResolveDomain() and ResolvedBaseDomain belong here.
  • DeepLinkAllowlist already owns host trust checks — extending IsDecentralandHost with the custom domain is the right place.
  • ChatEnvironmentValidator already owns teleport validation — parameterizing its domain suffix rather than hardcoding it is correct.
  • GatewayUrlsSource derives gateway routing from the base class — using ResolvedBaseDomain (protected) is legitimate inheritance, not leaky abstraction.

Teardown/consumption trace:

  • No new subscriptions, event hookups, connections, or disposable resources introduced. All changes are constructor-time wiring or one-time static initialization.

STEP 4 — Member audit: PASS ✅

Member Consumers Verdict
ResolveDomain() (private) Probe(), Url(), ResolveOptimizedAssetsUrl() — central substitution point Correctly encapsulated
ResolvedBaseDomain (protected) GatewayUrlsSource ctor (3 refs: resolvedNonClientHosts, gatewayPrefix, domainSuffix) Legitimate protected accessor for inheritance
SetCustomBaseDomain() (public static) MainSceneLoader.InitializeFlowAsync(), test teardown Follows existing SetWhitelistedWorlds pattern
DOMAIN_TOKEN (protected const) ResolveDomain(), GatewayUrlsSource ctor, GetFeatureFlagsUrl() Correctly scoped

No single-use intermediaries, no absent-≠-false predicates, no redundant guards.

STEP 5 — Line-level review

Security review: No security issues found.

  • BASE_DOMAIN is a CLI arg, NOT in the deep link PERMITTED_KEYS or WHITELISTED_REALM_PERMITTED_KEYS — an attacker cannot inject it via deep link ✅
  • IsDecentralandHost boundary checks are correct: host.Length > customBaseDomain.Length + host[..] == '.' + EndsWith — rejects both bare domain and suffix-spoof attacks ✅
  • ChatEnvironmentValidator.HostHasSuffix rejects userinfo-based host confusion (@ check before :) ✅
  • No secrets, credentials, or PII exposed ✅
  • Gateway routing works correctly: IsGatewayTransformable runs on unresolved templates (which always contain .decentraland.), then ResolveDomain substitutes the custom domain afterward ✅

Findings: Two P2 issues (see line comments below).

STEP 6 — Complexity: COMPLEX

20 files changed (+444 −37). Touches URL infrastructure (DecentralandUrlsSource, GatewayUrlsSource), gateway routing, deep link trust model (DeepLinkAllowlist), teleport validation, realm name resolution, and smart wearable content fallback across multiple assemblies.

STEP 7 — QA assessment: YES

Runtime code changes affecting URL routing, teleport validation, and realm trust. Although the default path is byte-identical (backward compatible when --base-domain is not supplied), the URL resolution and gateway routing paths have changed.

STEP 8 — Non-blocking warnings

None. Main scene not modified.

STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches URL infrastructure, gateway routing, deep link trust model, and teleport validation across 7+ assemblies
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by unknown via Slack

Comment thread docs/base-domain-generalization.md Outdated
@eordano eordano added the force-build Used to trigger a build on draft PR label Aug 13, 2026
@decentraland-bot

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

Rename catalyrst -> catalyst in the base-domain doc and drop the
ReSharper CheckNamespace suppression from the new characterization test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@decentraland-bot

This comment has been minimized.

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STEP 1 — Context & Scope

PR: feat(urls): configurable base domain via --base-domain
Target: dev
Files: 20 changed (+442 −37)
Subsystems: NetworkDefinitions (DecentralandUrlsSource, GatewayUrlsSource), Infrastructure (DeepLinkAllowlist, ChatContainer, RealmController, MainSceneLoader, StaticContainer, BootstrapContainer), Chat (ChatEnvironmentValidator), PlacesAPIService, SmartWearables.

Loaded: CLAUDE.md, docs/README.md, review-instructions.md. Reviewed full source of DecentralandUrlsSource.cs, GatewayUrlsSource.cs, IDecentralandUrlsSource.cs, DeepLinkAllowlist.cs, RealmController.cs, ChatContainer.cs, SmartWearableCache.cs, ThumbnailLoader.cs.

STEP 2 — Root-cause check

PASS. This PR adds a feature (configurable base domain via --base-domain), not a bug fix. The approach centralizes domain resolution through ResolveDomain() — a new private method that substitutes either {ENV} (default, byte-identical to prior behavior) or the full decentraland.{ENV} token (custom domain). The design correctly addresses the need to target non-decentraland.* deployments without modifying the DecentralandEnvironment enum.

STEP 3 — Design & integration

PASS. No new long-lived units introduced. The changes parameterize existing classes:

  • DecentralandUrlsSource: gains customBaseDomain field, DOMAIN_TOKEN constant, ResolveDomain() method, and ResolvedBaseDomain property. All contained within the existing class hierarchy.
  • GatewayUrlsSource: derives gateway prefix/suffix/non-client hosts from ResolvedBaseDomain instead of direct ENV replacement. Gateway transform runs on raw templates before ResolveDomain() — these compose correctly (confirmed by characterization tests).
  • DeepLinkAllowlist.SetCustomBaseDomain(): follows the established SetWhitelistedWorlds() pattern (static mutable state, same class). Domain-trust check mirrors the existing ALL_DOMAINS loop with identical dot-boundary validation. Suffix-spoofing correctly rejected (test at line 800 confirms interconnected.online.attacker.comfalse).
  • Consumer updates (ChatEnvironmentValidator, RealmNamesMap, SmartWearableCache, RealmController) receive the resolved domain via dependency injection or the URL source — no lifecycle duplication, no new reconciliation loops.

Owner search: No new lifecycle-owning units to audit.

Teardown/consumption trace: No new subscriptions, event hookups, or connections. RealmData.RealmType.OnUpdate += ResetRealmDependentUrls is pre-existing and unchanged.

STEP 4 — Member audit

Member Consumers Assessment
ResolvedBaseDomain (protected) ResolveDomain(), GatewayUrlsSource ctor (3 reads) Multi-use, justified. Two external call sites re-derive this value — see inline finding.
ResolveDomain() (private) Probe(), Url(), ResolveOptimizedAssetsUrl() 4 call sites, cleanly encapsulates branching logic.
DOMAIN_TOKEN (const) ResolveDomain(), GetFeatureFlagsUrl(), GatewayUrlsSource ctor 3 call sites across 2 files, justified.
SetCustomBaseDomain() MainSceneLoader.InitializeFlowAsync, test teardown 2 call sites, follows SetWhitelistedWorlds pattern.
realmDomainSuffix (ChatEnvironmentValidator) ValidateTeleport() Zone/Org cases Replaces previously hardcoded domain constants. Legitimate parameterization.

Note: GetFeatureFlagsUrl (line 255) duplicates the custom-domain-or-env-fallback branching from ResolveDomain() as a static method. This is necessary since it runs before the instance exists (InitializeDeepLinkWorldWhitelistAsync), but the parallel logic could drift — worth extracting a shared static helper if this method gains more callers.

STEP 5 — Line-level review

See inline comments for each finding with suggestion blocks.

Summary of findings:

# Sev File Issue
1 P2 ChatContainer.cs:106-108 Fragile .Replace("https://", "") re-derives ResolvedBaseDomain
2 P2 RealmController.cs:375 Same .Replace pattern (second occurrence)
3 P2 DeepLinkAllowlist.cs:172-173 No format validation on custom domain — single-label domains trust entire TLDs
4 P2 DecentralandUrlsSource.cs:68 Redundant null-forgiving ! operator

Security review: No secrets committed. --base-domain is correctly excluded from PERMITTED_KEYS (not injectable via deep links). Domain-trust expansion in IsDecentralandHost uses proper dot-boundary validation matching the existing ALL_DOMAINS pattern. Gateway routing correctly composes with domain resolution. No auth bypass vectors identified.

Characterization tests: The 246-line DecentralandUrlsSourceCharacterizationShould.cs is excellent — it pins every domain-bearing URL for both Org and Zone environments, verifies env-independent URLs stay unchanged, tests gateway routing round-trips, and covers custom-domain retargeting. This is exactly the safety net needed for this change.

STEP 6 — Complexity assessment

COMPLEX. Touches URL resolution, domain trust (DeepLinkAllowlist), gateway routing, realm validation, and chat teleport validation across 20 files spanning NetworkDefinitions, Infrastructure, Chat, PlacesAPIService, and SmartWearables assemblies.

STEP 7 — QA assessment

QA_REQUIRED: YES. Changes runtime URL resolution affecting realm connections, teleportation validation, gateway routing, smart wearable content loading, and realm name resolution. However, the PR is designed to be byte-identical on the default path (no custom domain), so the risk to existing behavior is low.

STEP 8 — Non-blocking warnings

None. Main.unity is not modified.

STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches URL resolution, domain trust, gateway routing, and realm validation across 20 files in NetworkDefinitions, Infrastructure, Chat, and SmartWearables assemblies
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by eordano via GitHub

Comment on lines +106 to +108
string realmDomainSuffix = bootstrapContainer.DecentralandUrlsSource
.Url(DCL.Multiplayer.Connections.DecentralandUrls.DecentralandUrl.Host)
.Replace("https://", string.Empty);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Re-derives ResolvedBaseDomain via fragile scheme stripping (CLAUDE.md: "Don't re-derive what already exists").

DecentralandUrlsSource.ResolvedBaseDomain already computes this exact value, but is protected and not on IDecentralandUrlsSource. Both this site and RealmController.ResolveHostname (line 375) independently strip the scheme with .Replace("https://", ""), which silently breaks if the scheme ever changes.

Ideal fix: expose BaseDomain on IDecentralandUrlsSource (backed by ResolvedBaseDomain). Immediate improvement: use Uri.Host for safer parsing:

Suggested change
string realmDomainSuffix = bootstrapContainer.DecentralandUrlsSource
.Url(DCL.Multiplayer.Connections.DecentralandUrls.DecentralandUrl.Host)
.Replace("https://", string.Empty);
string realmDomainSuffix = new Uri(bootstrapContainer.DecentralandUrlsSource
.Url(DCL.Multiplayer.Connections.DecentralandUrls.DecentralandUrl.Host)).Host;

// TODO: take in consideration the web3-network. If its sepolia then it should be .zone
? "realm-provider." + IDecentralandUrlsSource.ORG_DOMAIN
// Consider it as the "main" realm which shares the comms with many catalysts.
? "realm-provider." + decentralandUrlsSource.Url(DecentralandUrl.Host).Replace("https://", string.Empty)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Same .Replace("https://", "") pattern — second occurrence of the fragile domain extraction. Same BaseDomain interface property would eliminate both.

Suggested change
? "realm-provider." + decentralandUrlsSource.Url(DecentralandUrl.Host).Replace("https://", string.Empty)
? "realm-provider." + new Uri(decentralandUrlsSource.Url(DecentralandUrl.Host)).Host

Comment on lines +172 to +173
public static void SetCustomBaseDomain(string? domain) =>
customBaseDomain = string.IsNullOrWhiteSpace(domain) ? null : domain.Trim();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Defense-in-depth: validate domain shape. A single-label domain (e.g. --base-domain com) would cause IsDecentralandHost to trust every .com host. While --base-domain is CLI-only (not in PERMITTED_KEYS, so deep links cannot inject it), basic format validation prevents accidental misconfiguration and hardens the public API surface of SetCustomBaseDomain().

Suggested change
public static void SetCustomBaseDomain(string? domain) =>
customBaseDomain = string.IsNullOrWhiteSpace(domain) ? null : domain.Trim();
public static void SetCustomBaseDomain(string? domain)
{
if (string.IsNullOrWhiteSpace(domain))
{
customBaseDomain = null;
return;
}
string trimmed = domain.Trim();
// Require at least two labels and reject URL control characters (defense-in-depth).
if (!trimmed.Contains('.') || trimmed.IndexOfAny(new[] { '/', '?', '#', '@', ':' }) >= 0)
throw new ArgumentException($"Invalid base domain: '{trimmed}'");
customBaseDomain = trimmed;
}

string? customBaseDomain = null)
{
decentralandDomain = environment.ToString()!.ToLower();
this.customBaseDomain = string.IsNullOrWhiteSpace(customBaseDomain) ? null : customBaseDomain!.Trim();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Redundant null-forgiving !IsNullOrWhiteSpace returning false guarantees non-null on the else branch; the ! is unnecessary noise.

Suggested change
this.customBaseDomain = string.IsNullOrWhiteSpace(customBaseDomain) ? null : customBaseDomain!.Trim();
this.customBaseDomain = string.IsNullOrWhiteSpace(customBaseDomain) ? null : customBaseDomain.Trim();

eordano and others added 3 commits August 14, 2026 10:21
Gateway-eligible URLs resolved before feature flags load are no longer
cached in raw form for the process lifetime; they stay uncacheable until
the use-gateway flag is known. The main-realm comms fallback pins
realm-provider.decentraland.org for every decentraland.* environment and
follows only a custom base domain. Characterization tests pin the
pre-flags -> post-flags resolution sequence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SystemSpecUtils honors TryGetJsonPayload's result and absent individual
fields instead of dereferencing null members; a failing specs evaluation
reports the exception and never blocks startup, so
--skip-minimum-specs-screen stays effective.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A payload defining only one platform's requirements no longer rejects the
other platform: when every field of a dimension (cpu/gpu/os) is absent the
check passes, while an explicitly-present empty list still rejects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@decentraland-bot

This comment has been minimized.

eordano and others added 2 commits August 14, 2026 14:49
Clears the 50 InspectCode findings in files this PR touches (CS8618
uninitialized non-nullables via null! / nullable annotations per file
idiom, CS8602/03/04 via real guards or invariant-backed forgiveness),
bringing the warning count back under the ratchet baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…missing

The tolerance fixes land without a regression test for the missing-payload
path; this pins every SystemSpecUtils check against an empty flags
configuration, live-hit on a --base-domain deployment whose flags backend
served no alfa-minimum-requirements payload (boot died on splash).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9728, run #31804214385

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2331 (×3) 2374 (×3)
CPU average 38.3 ms (36.6–38.8) 37.7 ms (36.6–38.1) -0.6 ms ⚪ within noise
CPU 1% worst 380.0 ms (330.7–396.1) 299.5 ms (287.0–306.7) -80.5 ms 🟢 21% faster
CPU 0.1% worst 411.3 ms (351.0–424.5) 310.3 ms (307.0–323.3) -101.0 ms 🟢 25% faster
GPU average 8.0 ms (8.0–8.0) 8.1 ms (7.8–8.1) 0.1 ms ⚪ within noise
GPU 1% worst 18.8 ms (18.6–19.3) 18.8 ms (18.8–19.1) 0.1 ms ⚪ within noise
GPU 0.1% worst 19.2 ms (19.0–20.2) 19.5 ms (19.3–19.9) 0.3 ms ⚪ within noise
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

Apple M1

Metric Baseline Change Δ Result
Samples 4066 (×3) 3871 (×3)
CPU average 22.1 ms (21.9–22.1) 23.2 ms (21.7–23.4) 1.1 ms ⚪ within noise
CPU 1% worst 231.3 ms (196.1–233.3) 230.8 ms (206.0–234.4) -0.4 ms ⚪ within noise
CPU 0.1% worst 233.6 ms (231.9–238.3) 240.2 ms (236.0–245.0) 6.6 ms ⚪ within noise
GPU average 2.5 ms (1.9–2.7) 2.6 ms (2.1–10.7) 0.0 ms ⚪ within noise
GPU 1% worst 34.6 ms (34.2–35.5) 35.9 ms (34.4–36.7) 1.3 ms ⚪ within noise
GPU 0.1% worst 35.7 ms (35.2–37.2) 37.4 ms (36.3–37.8) 1.7 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

eordano and others added 4 commits August 14, 2026 16:22
…domain check

Pre-flags gateway-eligible URLs get a dedicated FlagsPending cache state:
cached provisionally, then dropped and re-resolved once feature flags
load. This closes the torn-read window that could permanently cache an
un-gatewayed URL when flags landed mid-resolution, and bounds
flags-never-load sessions to one resolution per URL instead of one per
call. RealmController classifies environment domains by exact membership
in ALL_DOMAINS instead of a spoofable name prefix, and SmartWearableCache
returns the tracked item instead of null! on the cancelled path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
StaticContainer.GPUInstancingService is nullable for real: the disabled
path (no render feature / instancing off) now skips registering the
GPU-instancing feature set (plugin, RoadsPresence, debug system) instead
of carrying a null! that consumers dereference; intentional disable and
misconfiguration are logged distinctly. RealmController's main-realm
hostname derivation becomes an internal static seam with tests pinning
org, the zone->org re-pin, custom domains, and the env-domain spoof shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Unity-bundled NUnit predates Assert.Multiple (CS0117), which broke
compilation of DCL.EditMode.Tests and with it the Lint and EditMode jobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9728, run #31818100451

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2287 (×3) 2370 (×3)
CPU average 39.2 ms (36.8–39.3) 37.7 ms (37.2–37.8) -1.5 ms ⚪ within noise
CPU 1% worst 378.9 ms (366.5–380.5) 293.1 ms (286.6–311.3) -85.8 ms 🟢 23% faster
CPU 0.1% worst 395.9 ms (389.0–396.8) 309.2 ms (308.4–328.4) -86.7 ms 🟢 22% faster
GPU average 9.5 ms (9.4–9.6) 8.4 ms (8.3–9.6) -1.1 ms ⚪ within noise
GPU 1% worst 40.2 ms (39.7–40.3) 20.3 ms (19.5–34.6) -19.9 ms 🟢 49% faster
GPU 0.1% worst 48.7 ms (44.9–49.0) 23.2 ms (20.9–41.6) -25.5 ms 🟢 52% faster
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

Apple M1

Metric Baseline Change Δ Result
Samples 4021 (×3) 3919 (×3)
CPU average 22.3 ms (21.7–23.2) 22.9 ms (22.2–23.2) 0.6 ms ⚪ within noise
CPU 1% worst 230.5 ms (224.7–232.1) 217.8 ms (215.2–222.4) -12.8 ms 🟢 6% faster
CPU 0.1% worst 238.5 ms (234.0–239.6) 221.8 ms (221.3–230.8) -16.7 ms 🟢 7% faster
GPU average 6.9 ms (2.7–7.2) 3.4 ms (2.2–6.9) -3.5 ms ⚪ within noise
GPU 1% worst 35.3 ms (33.7–37.2) 36.7 ms (34.8–37.7) 1.4 ms ⚪ within noise
GPU 0.1% worst 36.3 ms (35.1–38.0) 39.2 ms (36.4–40.0) 2.9 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ext-contribution Identifies a contribution which was not initiated by a Unity Developer force-build Used to trigger a build on draft PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants