Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
417ba5d
fix(home): serve wallpaper bytes with cross-origin-resource-policy
SashaMIT Sep 21, 2026
42f44f3
fix(home-gui): load wallpaper through the launch-token path
SashaMIT Sep 21, 2026
426948f
fix(library): keep preview status out of the toolbar
SashaMIT Sep 21, 2026
6cf258a
feat(library): lazy image thumbnails for image objects
SashaMIT Sep 21, 2026
2e51009
fix(home): let the active shell fetch the wallpaper it renders
SashaMIT Sep 21, 2026
f5ebb9e
fix(system): keep the window still after the picker; show the wallpap…
SashaMIT Sep 21, 2026
6412da5
fix(library): transient status lives in the footer
SashaMIT Sep 21, 2026
e8c794d
feat(home): set the desktop background from one of the person's own o…
SashaMIT Sep 21, 2026
c3ac261
feat(library): Set as Desktop Background from the context menu
SashaMIT Sep 21, 2026
63818af
Merge remote-tracking branch 'origin/develop' into feat/library-set-d…
SashaMIT Oct 16, 2026
e42be0c
fix(home): refuse a wallpaper whose declared size is too large to decode
claude Oct 17, 2026
f13396f
fix(library): thumbnails skip pictures too large to decode
claude Oct 17, 2026
06b6240
fix(home): set the wallpaper from Library only through Library's capa…
claude Oct 17, 2026
0ffbd2f
test(home): wallpaper source refuses traversal, encoded paths and links
claude Oct 17, 2026
756dad1
deps: move zip to 9.x for RUSTSEC-2026-0367/0368/0369
claude Oct 17, 2026
7e1b4d9
Merge fix/zip-9-rustsec into feat/library-set-desktop-background
claude Oct 17, 2026
1890c24
model-provider: held-lock journal test takes its lock with the bounde…
andersalm Oct 17, 2026
0f9bfd4
test(people): the first-run layout smoke waits for the rendered Profi…
claude Oct 17, 2026
e4b0a1a
fix(library): the wallpaper refuses protected bytes after a copy, ren…
claude Oct 17, 2026
9aef6b9
fix(home): Library's wallpaper intent is a single-use ticket, not its…
claude Oct 17, 2026
cee323e
fix(home): picture headers must be complete and well formed
claude Oct 17, 2026
581426c
fix(library): thumbnail headers must be complete and well formed
claude Oct 17, 2026
e64a325
fix(library): a protected publish records the bytes custody protected
claude Oct 17, 2026
6a90d14
fix(home): bound the legacy protection check and say how to recover
claude Oct 17, 2026
baaf15c
fix(library): show why Runtime refused a desktop background
claude Oct 17, 2026
bf96fa0
Merge develop into feat/library-set-desktop-background
claude Oct 18, 2026
c15d375
fix(library): limit pictures to 8192 px and 24 MP, bound thumbnails b…
claude Oct 18, 2026
8b0b810
fix(library): save a legacy digest only from a file with the publishe…
claude Oct 18, 2026
89d62f9
Use a platform-neutral app in Home link regression
SashaMIT Oct 17, 2026
0efcc74
Merge origin/develop into feat/library-set-desktop-background
claude Oct 18, 2026
f0b9e67
fix(library): save found digests only after the check answered in time
claude Oct 18, 2026
4df92a9
Merge origin/develop into feat/library-set-desktop-background
claude Oct 19, 2026
4f1bd73
test(auth): the limited principal-root read keeps its bound on develo…
claude Oct 19, 2026
a968ffc
test(auth): the limited principal-root read refuses links and non-reg…
claude Oct 19, 2026
7151dc8
fix(library): a plain message for an older protected file no file can…
claude Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions capsules/home-gui/browser/home-gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
toolbarInboxButton,
toolbarSignOutButton,
ensureHomeGuiDom,
fetchBlob,
initializeRecentTargets,
initializeShellLayout,
rememberSharedUiPreferences,
Expand Down Expand Up @@ -900,11 +901,52 @@ export function syncHomeGuiAppearance(summary) {
}
desktopBackdrop.dataset.overlay = overlayEnabled ? "true" : "false";
desktopBackdrop.style.setProperty("--desktop-overlay-opacity", String(overlayOpacity));
void applyDesktopWallpaper(imageUrl);
}

// The summary URL carries `v=` so it changes whenever the user uploads or
// resets the image; that string is the cache key for the loaded object URL.
const wallpaperLoad = { sourceUrl: "", objectUrl: "" };

function clearDesktopWallpaper() {
desktopBackdrop.style.removeProperty("--desktop-wallpaper");
if (wallpaperLoad.objectUrl) {
URL.revokeObjectURL(wallpaperLoad.objectUrl);
}
wallpaperLoad.objectUrl = "";
}

async function applyDesktopWallpaper(imageUrl) {
if (wallpaperLoad.sourceUrl === imageUrl) {
return;
}
wallpaperLoad.sourceUrl = imageUrl;
if (!imageUrl) {
desktopBackdrop.style.removeProperty("--desktop-wallpaper");
clearDesktopWallpaper();
return;
}
desktopBackdrop.style.setProperty("--desktop-wallpaper", `url("${imageUrl}")`);
let objectUrl = "";
try {
objectUrl = URL.createObjectURL(await fetchBlob(imageUrl));
} catch (error) {
// Fail closed to the default wallpaper instead of a broken image. The
// source URL stays recorded so summary refreshes do not retry until the
// Runtime advertises a new image version.
if (wallpaperLoad.sourceUrl === imageUrl) {
clearDesktopWallpaper();
}
console.warn("home-gui wallpaper unavailable", error);
return;
}
if (wallpaperLoad.sourceUrl !== imageUrl) {
URL.revokeObjectURL(objectUrl);
return;
}
if (wallpaperLoad.objectUrl) {
URL.revokeObjectURL(wallpaperLoad.objectUrl);
}
wallpaperLoad.objectUrl = objectUrl;
desktopBackdrop.style.setProperty("--desktop-wallpaper", `url("${objectUrl}")`);
}

function targetsChanged(previous, next) {
Expand Down
17 changes: 17 additions & 0 deletions capsules/home-gui/browser/shell-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,23 @@ export async function fetchJson(url, init) {
return response.json();
}

/**
* Fetch a binary Home resource with the launch token. Home GUI runs in an
* opaque sandboxed frame, so cookie-authenticated CSS `url()` loads cannot be
* relied on; this is the same authenticated path every other Home request uses.
*/
export async function fetchBlob(url) {
const response = await fetch(url, {
headers: homeGuiLaunchToken ? { "x-elastos-home-token": homeGuiLaunchToken } : {},
});
if (!response.ok) {
const error = new Error(`request failed: ${response.status} ${response.statusText}`);
error.status = response.status;
throw error;
}
return response.blob();
}

export function allVisibleTargets(summary) {
if (!summary || !Array.isArray(summary.targets)) {
return [];
Expand Down
63 changes: 63 additions & 0 deletions capsules/home/browser/home-shell-host.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ const SHELL_MESSAGE_DELIVER_TARGET_SOURCES = Object.freeze({
documents: new Set(["chat-room"]),
library: new Set(["archive-manager", "browser", "chat-room"]),
});
// Apps that may ask Home to make one of the person's own objects the wallpaper.
const SHELL_MESSAGE_DESKTOP_BACKGROUND_SOURCES = new Set(["library"]);
const DESKTOP_BACKGROUND_SOURCE_URI_MAX_LENGTH = 2048;
const PASSKEY_STEP_UP_TARGETS = new Set(["inbox", SYSTEM_APP_ID, "wallet"]);
const launchedAppContexts = new Map();
const pendingBrowserAuthorityRenewals = new Map();
Expand Down Expand Up @@ -1974,6 +1977,10 @@ window.addEventListener("message", (event) => {
});
return;
}
if (data.type === "home:set-desktop-background") {
handleSetDesktopBackgroundMessage(event, context, data);
return;
}
if (data.type === "home:deliver-to-target") {
const target = typeof data.target === "string" ? data.target.trim() : "";
if (!target || !canDeliverTargetFromHomeMessage(context, target)) {
Expand Down Expand Up @@ -2334,6 +2341,62 @@ function canOpenTargetFromHomeMessage(context, target) {
return policy.has(target);
}

// The app expresses intent; Home performs the change with its own appearance
// authority, so the app never gains one. Home carries only the single-use
// ticket the Library frame asked Runtime for (one object, one minute), never
// Library's launch token, and Runtime reads the object under Library's scope,
// Trash, and protection rules. Home never reads or decrypts the object itself.
const DESKTOP_BACKGROUND_TICKET_PATTERN = /^[A-Za-z0-9_-]{43}$/;
const DESKTOP_BACKGROUND_MESSAGE_MAX_LENGTH = 600;

function handleSetDesktopBackgroundMessage(event, context, data) {
const requestId = typeof data.requestId === "string" ? data.requestId.trim() : "";
const uri = typeof data.uri === "string" ? data.uri.trim() : "";
const ticket = typeof data.ticket === "string" ? data.ticket : "";
if (
context.kind !== "app-frame" ||
!SHELL_MESSAGE_DESKTOP_BACKGROUND_SOURCES.has(context.targetId) ||
!hasExactMessageKeys(data, ["type", "requestId", "homeToken", "uri", "ticket"]) ||
!requestId ||
requestId.length > 128 ||
!uri.startsWith("localhost://Users/") ||
uri.length > DESKTOP_BACKGROUND_SOURCE_URI_MAX_LENGTH ||
!DESKTOP_BACKGROUND_TICKET_PATTERN.test(ticket)
) {
console.warn("home ignored unauthorized set-desktop-background message", context.targetId);
replyToShellRequest(event, requestId, null, new Error("Home denied the desktop background change"));
return;
}
fetch("/api/apps/home/appearance/background-image", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ source_uri: uri, library_ticket: ticket }),
})
.then(async (response) => {
if (response.ok) {
requestShellSummaryRefresh({ reason: "desktop-background" });
replyToShellRequest(event, requestId, { accepted: true });
return;
}
// Runtime answers 400 with a plain sentence that tells the person what
// to do next (for example, which protected file to move back). Only
// that sentence goes back to Library; Library checks it again.
const detail = response.status === 400 ? (await response.text().catch(() => "")).trim() : "";
console.warn("home could not set the desktop background", response.status);
replyToShellRequest(
event,
requestId,
detail && detail.length <= DESKTOP_BACKGROUND_MESSAGE_MAX_LENGTH
? { accepted: false, message: detail }
: { accepted: false },
);
})
.catch((error) => {
console.warn("home could not set the desktop background", error);
replyToShellRequest(event, requestId, { accepted: false });
});
}

function canDeliverTargetFromHomeMessage(context, target) {
if (context.kind !== "app-frame" && context.kind !== "shell-frame") {
return false;
Expand Down
2 changes: 1 addition & 1 deletion capsules/library/browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ <h2 class="window-sidebar-title">Favorites</h2>
</section>
</div>
<div class="toolbar-right">
<span id="status-text" class="subtitle hidden">Loading Library...</span>
<div id="toolbar-search" class="toolbar-search">
<button
id="search-toggle-button"
Expand Down Expand Up @@ -87,6 +86,7 @@ <h2 class="window-sidebar-title">Favorites</h2>
<section id="content" class="content" data-view="list" aria-label="Items"></section>
<footer class="statusbar explorer-footer">
<span id="footer-left">0 items</span>
<span id="status-text" class="subtitle hidden" aria-live="polite">Loading Library...</span>
<span id="footer-right">On this device</span>
</footer>
</main>
Expand Down
21 changes: 21 additions & 0 deletions capsules/library/browser/library.css
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,19 @@
display: block;
}

/* Image files show their own thumbnail in place of the generic icon. */
.item--thumbnail .file-icon img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 3px;
}

.content[data-view="grid"] .item--thumbnail .file-icon {
overflow: hidden;
border-radius: 4px;
}

.item-name {
max-width: 100%;
overflow: hidden;
Expand Down Expand Up @@ -1009,6 +1022,14 @@
white-space: nowrap;
}

/* Transient status lives in the footer so it can never push toolbar
controls; it takes the spare middle space and truncates first. */
.statusbar #status-text {
flex: 1 1 auto;
text-align: center;
color: var(--muted);
}

.context-menu {
position: fixed;
z-index: 80;
Expand Down
13 changes: 13 additions & 0 deletions capsules/library/browser/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
childUri,
contentCid,
decimalIntegerToHexQuantity,
desktopBackgroundRefusalMessage,
hasCapability,
inTrash,
isArchiveObject,
Expand Down Expand Up @@ -35,6 +36,7 @@ export function createLibraryActions({
providerApi,
renderUploads,
selectedObjects,
setDesktopBackground,
setStatus,
setUploadProgress,
showMenuForObject,
Expand Down Expand Up @@ -266,6 +268,16 @@ export function createLibraryActions({
saveDownloadBlob(data.blob, data.filename || object.name || "download");
}

async function setObjectAsDesktopBackground(object) {
setStatus("Setting desktop background...");
const result = await setDesktopBackground(object.uri);
if (result?.accepted) {
setStatus("Desktop background updated.");
return;
}
setStatus(desktopBackgroundRefusalMessage(result) || "Could not set this image as the desktop background.");
}

async function downloadObjectAsZip(object) {
if (!isDirectory(object)) return;
const data = await downloadObjectRaw({ uri: object.uri, archive: "zip" });
Expand Down Expand Up @@ -731,6 +743,7 @@ export function createLibraryActions({
restoreObject,
restoreSelectedObjects,
setClipboard,
setObjectAsDesktopBackground,
shareObject,
showStatusObject,
trashObject,
Expand Down
51 changes: 51 additions & 0 deletions capsules/library/browser/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,56 @@ export function createLibraryRuntime({ getHomeToken }) {
});
}

// Library only expresses the intent; Home holds the appearance authority.
// Library first asks Runtime, with its own launch token, for a single-use
// ticket for this one object, and gives Home only that ticket, never its
// launch token. Resolves once Home has answered.
async function requestDesktopBackgroundTicket(uri, token) {
try {
const response = await fetch("/api/apps/library/desktop-background-ticket", {
method: "POST",
headers: {
"content-type": "application/json",
"x-elastos-home-token": token,
},
body: JSON.stringify({ source_uri: uri }),
});
const payload = await response.json().catch(() => ({}));
return response.ok && typeof payload.ticket === "string" ? payload.ticket : "";
} catch (_error) {
return "";
}
}

// Resolves to { accepted, message }. `message` is Runtime's reason for a
// refusal as Home passed it on (the caller checks it before showing it),
// or "" when there is none.
async function setDesktopBackground(uri) {
const requestId = crypto.randomUUID();
const token = getHomeToken();
const ticket = await requestDesktopBackgroundTicket(uri, token);
if (!ticket) return { accepted: false, message: "" };
return new Promise((resolve) => {
const finish = (accepted, message = "") => {
window.clearTimeout(timer);
window.removeEventListener("message", receive);
resolve({ accepted, message: accepted ? "" : message });
};
const receive = (event) => {
if (event.source !== window.top || event.origin !== homeParentOrigin ||
event.data?.type !== "home:shell-response" || event.data.requestId !== requestId) return;
const sameSession = getHomeToken() === token;
finish(
sameSession && event.data.result?.accepted === true && !event.data.error,
sameSession && typeof event.data.result?.message === "string" ? event.data.result.message : "",
);
};
const timer = window.setTimeout(() => finish(false), 20_000);
window.addEventListener("message", receive);
if (!shellMessage({ type: "home:set-desktop-background", uri, requestId, ticket })) finish(false);
});
}

function closeSelf() {
return shellMessage({ type: "home:close-self" });
}
Expand All @@ -276,6 +326,7 @@ export function createLibraryRuntime({ getHomeToken }) {
openTarget,
openPublishedUri,
deliverToTarget,
setDesktopBackground,
closeSelf,
};
}
13 changes: 13 additions & 0 deletions capsules/library/browser/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
hasCapability,
inTrash,
isBlockedObject,
isDesktopBackgroundCandidate,
isDirectory,
isRuntimeCustodyProtectableVideo,
isTrashRootUri,
Expand All @@ -27,6 +28,7 @@ import { createLibraryPreview } from "./preview.js";
import { createLibraryRealtime } from "./realtime.js";
import { createLibraryRenderer, iconPlaceholder } from "./render.js?v=library-20260711d";
import { createLibrarySelection } from "./selection.js";
import { createLibraryThumbnails } from "./thumbnails.js";
import {
MUTATING_PROVIDER_OPS,
cacheFolderListing,
Expand Down Expand Up @@ -72,6 +74,7 @@ import {
openTarget,
openPublishedUri,
deliverToTarget,
setDesktopBackground,
closeSelf,
} = createLibraryRuntime({ getHomeToken: () => state.homeToken });

Expand Down Expand Up @@ -145,6 +148,7 @@ import {
let restoreObject = async () => {};
let restoreSelectedObjects = async () => {};
let setClipboard = () => {};
let setObjectAsDesktopBackground = async () => {};
let shareObject = async () => {};
let showStatusObject = async () => {};
let trashObject = async () => {};
Expand Down Expand Up @@ -245,6 +249,10 @@ import {
perf,
selectedObjects,
state,
thumbnails: createLibraryThumbnails({
readObjectBlob: (uri) => downloadObjectRaw({ uri }).then((result) => result.blob),
root: elements.content,
}),
visibleObjects,
}));
({
Expand Down Expand Up @@ -297,6 +305,7 @@ import {
restoreObject,
restoreSelectedObjects,
setClipboard,
setObjectAsDesktopBackground,
shareObject,
showStatusObject,
trashObject,
Expand All @@ -320,6 +329,7 @@ import {
providerApi,
renderUploads,
selectedObjects,
setDesktopBackground,
setStatus,
setUploadProgress,
showMenuForObject,
Expand Down Expand Up @@ -991,6 +1001,9 @@ import {
children: viewers.map((viewer) => menuAction(viewer.label || viewer.id, () => openWithViewer(object, viewer.id))),
}));
}
if (!isAttachMode() && isDesktopBackgroundCandidate(object)) {
actions.push(menuAction("Set as Desktop Background", () => setObjectAsDesktopBackground(object)));
}
}
if (hasCapability(object, "download")) {
actions.push(menuAction("Download", () => downloadObject(object)));
Expand Down
Loading
Loading