From 68f34f0f0ccbdf489778b8a93b69ffe78e4944f5 Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Fri, 14 Aug 2026 14:37:17 -0400 Subject: [PATCH] fix(share): accept tilde-rooted paths like the FS routes do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK resolves relative paths to ~/..., but the share routes never expanded the tilde — a ~-prefixed string was read as a uid and every relative-path call 404'd. Item parsing now treats ~ as path-shaped and expands it to the actor's home with the same helper the legacy FS routes use, on share, revoke, and the shares listing. Co-Authored-By: Claude Fable 5 --- .../share/ShareController.http.test.ts | 33 +++++++++++++++++++ .../controllers/share/ShareController.ts | 22 +++++++++---- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/backend/controllers/share/ShareController.http.test.ts b/src/backend/controllers/share/ShareController.http.test.ts index bfedd6955..cd69f17fb 100644 --- a/src/backend/controllers/share/ShareController.http.test.ts +++ b/src/backend/controllers/share/ShareController.http.test.ts @@ -158,6 +158,39 @@ describe('share endpoints over HTTP', () => { } }); + it('accepts tilde-rooted paths the way the FS routes do', async () => { + const owner = env.users.user; + const recipient = env.users.other; + const file = await makeFile(owner); + const tildePath = `~/${file.path.split('/').pop()}`; + + const shareRes = await post('/share', owner.token, { + recipients: [recipient.username], + items: [tildePath], + mode: 'read', + }); + expect(shareRes.status).toBe(200); + expect(await shareRes.json()).toMatchObject({ status: 'success' }); + + const listRes = await get('/share/shares', owner.token, { + path: tildePath, + }); + expect(listRes.status).toBe(200); + const listed = (await listRes.json()) as { + items: Array<{ holder: string }>; + }; + expect( + listed.items.some((i) => i.holder === recipient.username), + ).toBe(true); + + const revokeRes = await post('/share/revoke', owner.token, { + recipients: [recipient.username], + items: [tildePath], + }); + expect(revokeRes.status).toBe(200); + expect(await revokeRes.json()).toMatchObject({ revoked: 1 }); + }); + it('reports per-pair outcomes when only some recipients resolve', async () => { const owner = env.users.user; const file = await makeFile(owner); diff --git a/src/backend/controllers/share/ShareController.ts b/src/backend/controllers/share/ShareController.ts index d68a2e658..9bc2bc9e9 100644 --- a/src/backend/controllers/share/ShareController.ts +++ b/src/backend/controllers/share/ShareController.ts @@ -26,6 +26,7 @@ import type { ShareRecipient, ShareTarget, } from '../../services/share/ShareService.js'; +import { expandTildePath } from '../../services/fs/resolveNode.js'; import { runWithConcurrencyLimitSettled } from '../../util/concurrency.js'; import { normalizeLimit } from '../../util/pagination.js'; import { PuterController } from '../types.js'; @@ -101,7 +102,7 @@ export class ShareController extends PuterController { const actor = this.#requireActor(req); const body = this.#body(req); const recipients = this.#recipients(body); - const items = this.#items(body); + const items = this.#items(body, actor); const mode = typeof body.mode === 'string' ? body.mode : 'read'; // Every (recipient, item) pair is a distinct (holder, entry) key, so @@ -175,7 +176,7 @@ export class ShareController extends PuterController { const actor = this.#requireActor(req); const body = this.#body(req); const recipients = this.#recipients(body); - const items = this.#items(body); + const items = this.#items(body, actor); const pairs = recipients.flatMap((recipient) => items.map((item) => ({ recipient, item })), @@ -262,7 +263,8 @@ export class ShareController extends PuterController { const query = this.#query(req); const target: ShareTarget = {}; if (typeof query.uid === 'string') target.uid = query.uid; - if (typeof query.path === 'string') target.path = query.path; + if (typeof query.path === 'string') + target.path = expandTildePath(query.path, actor.user?.username); if (!target.uid && !target.path) { throw new HttpError(400, 'one of `uid` or `path` is required', { legacyCode: 'bad_request', @@ -358,7 +360,8 @@ export class ShareController extends PuterController { return out; } - #items(body: Record): ShareTarget[] { + #items(body: Record, actor: Actor): ShareTarget[] { + const username = actor.user?.username; const raw = body.items ?? body.item ?? body.path ?? body.uid; const list = Array.isArray(raw) ? raw : [raw]; const out: ShareTarget[] = []; @@ -366,14 +369,21 @@ export class ShareController extends PuterController { if (typeof entry === 'string') { const value = entry.trim(); if (!value) continue; + // Tilde-rooted strings are paths, as the legacy FS routes treat them. + const isPath = value.startsWith('/') || value.startsWith('~'); out.push( - value.startsWith('/') ? { path: value } : { uid: value }, + isPath + ? { path: expandTildePath(value, username) } + : { uid: value }, ); continue; } if (entry && typeof entry === 'object') { const item = entry as Record; - const path = typeof item.path === 'string' ? item.path : ''; + const path = + typeof item.path === 'string' + ? expandTildePath(item.path, username) + : ''; const uid = typeof item.uid === 'string' ? item.uid : ''; if (path || uid) out.push(path ? { path } : { uid }); }