mirror of
https://github.com/HeyPuter/puter.git
synced 2026-07-12 18:17:55 +00:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user