<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[REST API broken since last update]]></title><description><![CDATA[<p dir="auto">I had been using Cubby via its REST API, without any issues until July 18th, before the last update.<br />
But now, i'm getting Auth errors everytime.<br />
As far as i can remember, i had used an App Password (scoped to Cubby) earlier.</p>
]]></description><link>https://forum.cloudron.io/topic/15809/rest-api-broken-since-last-update</link><generator>RSS for Node</generator><lastBuildDate>Mon, 17 Aug 2026 16:43:54 GMT</lastBuildDate><atom:link href="https://forum.cloudron.io/topic/15809.rss" rel="self" type="application/rss+xml"/><pubDate>Sat, 08 Aug 2026 11:00:16 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to REST API broken since last update on Tue, 11 Aug 2026 10:42:58 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/james" aria-label="Profile: james">@<bdi>james</bdi></a> <a href="/post/128029">said</a>:</p>
<p dir="auto">So what you need is a way for users in Cubby to create auth tokens that can be used with the API, right?</p>
</blockquote>
<p dir="auto">Yes, essentially.</p>
]]></description><link>https://forum.cloudron.io/post/128031</link><guid isPermaLink="true">https://forum.cloudron.io/post/128031</guid><dc:creator><![CDATA[shrey]]></dc:creator><pubDate>Tue, 11 Aug 2026 10:42:58 GMT</pubDate></item><item><title><![CDATA[Reply to REST API broken since last update on Tue, 11 Aug 2026 10:15:03 GMT]]></title><description><![CDATA[<p dir="auto">Hello <a class="plugin-mentions-user plugin-mentions-a" href="/user/shrey" aria-label="Profile: shrey">@<bdi>shrey</bdi></a><br />
So what you need is a way for users in Cubby to create auth tokens that can be used with the API, right?<br />
This would be a feature request.</p>
]]></description><link>https://forum.cloudron.io/post/128029</link><guid isPermaLink="true">https://forum.cloudron.io/post/128029</guid><dc:creator><![CDATA[james]]></dc:creator><pubDate>Tue, 11 Aug 2026 10:15:03 GMT</pubDate></item><item><title><![CDATA[Reply to REST API broken since last update on Tue, 11 Aug 2026 09:46:07 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/nebulon" aria-label="Profile: nebulon">@<bdi>nebulon</bdi></a> <a href="/post/128023">said</a>:</p>
<p dir="auto">Please do not dump AI output directly into the forum.</p>
</blockquote>
<p dir="auto">Fair enough.</p>
<hr />
<p dir="auto">My use-case: Headless, auth enabled filestore with link-based sharing mechanism, with a decent admin GUI. Cubby seems to fulfil most of this, except for the 'headless automation as a first-class feature' aspect. Basically, upload files/folders, and generate unique, revocable and individual share links for them, as required. Currently, i'm managing those links in another online spreadsheet tool.</p>
<p dir="auto">Btw, i got around to Cubby after i searched for a solution in the Cloudron App Store, and couldn't identify any useful-enough option.</p>
<p dir="auto">OIDC is going to be tricky due to the interactive nature of the auth mechanism, assuming that the access token issued there are short-lived.</p>
<p dir="auto">So yeah, an ideal solution would be a long-lived access token/API key that allows for full programmatic access to the REST API</p>
]]></description><link>https://forum.cloudron.io/post/128028</link><guid isPermaLink="true">https://forum.cloudron.io/post/128028</guid><dc:creator><![CDATA[shrey]]></dc:creator><pubDate>Tue, 11 Aug 2026 09:46:07 GMT</pubDate></item><item><title><![CDATA[Reply to REST API broken since last update on Tue, 11 Aug 2026 08:48:38 GMT]]></title><description><![CDATA[<p dir="auto">Please do not dump AI output directly into the forum.</p>
<p dir="auto">From what I got there is that the root cause is that you are createing the tokens via direct database access. That is bound to break eventually when we restructure internals. I think the correct solution would be to provide a way to create and manage api tokens via the UI. However first we have to see if this is an actual use-case for cubby to be honest. Maybe you can describe the usecase you have here as for example the mobile app we are working on on the side uses the OpenID login flow to obtain a token. That would be the best way for other UIs using the API, but not sure if this applies to your usage?</p>
]]></description><link>https://forum.cloudron.io/post/128023</link><guid isPermaLink="true">https://forum.cloudron.io/post/128023</guid><dc:creator><![CDATA[nebulon]]></dc:creator><pubDate>Tue, 11 Aug 2026 08:48:38 GMT</pubDate></item><item><title><![CDATA[Reply to REST API broken since last update on Tue, 11 Aug 2026 07:20:39 GMT]]></title><description><![CDATA[<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/nebulon" aria-label="Profile: nebulon">@<bdi>nebulon</bdi></a><br />
Compiled with assistance from Claude:</p>
<hr />
<p dir="auto">You're right that the auth <em>code path</em> is unchanged. Nothing about how Cubby resolves a credential changed. What changed is <strong>how long a token survives</strong>, and that's enough to break Bearer-authenticated API use.</p>
<p dir="auto">Answering your questions directly first.</p>
<h3>What I was actually doing, since that seems unexpected</h3>
<p dir="auto">I've been driving Cubby's REST API server-to-server since late June, as a headless file store for an automation pipeline. A dedicated service user owns one folder; that folder has a single read-only share, so published files are readable over a public link while writes stay authenticated. There is no browser and no user session anywhere in this — just an HTTP client sending <code>Authorization: Bearer &lt;token&gt;</code>, where the token is a row in the <code>tokens</code> table, resolved by <code>getUserFromToken</code> → <code>users.getByAccessToken</code> → <code>tokens.get</code>. <strong>Not</strong> the OpenID access token.</p>
<p dir="auto">Endpoints in use, all with that same Bearer credential:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Purpose</th>
<th>Call</th>
</tr>
</thead>
<tbody>
<tr>
<td>Write / overwrite a file</td>
<td><code>POST /api/v1/files?path=/home/&lt;folder&gt;/&lt;name&gt;&amp;overwrite=true</code>, raw binary body with the real <code>Content-Type</code></td>
</tr>
<tr>
<td>Create a folder</td>
<td><code>POST /api/v1/files?path=&lt;path&gt;&amp;directory=true</code></td>
</tr>
<tr>
<td>Read, list, delete, rename</td>
<td><code>GET</code> / <code>DELETE</code> / relocate on <code>/api/v1/files</code></td>
</tr>
<tr>
<td>Publish a per-file link</td>
<td><code>POST /api/v1/shares</code> with <code>{ path, readonly: true, ownerUsername: &lt;service user&gt; }</code></td>
</tr>
<tr>
<td>Revoke a link</td>
<td><code>DELETE /api/v1/shares?shareId=&lt;id&gt;</code></td>
</tr>
<tr>
<td>Public read of a published file</td>
<td><code>GET /api/v1/shares/&lt;shareId&gt;?type=raw</code> — no auth, and this still works fine</td>
</tr>
</tbody>
</table>
<p dir="auto">That last row is why the app kept looking healthy: reads go through the share and never touch the token. Only the authenticated write path broke.</p>
<p dir="auto">The failing request, from my client's execution log, 2026-08-08T08:41:15Z:</p>
<pre><code>POST https://&lt;my-cubby&gt;/api/v1/files?path=/home/&lt;folder&gt;/&lt;file&gt;.svg&amp;overwrite=true
Content-Type: image/svg+xml
Content-Length: 199
Authorization: Bearer &lt;redacted&gt;

→ 403 {"status": "Forbidden", "message": "not allowed"}
</code></pre>
<h3>Do I see errors in backend or frontend</h3>
<p dir="auto">No. There is no frontend involved — this is server-to-server. And the backend logs nothing, because a request bearing a deleted token isn't treated as an error at all; it's treated as an anonymous request and rejected in path resolution. <code>debugLog</code> is off without <code>DEBUG</code> set, so nothing is written.</p>
<p dir="auto">I ran the same request with <strong>no credential at all</strong> and got a byte-identical 403. So an expired credential and an absent credential are indistinguishable to the client and invisible on the server. That's what made this hard to diagnose: my first assumption was a permissions or share-ownership problem, and my initial post here blamed the wrong credential type as a result.</p>
<h3>What changed</h3>
<p dir="auto">Commit <strong><code>00c42b6681d57b1a6428ad4e458fa2c1285bc937</code></strong> — "Add wopi lock support for collaboration", 2026-07-21T07:14:42Z.<br />
<a href="https://github.com/getcubby/cubby/commit/00c42b6681d57b1a6428ad4e458fa2c1285bc937" target="_blank" rel="noopener noreferrer nofollow ugc">https://github.com/getcubby/cubby/commit/00c42b6681d57b1a6428ad4e458fa2c1285bc937</a></p>
<p dir="auto">Alongside the WOPI lock work, it added to <code>backend/tokens.js</code>:</p>
<pre><code class="language-js">async function cleanup(maxAgeMs) {
    const cutoff = new Date(Date.now() - maxAgeMs);
    await database.query('DELETE FROM tokens WHERE created_at &lt; $1', [ cutoff ]);
}
</code></pre>
<p dir="auto">and wired it up in <code>backend/server.js</code>:</p>
<pre><code class="language-js">const TOKEN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
...
tokens.cleanup(TOKEN_MAX_AGE_MS)                    // at boot
gTokenCleanupInterval = setInterval(() =&gt; {
    tokens.cleanup(TOKEN_MAX_AGE_MS)
}, 60 * 60 * 1000);                                 // then hourly
</code></pre>
<p dir="auto"><code>cleanup()</code> did not exist before this commit. <code>backend/tokens.js</code> has nine commits in its entire history; the one before this is "Port to esm" from 2026-02-25, which was mechanical.</p>
<p dir="auto">I understand <em>why</em> it's there — <code>getHandle</code> calls <code>tokens.add()</code> on every open and every collaborator join, so an office session accumulates rows quickly and something has to reap them. That's reasonable. The issue is that <code>tokens</code> is one undifferentiated table, and <code>add()</code> writes only <code>(id, username)</code> — there's no column recording what a token is for, so <code>cleanup()</code> can only sweep everything. WOPI handles and API tokens are the same row shape, so both go.</p>
<h3>Which release it shipped in</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>When (UTC)</th>
<th>What</th>
</tr>
</thead>
<tbody>
<tr>
<td>2026-07-02 08:39</td>
<td>package version → <strong>2.15.1</strong></td>
</tr>
<tr>
<td>2026-07-21 07:14</td>
<td><code>00c42b6</code> — reaper added</td>
</tr>
<tr>
<td>2026-07-21 16:17</td>
<td><code>dc23534</code> — further WOPI work (touches <code>tokens.js</code>, but does not alter <code>cleanup</code>)</td>
</tr>
<tr>
<td>2026-07-21 16:38</td>
<td>Renovate bumps app digest to <code>56b1426</code></td>
</tr>
<tr>
<td>2026-07-21 17:27</td>
<td>package version → <strong>2.16.0</strong></td>
</tr>
</tbody>
</table>
<p dir="auto">So the reaper is in 2.16.0 and in no earlier release.</p>
<p dir="auto"><strong>Where my token came from, since it's relevant.</strong> I minted it on 28 June by inserting the row directly, inside the container via <code>cloudron exec</code>, with a small script that did <code>crypto.randomBytes(32).toString('hex')</code> and then <code>INSERT INTO tokens (id, username) VALUES ($1, $2)</code>. That is the same statement <code>tokens.add()</code> runs, so the row is indistinguishable from one issued through the mobile flow — but I should be straight that I put it there myself rather than obtaining it through a supported path. I went that way because at the time the source had no token TTL anywhere in it, and there was no other non-interactive way to get a credential.</p>
<p dir="auto">I'm reporting it because two things behind it aren't specific to me at all: the 403 that should be a 401, and a reaper that can't tell one kind of token from another.</p>
<p dir="auto"><strong>20 days, 12 hours of continuous validity on a single token</strong>, with timestamps on both ends. Whatever the intended lifetime is now, nothing was expiring tokens before 2.16.0.</p>
<p dir="auto">My client's execution history brackets it the same way. Successful writes on:</p>
<ul>
<li>1 July — 5 executions</li>
<li>7 July — 7 executions</li>
<li>18 July — 5 executions, last at <strong>2026-07-18T18:08:59Z</strong></li>
</ul>
<p dir="auto">Then the pipeline sat idle. The next write attempt was <strong>2026-08-08T08:41:15Z</strong> — 21 days later, across the 2.16.0 boundary — and it 403'd, as did every attempt after it until I moved to WebDAV on 9 August. There are no executions in the gap, so this isn't a case of it degrading unnoticed; the first use after the update simply failed.</p>
<h3>Why the response is 403 and not 401</h3>
<p dir="auto"><code>backend/routes/files.js</code>, <code>add()</code>:</p>
<pre><code class="language-js">const subject = await files.translateResourcePath(req.user?.username, filePath);
if (!subject) return next(new HttpError(403, 'not allowed'));          // line 33-34

if (subject.share?.readonly) return next(new HttpError(403, 'share is read-only'));
if (!subject.share &amp;&amp; !req.user) return next(new HttpError(401, 'not allowed')); // line 37
</code></pre>
<p dir="auto">and <code>backend/files.js</code>:</p>
<pre><code class="language-js">// only shares may have optional auth
if (resource !== 'shares' &amp;&amp; !username) return null;
</code></pre>
<p dir="auto">For a <code>/home/...</code> path with no resolved user, <code>translateResourcePath</code> returns <code>null</code>, so line 34 fires and the 401 on line 37 is unreachable. The 401 you already wrote for exactly this case can never be reached on <code>/home</code> paths — it only applies to share paths.</p>
<p dir="auto">Swapping those two checks (or returning 401 when <code>!username &amp;&amp; resource !== 'shares'</code>) would turn this class of failure from silent into immediately diagnosable, independently of anything about token lifetime.</p>
<h3>One thing worth checking on your side</h3>
<p dir="auto">The cutoff is measured from mint, not from last use — <code>tokens.get</code> is a plain <code>SELECT</code> and never touches <code>created_at</code>, so there's no sliding window. A token dies 24 hours after it was issued no matter how actively it's being used.</p>
<p dir="auto">That applies to mobile tokens too. <code>POST /api/v1/mobile/code-to-token</code> returns:</p>
<pre><code class="language-js">next(new HttpSuccess(200, {
    token: apiToken,
    user: { username, email, displayName, admin }
}));
</code></pre>
<p dir="auto">— no expiry field, no refresh token, and no refresh path on that endpoint. So server-side, a token issued to <code>org.getcubby.app</code> stops resolving within 24 hours of issue, where before 2.16.0 it didn't expire at all.</p>
<p dir="auto">I can't see the Android app's source, so I don't know what it does when its token stops working — if it silently re-runs the pairing flow against a live IdP session, users may never notice. Flagging it in case that path isn't as silent as it needs to be now.</p>
<h3>What I'd ask for</h3>
<ol>
<li><strong>Record a token type at mint time</strong> and have <code>cleanup()</code> filter on it, so the WOPI sweep only reaps WOPI tokens. If a blanket 24-hour lifetime on all tokens is intended, that's fine — but it needs a changelog line and a doc note. The 2.16.0 entry lists the activity log, file drop, office collaboration, recents fixes and dependencies; nothing about tokens.</li>
<li><strong>Return 401 when a credential is presented and doesn't resolve.</strong> Three-line change, and it's correct HTTP regardless of the rest. WebDAV already gets this right — <code>webdav.js</code> sends <code>WWW-Authenticate: Basic realm="Cubby"</code> on an unauthenticated request.</li>
<li><strong>A supported way to obtain a durable API credential.</strong> Right now <code>tokens.add()</code> is reachable only from the mobile OIDC exchange and the WOPI flow; the browser UI has used cookie sessions since 0.7.0. So there's no non-interactive way to get a token, and <code>POST /api/v1/shares</code> is <code>isAuthenticated</code>, meaning share creation has no non-interactive path at all. <code>verifyCloudronCredentials</code> already exists in <code>webdav.js</code> and works — accepting App Passwords via Basic auth on <code>/api/v1/*</code> too would solve this cleanly, if that's less work than it looks from outside.</li>
</ol>
<p dir="auto">I've moved my file operations to WebDAV with a Cloudron App Password, which is durable and has no token involved, so I'm not blocked. Reporting it because the silent-403 behaviour will catch others the same way, and because of the mobile app question above.</p>
<h3>Reproducing</h3>
<p dir="auto">Reaper history, no instance needed:</p>
<pre><code class="language-bash">git clone --filter=blob:none https://github.com/getcubby/cubby.git &amp;&amp; cd cubby
git log -S "TOKEN_MAX_AGE_MS" --date=iso --pretty="%ad %H %s"
git log --follow --date=iso --pretty="%ad %h %s" -- backend/tokens.js
</code></pre>
<p dir="auto">Silent 403, any instance — no credential at all:</p>
<pre><code class="language-bash">curl -s -w '\n%{http_code}\n' -X POST \
  'https://&lt;cubby&gt;/api/v1/files?path=/home/x&amp;directory=true'
</code></pre>
]]></description><link>https://forum.cloudron.io/post/128015</link><guid isPermaLink="true">https://forum.cloudron.io/post/128015</guid><dc:creator><![CDATA[shrey]]></dc:creator><pubDate>Tue, 11 Aug 2026 07:20:39 GMT</pubDate></item><item><title><![CDATA[Reply to REST API broken since last update on Mon, 10 Aug 2026 08:20:26 GMT]]></title><description><![CDATA[<p dir="auto">Nothing really changed auth wise in the last cubby release. Do you see any errors in the back or frontend? Also how is auth performed? Are you using the OpenID issued access token?</p>
]]></description><link>https://forum.cloudron.io/post/127974</link><guid isPermaLink="true">https://forum.cloudron.io/post/127974</guid><dc:creator><![CDATA[nebulon]]></dc:creator><pubDate>Mon, 10 Aug 2026 08:20:26 GMT</pubDate></item></channel></rss>