mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-14 09:50:49 +00:00
* perf(auth): renew godmode app tokens from cache instead of the primary Every godmode launch read its token row from the primary and rewrote its expiry, a few cross-region round trips per launch far from the primary. - A cache pointer names the row per (user, web session, app). The row is read through the uuid cache token auth uses and re-checked (owner, session, app, revoked, expired, max age); anything else falls back to the primary read. Revoking the row drops the pointer. - The expiry write is skipped when it would move by under an hour. A renewal that hits a revoked or lapsed row mints a new one instead. - The response carries the row's actual expires_at. * perf(gui): reuse a just-minted godmode token for repeat launches A launch of a godmode app reuses the token this page minted for it within the last ten minutes under the same desktop session, instead of asking again. The cache is dropped when an app reports its token dead, after a session revoke from the session manager or an uninstall, and across tabs via a BroadcastChannel that carries token row ids, never tokens. A mint in flight during a drop is not remembered.
147 lines
6.0 KiB
JavaScript
147 lines
6.0 KiB
JavaScript
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { mintGodmodeToken, reusableGodmodeToken } from './godmodeTokens.js';
|
|
import { appSessionUuids, revokeAppSessions } from './revokeAppSessions.js';
|
|
|
|
const APP = 'app-1111';
|
|
|
|
// A list-sessions payload covering every row kind that carries an app_uid.
|
|
const sessions = [
|
|
{ uuid: 'web-1', kind: 'web', app_uid: null },
|
|
{ uuid: 'app-a', kind: 'app', app_uid: APP },
|
|
{ uuid: 'app-b', kind: 'app', app_uid: APP },
|
|
{ uuid: 'other', kind: 'app', app_uid: 'app-2222' },
|
|
{ uuid: 'wrk-1', kind: 'worker', app_uid: APP },
|
|
{ uuid: 'tok-1', kind: 'access_token', app_uid: APP },
|
|
];
|
|
|
|
describe('appSessionUuids', () => {
|
|
it("picks the app's own app rows and its godmode token row", () => {
|
|
expect(appSessionUuids(sessions, APP)).toEqual(['app-a', 'app-b', 'tok-1']);
|
|
});
|
|
|
|
it('leaves worker rows alone — they are deployment credentials', () => {
|
|
expect(appSessionUuids(sessions, APP)).not.toContain('wrk-1');
|
|
});
|
|
|
|
it('returns empty for a missing uid or a non-array payload', () => {
|
|
expect(appSessionUuids(sessions, '')).toEqual([]);
|
|
expect(appSessionUuids(null, APP)).toEqual([]);
|
|
expect(appSessionUuids({ error: 'nope' }, APP)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// Minimal fetch double: records calls and answers by URL.
|
|
const makeFetch = ({ list = sessions, listOk = true, revokeStatus = 200 } = {}) => {
|
|
const calls = [];
|
|
const fetchImpl = async (url, opts = {}) => {
|
|
calls.push({ url, method: opts.method, body: opts.body ? JSON.parse(opts.body) : null });
|
|
if ( url.endsWith('/auth/list-sessions') ) {
|
|
return { ok: listOk, status: listOk ? 200 : 500, json: async () => list };
|
|
}
|
|
const status = typeof revokeStatus === 'function' ? revokeStatus(calls.length) : revokeStatus;
|
|
return { ok: status >= 200 && status < 300, status };
|
|
};
|
|
return { fetchImpl, calls };
|
|
};
|
|
|
|
const deps = (over = {}) => ({
|
|
antiCsrfToken: async () => 'csrf-token',
|
|
apiOrigin: 'https://api.test',
|
|
authToken: 'auth-token',
|
|
...over,
|
|
});
|
|
|
|
describe('revokeAppSessions', () => {
|
|
it("revokes each of the app's sessions and reports the count", async () => {
|
|
const { fetchImpl, calls } = makeFetch();
|
|
const revoked = await revokeAppSessions(APP, deps({ fetchImpl }));
|
|
|
|
expect(revoked).toBe(3);
|
|
const revokes = calls.filter(c => c.url.endsWith('/auth/revoke-session'));
|
|
expect(revokes.map(c => c.body.uuid)).toEqual(['app-a', 'app-b', 'tok-1']);
|
|
});
|
|
|
|
it('sends an anti-csrf token with every revoke — they are single-use', async () => {
|
|
const { fetchImpl, calls } = makeFetch();
|
|
let issued = 0;
|
|
await revokeAppSessions(APP, deps({
|
|
fetchImpl,
|
|
antiCsrfToken: async () => `csrf-${++issued}`,
|
|
}));
|
|
|
|
const tokens = calls
|
|
.filter(c => c.url.endsWith('/auth/revoke-session'))
|
|
.map(c => c.body.anti_csrf);
|
|
expect(tokens).toEqual(['csrf-1', 'csrf-2', 'csrf-3']);
|
|
});
|
|
|
|
it('makes no revoke call when the app has no sessions', async () => {
|
|
const { fetchImpl, calls } = makeFetch({ list: [{ uuid: 'web-1', kind: 'web' }] });
|
|
expect(await revokeAppSessions(APP, deps({ fetchImpl }))).toBe(0);
|
|
expect(calls.some(c => c.url.endsWith('/auth/revoke-session'))).toBe(false);
|
|
});
|
|
|
|
it('counts a 404 as done — the row already went away', async () => {
|
|
const { fetchImpl } = makeFetch({ revokeStatus: 404 });
|
|
expect(await revokeAppSessions(APP, deps({ fetchImpl }))).toBe(3);
|
|
});
|
|
|
|
it('keeps going after one row fails, and reports the shortfall', async () => {
|
|
// First revoke 500s, second succeeds.
|
|
let n = 0;
|
|
const { fetchImpl, calls } = makeFetch({ revokeStatus: () => (++n === 1 ? 500 : 200) });
|
|
const revoked = await revokeAppSessions(APP, deps({ fetchImpl }));
|
|
|
|
expect(revoked).toBe(2);
|
|
expect(calls.filter(c => c.url.endsWith('/auth/revoke-session'))).toHaveLength(3);
|
|
});
|
|
|
|
it('throws when the session list cannot be read', async () => {
|
|
const { fetchImpl } = makeFetch({ listOk: false });
|
|
await expect(revokeAppSessions(APP, deps({ fetchImpl }))).rejects.toThrow(
|
|
/Failed to list sessions/,
|
|
);
|
|
});
|
|
|
|
it('stops launches reusing the godmode token it just revoked', async () => {
|
|
const minted = async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ token: 'jwt', godmode: true, expires_at: Math.floor(Date.now() / 1000) + 12 * 60 * 60 }),
|
|
});
|
|
await mintGodmodeToken(APP, { fetchImpl: minted, apiOrigin: 'https://api.test', authToken: 'auth-token' });
|
|
expect(reusableGodmodeToken(APP, { authToken: 'auth-token' })).not.toBeNull();
|
|
|
|
const { fetchImpl } = makeFetch({ revokeStatus: () => {
|
|
throw new Error('offline');
|
|
} });
|
|
await expect(revokeAppSessions(APP, deps({ fetchImpl }))).rejects.toThrow('offline');
|
|
expect(reusableGodmodeToken(APP, { authToken: 'auth-token' })).toBeNull();
|
|
});
|
|
|
|
it('does nothing without an app uid', async () => {
|
|
const { fetchImpl, calls } = makeFetch();
|
|
expect(await revokeAppSessions('', deps({ fetchImpl }))).toBe(0);
|
|
expect(calls).toHaveLength(0);
|
|
});
|
|
});
|