Mark external apps and use the flag for dashboard titles

An app with no owner_user_id (null/empty) isn't owned by a Puter user —
it's an external, origin-bootstrapped app whose uuid/name/title are all the
opaque app-… id. The dashboard used to detect these client-side by comparing
uuid/name/title and a name.startsWith('app-') check.

Expose an authoritative `external` flag from the API instead:

- /installedApps and /get-launch-apps now return `external`, derived from
  owner_user_id, and no longer leak the raw owner id.
- The dashboard (Home + Apps tabs) shows the index_url hostname for external
  apps based on `external` rather than the uuid/name/title heuristic.

Add/extend extension tests to cover the `external` flag and non-leak.
This commit is contained in:
jelveh
2026-07-06 17:05:00 -07:00
parent fbb646b8eb
commit acdc91a271
5 changed files with 59 additions and 33 deletions
+29
View File
@@ -138,6 +138,35 @@ describe('installedApps extension — handleInstalledApps', () => {
expect(Object.prototype.hasOwnProperty.call(list[0], 'iconUrl')).toBe(
true,
);
// An owned app is not external, and the raw owner id must not leak.
expect(list[0].external).toBe(false);
expect(
Object.prototype.hasOwnProperty.call(list[0], 'owner_user_id'),
).toBe(false);
});
it('flags apps with no owner_user_id as external', async () => {
const user = await seedUser();
const slug = Math.random().toString(36).slice(2, 8);
// createFromOrigin bootstraps an app with owner_user_id = null.
const app = await server.stores.app.createFromOrigin(
`app-${slug}`,
`https://external-${slug}.example.com`,
);
await grantInstalled(app!.id as number, user.id as number);
const { res, captured } = makeRes();
await runWithContext(
{ actor: { user: { uuid: user.uuid, id: user.id as number } } },
() => handleInstalledApps(makeReq({}), res),
);
const list = captured.body as Array<Record<string, unknown>>;
expect(list).toHaveLength(1);
expect(list[0].external).toBe(true);
expect(
Object.prototype.hasOwnProperty.call(list[0], 'owner_user_id'),
).toBe(false);
});
it('clamps page/limit to safe ranges (page>=1, 1<=limit<=100)', async () => {
+13 -4
View File
@@ -50,6 +50,7 @@ export const handleInstalledApps = async (
apps.description,
apps.icon,
apps.index_url,
apps.owner_user_id,
MIN(perm.dt) AS installed_at
FROM apps
LEFT JOIN user_to_app_permissions AS perm ON apps.id = perm.app_id
@@ -63,10 +64,18 @@ export const handleInstalledApps = async (
const apiBaseUrl = extension.config.api_base_url as string | undefined;
res.json(
installedApps.map((app) => ({
...app,
iconUrl: getAppIconUrl(app, { apiBaseUrl }),
})),
installedApps.map((app) => {
// An app with no owner_user_id (null/empty) isn't owned by a Puter
// user — it's an "external" app. Derive a flag and don't leak the
// raw owner id to the client.
const { owner_user_id, ...rest } = app;
const external = owner_user_id == null || owner_user_id === '';
return {
...rest,
iconUrl: getAppIconUrl(app, { apiBaseUrl }),
external,
};
}),
);
};
@@ -158,8 +158,7 @@ export class LegacyFSController extends PuterController {
router.get('/get-launch-apps', apiOptions, async (req, res) => {
const recommendedSvc = this.services.recommendedApps as unknown as
| { getRecommendedApps?: () => Promise<unknown[]> }
| undefined;
{ getRecommendedApps?: () => Promise<unknown[]> } | undefined;
const recommended = recommendedSvc?.getRecommendedApps
? await recommendedSvc.getRecommendedApps()
: [];
@@ -194,6 +193,11 @@ export class LegacyFSController extends PuterController {
godmode: Boolean(app.godmode),
maximize_on_start: Boolean(app.maximize_on_start),
index_url: app.index_url,
// An app with no owner isn't owned by a Puter user —
// it's an "external" (origin-bootstrapped) app.
external:
app.owner_user_id == null ||
app.owner_user_id === '',
});
}
}
@@ -555,9 +559,7 @@ export class LegacyFSController extends PuterController {
// Trash, and `null`/`{}` when restoring. See
// `src/gui/src/helpers.js` → `window.move_items`.
newMetadata: (body.new_metadata ?? undefined) as
| Record<string, unknown>
| null
| undefined,
Record<string, unknown> | null | undefined,
});
const oldPath = source.path;
await this.#emitGuiEvent('outer.gui.item.moved', moved, {
@@ -973,8 +975,7 @@ export class LegacyFSController extends PuterController {
}
type SignedOrEmpty =
| (SignedFile & { path?: string })
| Record<string, never>;
(SignedFile & { path?: string }) | Record<string, never>;
const result: { signatures: SignedOrEmpty[]; token?: string } = {
signatures: [],
};
@@ -1515,10 +1516,7 @@ export class LegacyFSController extends PuterController {
const subjectRef = body.subject;
const appRef = body.app;
const mode = (getString(body, 'mode') ?? 'read') as
| 'see'
| 'list'
| 'read'
| 'write';
'see' | 'list' | 'read' | 'write';
if (!subjectRef || !appRef)
throw new HttpError(400, '`subject` and `app` are required', {
legacyCode: 'bad_request',
+5 -10
View File
@@ -47,16 +47,10 @@ function buildAppsGrid (apps) {
for ( const app of apps ) {
let title = (app.title || app.name || '').trim();
// Anonymous apps report an opaque id (uuid === name === title, all
// starting with 'app-'); show the hostname of index_url instead,
// matching the Home tab.
const appUid = app.uid || app.uuid;
if (
app.name === app.title &&
app.name === appUid &&
app.name?.startsWith('app-') &&
app.index_url
) {
// External apps (not owned by a Puter user) report an opaque app-… id
// as their title; show the hostname of index_url instead, matching the
// Home tab.
if (app.external && app.index_url) {
title = new URL(app.index_url).hostname;
}
@@ -307,6 +301,7 @@ const TabApps = {
title: app.title,
uid: app.uuid || app.uid || null,
index_url: app.index_url || null,
external: app.external ?? false,
iconUrl: app.iconUrl || app.icon || null,
}));
+3 -8
View File
@@ -28,14 +28,9 @@ function buildRecentAppsHTML() {
// Show up to 6 recent apps (2 columns x 3 rows)
const recentApps = window.launch_apps.recent.slice(0, 6);
for (const app_info of recentApps) {
// if title, name and uuid are the same and start with 'app-' and index_url
// is set, then show the hostname of index_url instead of the opaque app id
if (
app_info.name === app_info.title &&
app_info.name === app_info.uuid &&
app_info.name?.startsWith('app-') &&
app_info.index_url
) {
// External apps (not owned by a Puter user) report an opaque app-… id
// as their title; show the hostname of index_url instead.
if (app_info.external && app_info.index_url) {
app_info.title = new URL(app_info.index_url).hostname;
app_info.target_link = app_info.index_url;
}