fix: origin app Ids (#2801)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled

This commit is contained in:
Daniel Salazar
2026-04-10 13:47:53 -07:00
committed by GitHub
parent 9c38c43f82
commit 7f7c01956e
2 changed files with 77 additions and 8 deletions
+42 -5
View File
@@ -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;
@@ -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 () => {