diff --git a/src/backend/src/services/auth/AuthService.js b/src/backend/src/services/auth/AuthService.js index 4df916083..642394186 100644 --- a/src/backend/src/services/auth/AuthService.js +++ b/src/backend/src/services/auth/AuthService.js @@ -1321,6 +1321,7 @@ class AuthService extends BaseService { const normalizedOrigin = this._origin_from_url(origin); if ( normalizedOrigin === null ) return null; + const isFirstPartyHostedOrigin = this.isHostedOriginOnConfiguredDomain(normalizedOrigin); const canonicalOrigin = this.canonicalizeHostedAppOriginForUid(normalizedOrigin); const localCachedAppUid = this.readLocalCanonicalAppUidFromCache(canonicalOrigin); if ( localCachedAppUid !== undefined ) { @@ -1333,7 +1334,10 @@ class AuthService extends BaseService { return redisCachedAppUid; } - const canonicalAppUid = await this.lookupCanonicalAppUidFromOrigin(canonicalOrigin); + const canonicalAppUid = await this.lookupCanonicalAppUidFromOrigin(canonicalOrigin, { + allowHostedOwnerlessLookup: isFirstPartyHostedOrigin, + restrictToOriginHost: isFirstPartyHostedOrigin, + }); this.writeLocalCanonicalAppUidToCache(canonicalOrigin, canonicalAppUid); try { await this.writeCanonicalAppUidToRedisCache(canonicalOrigin, canonicalAppUid); @@ -1410,14 +1414,15 @@ class AuthService extends BaseService { await this.invalidateCanonicalAppUidCacheForOrigins(canonicalOrigins); } - buildIndexUrlCandidatesFromOrigin (origin) { + buildIndexUrlCandidatesFromOrigin (origin, options = {}) { + const includeHostedAliases = options?.includeHostedAliases !== false; try { const parsedOrigin = new URL(origin); const hostCandidates = new Set(); hostCandidates.add(parsedOrigin.host.toLowerCase()); const hostedSubdomain = this.extractHostedAppSubdomainFromHostname(parsedOrigin.hostname); - if ( hostedSubdomain ) { + if ( hostedSubdomain && includeHostedAliases ) { const hostedDomainCandidates = this.getHostedAppDomainCandidatesForMatch(); for ( const hostedDomainCandidate of hostedDomainCandidates ) { if ( hostedDomainCandidate?.host ) { @@ -1519,8 +1524,31 @@ class AuthService extends BaseService { } } - async lookupCanonicalAppUidFromOrigin (origin) { - const indexUrlCandidates = this.buildIndexUrlCandidatesFromOrigin(origin); + isHostedOriginOnConfiguredDomain (origin) { + try { + const parsedOrigin = new URL(origin); + const hostedSubdomain = this.extractHostedAppSubdomainFromHostname(parsedOrigin.hostname); + if ( ! hostedSubdomain ) return false; + + const firstPartyHostedDomain = this.normalizeHostedDomainCandidate(this.global_config.domain); + if ( ! firstPartyHostedDomain?.hostname ) return false; + + const normalizedOriginHostname = parsedOrigin.hostname.trim().toLowerCase(); + return ( + normalizedOriginHostname === firstPartyHostedDomain.hostname || + normalizedOriginHostname.endsWith(`.${firstPartyHostedDomain.hostname}`) + ); + } catch { + return false; + } + } + + async lookupCanonicalAppUidFromOrigin (origin, options = {}) { + const allowHostedOwnerlessLookup = options?.allowHostedOwnerlessLookup === true; + const restrictToOriginHost = options?.restrictToOriginHost === true; + const indexUrlCandidates = this.buildIndexUrlCandidatesFromOrigin(origin, { + includeHostedAliases: !restrictToOriginHost, + }); if ( indexUrlCandidates.length === 0 ) return null; try { @@ -1528,6 +1556,13 @@ class AuthService extends BaseService { const hostedSubdomain = this.extractHostedAppSubdomainFromHostname(parsedOrigin.hostname); if ( hostedSubdomain ) { + if ( allowHostedOwnerlessLookup || this.isHostedOriginOnConfiguredDomain(origin) ) { + return await this.queryCanonicalAppUidForIndexUrlCandidates({ + indexUrlCandidates, + preferNonBootstrap: true, + }); + } + const hostedSubdomainOwnerUserId = await this.getHostedSubdomainOwnerUserId(hostedSubdomain); if ( ! hostedSubdomainOwnerUserId ) { return null; @@ -1633,6 +1668,8 @@ class AuthService extends BaseService { canonicalizeHostedAppOriginForUid (origin) { try { + if ( this.isHostedOriginOnConfiguredDomain(origin) ) return origin; + const parsedOrigin = new URL(origin); const hostedSubdomain = this.extractHostedAppSubdomainFromHostname(parsedOrigin.hostname); if ( ! hostedSubdomain ) return origin; diff --git a/src/backend/src/services/auth/AuthService.privateAssetToken.test.ts b/src/backend/src/services/auth/AuthService.privateAssetToken.test.ts index e212c5956..aa60433af 100644 --- a/src/backend/src/services/auth/AuthService.privateAssetToken.test.ts +++ b/src/backend/src/services/auth/AuthService.privateAssetToken.test.ts @@ -458,7 +458,39 @@ describe('AuthService private asset token helpers', () => { uid: canonicalUid, name: `music-player-${subdomain}`, title: `music-player-${subdomain}`, - indexUrl: `https://${subdomain}.puter.site/`, + indexUrl: `https://${subdomain}.puter.com/index.html`, + ownerUserId: owner.id, + }); + + const appUid = await authService.app_uid_from_origin(`https://${subdomain}.puter.com`); + + expect(appUid).toBe(canonicalUid); + }); + + it('prefers canonical first-party app for hosted subdomain without subdomain owner record', async () => { + const authService = createAuthService(); + authService.global_config.domain = 'puter.com'; + const owner = await insertUser(); + const subdomain = `music${Math.random().toString(36).slice(2, 9)}`; + const bootstrapUid = `app-bootstrap-${randomUUID()}`; + const canonicalUid = `app-canonical-${randomUUID()}`; + + await db.write( + 'INSERT INTO `apps` (`uid`, `name`, `title`, `description`, `index_url`, `owner_user_id`) VALUES (?, ?, ?, ?, ?, ?)', + [ + bootstrapUid, + bootstrapUid, + bootstrapUid, + `App created from origin https://${subdomain}.puter.com`, + `https://${subdomain}.puter.com`, + null, + ], + ); + await insertApp({ + uid: canonicalUid, + name: `music-player-${subdomain}`, + title: `music-player-${subdomain}`, + indexUrl: `https://${subdomain}.puter.com/`, ownerUserId: owner.id, }); @@ -521,7 +553,7 @@ describe('AuthService private asset token helpers', () => { expect(canonicalOrigins.filter(origin => origin === 'https://beans.puter.site')).toHaveLength(1); }); - it('derives same app uid for hosted app domain aliases', async () => { + it('keeps puter.com distinct while deriving same uid for non-domain hosted aliases', async () => { const authService = createAuthService(); authService.global_config.static_hosting_domain = 'puter.site'; authService.global_config.static_hosting_domain_alt = 'puter.host'; @@ -538,7 +570,7 @@ describe('AuthService private asset token helpers', () => { expect(uidSite).toBe(uidStaticAlt); expect(uidSite).toBe(uidPrivatePrimary); expect(uidSite).toBe(uidPrivateAlt); - expect(uidSite).toBe(uidMainDomain); + expect(uidMainDomain).not.toBe(uidSite); }); it('keeps distinct app uid per subdomain under hosted alias canonicalization', async () => {