mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-03 16:10:31 +00:00
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
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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user