Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 58a799251c fix: resolve manifest asset URLs for tarball-deployed applications
https://sonarly.com/issue/33657?type=bug

Tarball-deployed application logos and screenshots render as broken images (404) because the tarball upload path stores the raw SDK-relative manifest path (e.g. `public/linear-logomark.svg`) without resolving it to an absolute URL, unlike the NPM and dev paths which call `resolveManifestAssetUrls()`.

Fix: **Problem:** Tarball-deployed application logos render as broken images because the tarball upload path stores the raw SDK-relative manifest path (e.g. `public/linear-logomark.svg`) in the application registration, and the install service copies this raw path to the `application.logo` column without resolving it to an absolute URL. The NPM and dev paths both call `resolveManifestAssetUrls()` to convert relative paths to absolute URLs, but the install-time sync path does not.

**Fix (2 files):**

1. **`application-sync.service.ts`** — In the `syncApplication` method (called for all deploy paths: NPM, dev, tarball), resolve the manifest's asset URLs to absolute server URLs before storing `logo` on the install record. This uses the same `resolveManifestAssetUrls` utility already used by the NPM catalog sync and dev resolver, with the same URL pattern (`<SERVER_URL>/public-assets/<workspaceId>/<applicationId>/<filePath>`). The utility safely preserves already-absolute URLs, so NPM apps with CDN URLs are unaffected.

2. **`SettingsApplicationDetails.tsx`** — Reorder the logo resolution fallback chain so `application?.logo` (the resolved install-record URL) takes precedence over `app?.logoUrl` (the raw manifest path from the shared registration). Previously `app?.logoUrl` won first, which for tarball apps contained the unresolved raw path.

This implements Option A from the issue: resolve at install time, store on the install record. It's the minimal fix that addresses the immediate user-visible bug (broken logos for tarball-deployed apps) while being safe for all existing deploy paths.

**Not addressed (acknowledged out of scope in the issue):**
- Screenshots from tarball manifests still have raw paths (would need a new column or a `@ResolveField` — Option B from the issue)
- The dev path still bakes workspace-scoped URLs into the shared registration manifest (harmless in single-workspace dev, but architecturally incorrect)
- Pre-install marketplace browse page still shows raw paths for tarball apps (needs Option C — registration-scoped public assets)
2026-05-03 06:18:22 +00:00
2 changed files with 13 additions and 2 deletions
@@ -89,8 +89,8 @@ export const SettingsApplicationDetails = () => {
app?.displayName ?? application?.name ?? t`Application details`;
const description = app?.description ?? resolvedDescription;
const logoUrl =
app?.logoUrl ??
application?.logo ??
app?.logoUrl ??
application?.applicationRegistration?.logoUrl ??
undefined;
@@ -16,8 +16,10 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/application/application-variable/application-variable.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -37,6 +39,7 @@ export class ApplicationSyncService {
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fileStorageService: FileStorageService,
private readonly twentyConfigService: TwentyConfigService,
) {}
public async synchronizeFromManifest({
@@ -156,10 +159,18 @@ export class ApplicationSyncService {
const resolvedRegistrationId =
applicationRegistrationId ?? application.applicationRegistrationId;
const serverUrl = this.twentyConfigService.get('SERVER_URL');
const resolvedManifest = resolveManifestAssetUrls(
manifest,
(filePath) =>
`${serverUrl}/public-assets/${workspaceId}/${application.id}/${filePath}`,
);
return await this.applicationService.update(application.id, {
name,
description: manifest.application.description,
logo: manifest.application.logoUrl ?? null,
logo: resolvedManifest.application.logoUrl ?? null,
version: packageJson.version,
packageJsonChecksum: manifest.application.packageJsonChecksum,
yarnLockChecksum: manifest.application.yarnLockChecksum,