diff --git a/src/gui/src/helpers/appendSignedItemParams.js b/src/gui/src/helpers/appendSignedItemParams.js
new file mode 100644
index 000000000..0c03b9176
--- /dev/null
+++ b/src/gui/src/helpers/appendSignedItemParams.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * Append a signed file's `puter.item.*` params onto the launch iframe URL.
+ *
+ * @param {URLSearchParams} searchParams the iframe URL's search params
+ * @param {object} file_signature the /sign or /open_item signature object
+ * @param {string} itemPath the (privacy-aware) path to advertise to the app
+ */
+export const append_signed_item_params = (searchParams, file_signature, itemPath) => {
+ searchParams.append('puter.item.uid', file_signature.uid);
+ searchParams.append('puter.item.path', itemPath);
+ searchParams.append('puter.item.name', file_signature.fsentry_name);
+ searchParams.append('puter.item.read_url', file_signature.read_url);
+ // Read-only shares carry no write_url; appending undefined stringifies it to "undefined", a truthy invalid URL that stops the editor opening the file.
+ if ( file_signature.write_url ) {
+ searchParams.append('puter.item.write_url', file_signature.write_url);
+ }
+ searchParams.append('puter.item.metadata_url', file_signature.metadata_url);
+ searchParams.append('puter.item.size', file_signature.fsentry_size);
+ searchParams.append('puter.item.accessed', file_signature.fsentry_accessed);
+ searchParams.append('puter.item.modified', file_signature.fsentry_modified);
+ searchParams.append('puter.item.created', file_signature.fsentry_created);
+};
diff --git a/src/gui/src/helpers/appendSignedItemParams.test.js b/src/gui/src/helpers/appendSignedItemParams.test.js
new file mode 100644
index 000000000..290dfe680
--- /dev/null
+++ b/src/gui/src/helpers/appendSignedItemParams.test.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { describe, it, expect } from 'vitest';
+import { append_signed_item_params } from './appendSignedItemParams.js';
+
+const readOnlySignature = {
+ uid: 'uid-1',
+ fsentry_name: 'shared.txt',
+ read_url: 'https://api.puter.test/file?uid=uid-1&signature=read',
+ // write_url intentionally absent — the backend strips it for a read-only share.
+ metadata_url: 'https://api.puter.test/itemMetadata?uid=uid-1&signature=read',
+ fsentry_size: 12,
+ fsentry_accessed: 1,
+ fsentry_modified: 2,
+ fsentry_created: 3,
+ path: '/owner/shared.txt',
+};
+
+describe('append_signed_item_params', () => {
+ // Appending an absent write_url stringifies it to a truthy, invalid "undefined" that stops the editor opening; it must be omitted.
+ it('omits write_url for a read-only share instead of forwarding "undefined"', () => {
+ const params = new URLSearchParams();
+ append_signed_item_params(params, readOnlySignature, '/owner/shared.txt');
+
+ expect(params.has('puter.item.write_url')).toBe(false);
+ expect(params.get('puter.item.write_url')).toBeNull();
+ expect(params.get('puter.item.read_url')).toBe(readOnlySignature.read_url);
+ });
+
+ it('forwards write_url when the share grants write', () => {
+ const params = new URLSearchParams();
+ append_signed_item_params(
+ params,
+ { ...readOnlySignature, write_url: 'https://api.puter.test/writeFile?uid=uid-1&signature=write' },
+ '/owner/shared.txt',
+ );
+
+ expect(params.get('puter.item.write_url')).toBe(
+ 'https://api.puter.test/writeFile?uid=uid-1&signature=write',
+ );
+ });
+
+ it('advertises the passed path rather than the signature path', () => {
+ const params = new URLSearchParams();
+ append_signed_item_params(params, readOnlySignature, '~/shared.txt');
+ expect(params.get('puter.item.path')).toBe('~/shared.txt');
+ });
+});
diff --git a/src/gui/src/helpers/launchApp.js b/src/gui/src/helpers/launchApp.js
index c51083fa3..4d3b3ff48 100644
--- a/src/gui/src/helpers/launchApp.js
+++ b/src/gui/src/helpers/launchApp.js
@@ -21,6 +21,7 @@ import path from '../lib/path.js';
import { PROCESS_IPC_ATTACHED, PROCESS_RUNNING, PortalProcess, PseudoProcess } from '../definitions.js';
import UIWindow from '../UI/UIWindow.js';
import { starts_hidden } from './startsHidden.js';
+import { append_signed_item_params } from './appendSignedItemParams.js';
const normalizePrivateAccessDecision = (privateAccess) => {
if ( !privateAccess || typeof privateAccess !== 'object' ) {
@@ -463,16 +464,11 @@ const launch_app = async (options) => {
}
if ( file_signature ) {
- iframe_url.searchParams.append('puter.item.uid', file_signature.uid);
- iframe_url.searchParams.append('puter.item.path', options.file_path ? privacy_aware_path(options.file_path) : file_signature.path);
- iframe_url.searchParams.append('puter.item.name', file_signature.fsentry_name);
- iframe_url.searchParams.append('puter.item.read_url', file_signature.read_url);
- iframe_url.searchParams.append('puter.item.write_url', file_signature.write_url);
- iframe_url.searchParams.append('puter.item.metadata_url', file_signature.metadata_url);
- iframe_url.searchParams.append('puter.item.size', file_signature.fsentry_size);
- iframe_url.searchParams.append('puter.item.accessed', file_signature.fsentry_accessed);
- iframe_url.searchParams.append('puter.item.modified', file_signature.fsentry_modified);
- iframe_url.searchParams.append('puter.item.created', file_signature.fsentry_created);
+ append_signed_item_params(
+ iframe_url.searchParams,
+ file_signature,
+ options.file_path ? privacy_aware_path(options.file_path) : file_signature.path,
+ );
}
else if ( options.readURL ) {
iframe_url.searchParams.append('puter.item.name', options.filename);