mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-23 22:47:19 +00:00
fix: limit api pointing to apps only (#3470)
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import UIWindow from './UIWindow.js';
|
||||
import { loginProgressBody } from './loginProgressBody.js';
|
||||
|
||||
async function UIWindowLoginInProgress (options) {
|
||||
return new Promise(async (resolve) => {
|
||||
@@ -34,17 +35,7 @@ async function UIWindowLoginInProgress (options) {
|
||||
profile_pic = window.icons['profile.svg'];
|
||||
}
|
||||
|
||||
let h = '';
|
||||
h += '<div class="login-progress">';
|
||||
h += `<div class="profile-pic" style="background-color: #cecece; background-image: url('${profile_pic}'); width: 70px; height: 70px; background-position: center; background-size: cover; border-radius: 50px; margin-bottom: 15px; margin-top: 40px;"></div>`;
|
||||
h += `<h1 style="text-align: center;
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
font-weight: 300; margin: -10px 10px 4px 10px;">Logging in as <strong>${options.user_info.email === null ? options.user_info.username : options.user_info.email}</strong></h1>`;
|
||||
// spinner
|
||||
h += '<svg style="float:left; margin-right: 7px; margin-bottom: 30px;" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>';
|
||||
|
||||
h += '</div>';
|
||||
const h = loginProgressBody(options.user_info, profile_pic);
|
||||
|
||||
const el_window = await UIWindow({
|
||||
title: i18n('window_title_authenticating'),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
const SPINNER_SVG = '<svg style="float:left; margin-right: 7px; margin-bottom: 30px;" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>';
|
||||
|
||||
/**
|
||||
* Body markup for the "logging in as …" window.
|
||||
*
|
||||
* Every interpolated value is encoded: the user object is whatever the API
|
||||
* answered `whoami` with, and the picture lands inside a quoted `style`
|
||||
* attribute where an unencoded quote would end the attribute.
|
||||
*
|
||||
* @param {{ username?: string, email?: string|null }} [userInfo]
|
||||
* @param {string} [profilePic] - Image URL or data URI.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const loginProgressBody = (userInfo, profilePic) => {
|
||||
const identity = userInfo?.email || userInfo?.username || '';
|
||||
|
||||
let h = '';
|
||||
h += '<div class="login-progress">';
|
||||
h += `<div class="profile-pic" style="background-color: #cecece; background-image: url('${html_encode(profilePic ?? '')}'); width: 70px; height: 70px; background-position: center; background-size: cover; border-radius: 50px; margin-bottom: 15px; margin-top: 40px;"></div>`;
|
||||
h += `<h1 style="text-align: center;
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
font-weight: 300; margin: -10px 10px 4px 10px;">${i18n('logging_in_as', { identity: html_encode(identity) }, false)}</h1>`;
|
||||
h += SPINNER_SVG;
|
||||
h += '</div>';
|
||||
|
||||
return h;
|
||||
};
|
||||
|
||||
export default loginProgressBody;
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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, beforeAll } from 'vitest';
|
||||
import { encode } from 'html-entities';
|
||||
import { loginProgressBody } from './loginProgressBody.js';
|
||||
|
||||
beforeAll(() => {
|
||||
// Same encoder the GUI installs globally (see lib/html-entities.js).
|
||||
globalThis.html_encode = str => encode(str);
|
||||
// Placeholder substitution only — the caller is responsible for encoding,
|
||||
// which is exactly what these tests check.
|
||||
globalThis.i18n = (key, vars = {}, encode_html = true) => {
|
||||
expect(encode_html).toBe(false);
|
||||
return `Logging in as <strong>${vars.identity}</strong>`;
|
||||
};
|
||||
});
|
||||
|
||||
describe('loginProgressBody', () => {
|
||||
it('shows the email, falling back to the username', () => {
|
||||
expect(loginProgressBody({ username: 'alice', email: 'alice@example.com' }, ''))
|
||||
.toContain('alice@example.com');
|
||||
expect(loginProgressBody({ username: 'alice', email: null }, ''))
|
||||
.toContain('<strong>alice</strong>');
|
||||
});
|
||||
|
||||
it('encodes an identity carrying markup', () => {
|
||||
const h = loginProgressBody({ email: '<img src=x onerror=alert(1)>' }, '');
|
||||
expect(h).not.toContain('<img');
|
||||
expect(h).toContain('<img src=x onerror=alert(1)>');
|
||||
});
|
||||
|
||||
it('encodes a picture URL that tries to escape the style attribute', () => {
|
||||
const h = loginProgressBody(
|
||||
{ username: 'alice' },
|
||||
"data:image/svg+xml,'); background: red; x: ('",
|
||||
);
|
||||
expect(h).not.toContain("'); background: red");
|
||||
expect(h).toContain(''); background: red');
|
||||
});
|
||||
|
||||
it('renders without a user or a picture', () => {
|
||||
const h = loginProgressBody(undefined, undefined);
|
||||
expect(h).toContain('<strong></strong>');
|
||||
expect(h).toContain("url('')");
|
||||
});
|
||||
});
|
||||
@@ -578,7 +578,7 @@ window.refresh_user_data = async (auth_token) => {
|
||||
}
|
||||
};
|
||||
|
||||
window.update_auth_data = async (auth_token, user, api_origin) => {
|
||||
window.update_auth_data = async (auth_token, user) => {
|
||||
window.auth_token = auth_token;
|
||||
// Write the v2 key going forward and clear any lingering v1 key so
|
||||
// a single localStorage source-of-truth is used.
|
||||
@@ -606,11 +606,6 @@ window.update_auth_data = async (auth_token, user, api_origin) => {
|
||||
}
|
||||
}
|
||||
|
||||
if ( api_origin ) {
|
||||
window.api_origin = api_origin;
|
||||
localStorage.setItem('api_origin', api_origin);
|
||||
}
|
||||
|
||||
// Has username changed?
|
||||
if ( window.user?.username !== user.username )
|
||||
{
|
||||
|
||||
@@ -181,6 +181,7 @@ const en = {
|
||||
log_in: 'Log In',
|
||||
log_into_another_account_anyway: 'Log into another account anyway',
|
||||
log_out: 'Log Out',
|
||||
logging_in_as: 'Logging in as <strong>{{identity}}</strong>',
|
||||
looks_good: 'Looks good!',
|
||||
manage_sessions: 'Manage Sessions',
|
||||
modified: 'Modified',
|
||||
|
||||
+28
-23
@@ -58,6 +58,7 @@ import { ThemeService } from './services/ThemeService.js';
|
||||
// factory name so a bare `privacy_aware_path(path)` call in this module can't
|
||||
// silently resolve to the factory — use `window.privacy_aware_path` instead.
|
||||
import { privacy_aware_path as privacy_aware_path_factory } from './util/desktop.js';
|
||||
import { resolveAPIOrigin } from './util/apiOrigin.js';
|
||||
import {
|
||||
deliversTokenToOpener,
|
||||
trustsOpenerOriginParam,
|
||||
@@ -896,6 +897,9 @@ window.initgui = async function (options) {
|
||||
.filter((element) => element);
|
||||
window.url_paths = url_paths;
|
||||
|
||||
// GET query params provided
|
||||
window.url_query_params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Install device signal helpers; collection is lazy. The fingerprint is
|
||||
// on by default (gui_params.thumbmarkEnabled = false kills it); the Prelude
|
||||
// dispatch id needs gui_params.preludeSdkKey.
|
||||
@@ -907,11 +911,25 @@ window.initgui = async function (options) {
|
||||
if (window.auth_token && puter.authToken !== window.auth_token) {
|
||||
puter.setAuthToken(window.auth_token);
|
||||
}
|
||||
// update SDK if api_origin is different from the one in the SDK
|
||||
if (window.api_origin && puter.APIOrigin !== window.api_origin) {
|
||||
puter.setAPIOrigin(
|
||||
localStorage.getItem('api_origin') || window.api_origin,
|
||||
);
|
||||
// Point the SDK at this deployment's own API. Anywhere but a Puter served
|
||||
// off the developer's own machine, that is the only origin we'll use — see
|
||||
// resolveAPIOrigin for why, and for the dev flow that is the exception.
|
||||
if (window.api_origin) {
|
||||
const api_origin = resolveAPIOrigin({
|
||||
configuredOrigin: window.api_origin,
|
||||
guiOrigin: window.location.origin,
|
||||
urlOrigin: window.url_query_params.get('api_origin'),
|
||||
storedOrigin: localStorage.getItem('api_origin'),
|
||||
});
|
||||
if (api_origin === window.api_origin) {
|
||||
localStorage.removeItem('api_origin');
|
||||
} else {
|
||||
// Sticks for the rest of the session: only the boot that carried
|
||||
// the parameter has it in the URL.
|
||||
localStorage.setItem('api_origin', api_origin);
|
||||
window.api_origin = api_origin;
|
||||
}
|
||||
if (puter.APIOrigin !== api_origin) puter.setAPIOrigin(api_origin);
|
||||
}
|
||||
|
||||
// Print the version to the console
|
||||
@@ -960,9 +978,6 @@ window.initgui = async function (options) {
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">',
|
||||
);
|
||||
|
||||
// GET query params provided
|
||||
window.url_query_params = new URLSearchParams(window.location.search);
|
||||
|
||||
// will hold the result of the whoami API call
|
||||
let whoami;
|
||||
|
||||
@@ -1333,17 +1348,12 @@ window.initgui = async function (options) {
|
||||
// -------------------------------------------------------------------------------------
|
||||
else if (window.url_query_params.has('auth_token')) {
|
||||
let query_param_auth_token = window.url_query_params.get('auth_token');
|
||||
let api_origin;
|
||||
|
||||
// check if we have api_origin in the URL query params
|
||||
if (window.url_query_params.has('api_origin')) {
|
||||
api_origin = window.url_query_params.get('api_origin');
|
||||
puter.setAPIOrigin(api_origin);
|
||||
}
|
||||
|
||||
const previous_auth_token = window.auth_token;
|
||||
const previous_api_origin = window.api_origin;
|
||||
|
||||
// The token is resolved against our own API origin — deliberately not
|
||||
// one named by the same URL that carried the token, which would leave
|
||||
// the identity we're about to render up to whoever wrote the link.
|
||||
puter.setAuthToken(query_param_auth_token);
|
||||
|
||||
try {
|
||||
@@ -1358,7 +1368,7 @@ window.initgui = async function (options) {
|
||||
// Confirm the identity before adopting a token that came from the
|
||||
// URL. Every other path into `update_auth_data` is an account the
|
||||
// user picked in the UI, so those don't ask.
|
||||
if (whoami && window.user?.uuid !== whoami.uuid) {
|
||||
if (whoami && (!window.user || window.user.uuid !== whoami.uuid)) {
|
||||
const proceed = await UIAlert({
|
||||
type: 'confirm',
|
||||
// `false` — UIAlert encodes the message itself.
|
||||
@@ -1376,7 +1386,6 @@ window.initgui = async function (options) {
|
||||
// Back to whatever session was already here; normal boot
|
||||
// picks up below.
|
||||
puter.setAuthToken(previous_auth_token);
|
||||
if (api_origin) puter.setAPIOrigin(previous_api_origin);
|
||||
whoami = null;
|
||||
}
|
||||
}
|
||||
@@ -1432,11 +1441,7 @@ window.initgui = async function (options) {
|
||||
// show login progress window
|
||||
UIWindowLoginInProgress({ user_info: whoami });
|
||||
// update auth data
|
||||
await window.update_auth_data(
|
||||
query_param_auth_token,
|
||||
whoami,
|
||||
api_origin,
|
||||
);
|
||||
await window.update_auth_data(query_param_auth_token, whoami);
|
||||
}
|
||||
// remove auth_token from URL, keeping the current path (e.g. `/` or `/desktop`)
|
||||
// and hash (dashboard tab links like /#usage)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
const LOOPBACK_HOSTNAMES = new Set(['localhost', '[::1]', '::1']);
|
||||
const LOOPBACK_IPV4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
||||
|
||||
/**
|
||||
* Is this origin a Puter served off the developer's own machine?
|
||||
*
|
||||
* @param {string} [origin] - An absolute origin, e.g. `https://puter.com`.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isLoopbackOrigin = (origin) => {
|
||||
let hostname;
|
||||
try {
|
||||
hostname = new URL(origin).hostname;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LOOPBACK_HOSTNAMES.has(hostname) ||
|
||||
hostname.endsWith('.localhost') ||
|
||||
LOOPBACK_IPV4.test(hostname);
|
||||
};
|
||||
|
||||
/**
|
||||
* The API origin the GUI talks to.
|
||||
*
|
||||
* Normally this deployment's own configured origin, and nothing else: an
|
||||
* origin named by the URL would let a link resolve the signed-in desktop --
|
||||
* `whoami` included -- against a server we don't control. Apps may repoint
|
||||
* their own SDK instance; the GUI may not.
|
||||
*
|
||||
* The one exception is a Puter served from the developer's own machine, where
|
||||
* `npm start -- --server` drives a local GUI against a remote backend. That
|
||||
* choice sticks for the rest of the session, so the caller persists whatever
|
||||
* comes back when it differs from the configured origin, and passes it here as
|
||||
* `storedOrigin` on later boots.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {string} params.configuredOrigin - Server-templated `api_origin`.
|
||||
* @param {string} params.guiOrigin - Origin this page is served from.
|
||||
* @param {string|null} [params.urlOrigin] - `?api_origin=` from the URL.
|
||||
* @param {string|null} [params.storedOrigin] - Origin kept from a past boot.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const resolveAPIOrigin = ({
|
||||
configuredOrigin,
|
||||
guiOrigin,
|
||||
urlOrigin,
|
||||
storedOrigin,
|
||||
}) => {
|
||||
if (!isLoopbackOrigin(guiOrigin)) return configuredOrigin;
|
||||
return urlOrigin || storedOrigin || configuredOrigin;
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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 { isLoopbackOrigin, resolveAPIOrigin } from './apiOrigin.js';
|
||||
|
||||
const CONFIGURED = 'https://api.puter.com';
|
||||
const EVIL = 'https://attacker.example';
|
||||
|
||||
describe('isLoopbackOrigin', () => {
|
||||
it('accepts the hosts a local Puter is served from', () => {
|
||||
for ( const origin of [
|
||||
'http://localhost:4100',
|
||||
'http://puter.localhost:4100',
|
||||
'http://api.puter.localhost:4100',
|
||||
'http://127.0.0.1:4100',
|
||||
'http://127.10.0.2',
|
||||
'http://[::1]:4100',
|
||||
] ) {
|
||||
expect(isLoopbackOrigin(origin), origin).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects everything else, including lookalikes', () => {
|
||||
for ( const origin of [
|
||||
'https://puter.com',
|
||||
'https://myputer.example',
|
||||
'https://localhost.attacker.example',
|
||||
'https://notlocalhost',
|
||||
'https://127.0.0.1.attacker.example',
|
||||
'not a url',
|
||||
undefined,
|
||||
] ) {
|
||||
expect(isLoopbackOrigin(origin), String(origin)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAPIOrigin', () => {
|
||||
it('ignores a URL-supplied origin on a hosted deployment', () => {
|
||||
expect(resolveAPIOrigin({
|
||||
configuredOrigin: CONFIGURED,
|
||||
guiOrigin: 'https://puter.com',
|
||||
urlOrigin: EVIL,
|
||||
})).toBe(CONFIGURED);
|
||||
});
|
||||
|
||||
it('ignores a stored origin on a hosted deployment', () => {
|
||||
expect(resolveAPIOrigin({
|
||||
configuredOrigin: CONFIGURED,
|
||||
guiOrigin: 'https://puter.com',
|
||||
storedOrigin: EVIL,
|
||||
})).toBe(CONFIGURED);
|
||||
});
|
||||
|
||||
it('ignores both on a self-hosted deployment', () => {
|
||||
expect(resolveAPIOrigin({
|
||||
configuredOrigin: 'https://api.myputer.example',
|
||||
guiOrigin: 'https://myputer.example',
|
||||
urlOrigin: EVIL,
|
||||
storedOrigin: EVIL,
|
||||
})).toBe('https://api.myputer.example');
|
||||
});
|
||||
|
||||
it('honors a URL-supplied origin on a local Puter', () => {
|
||||
expect(resolveAPIOrigin({
|
||||
configuredOrigin: 'http://api.puter.localhost:4100',
|
||||
guiOrigin: 'http://puter.localhost:4100',
|
||||
urlOrigin: CONFIGURED,
|
||||
})).toBe(CONFIGURED);
|
||||
});
|
||||
|
||||
it('falls back to a stored origin on a local Puter, URL winning', () => {
|
||||
const local = {
|
||||
configuredOrigin: 'http://api.puter.localhost:4100',
|
||||
guiOrigin: 'http://puter.localhost:4100',
|
||||
};
|
||||
expect(resolveAPIOrigin({ ...local, storedOrigin: CONFIGURED }))
|
||||
.toBe(CONFIGURED);
|
||||
expect(resolveAPIOrigin({ ...local, urlOrigin: CONFIGURED, storedOrigin: EVIL }))
|
||||
.toBe(CONFIGURED);
|
||||
});
|
||||
|
||||
it('falls back to the configured origin when nothing is supplied', () => {
|
||||
expect(resolveAPIOrigin({
|
||||
configuredOrigin: CONFIGURED,
|
||||
guiOrigin: 'http://puter.localhost:4100',
|
||||
urlOrigin: null,
|
||||
storedOrigin: null,
|
||||
})).toBe(CONFIGURED);
|
||||
});
|
||||
});
|
||||
@@ -120,8 +120,8 @@ By default, video and trace are saved **only on failure**. `test:e2e:record` ena
|
||||
`globalSetup` runs at the start of the test run:
|
||||
|
||||
1. Direct `POST /login` from Node with the admin credentials → JWT token.
|
||||
2. Probes `window.api_origin` from Puter (server-templated) so we can pass it through.
|
||||
3. Opens chromium once and navigates to `puter.localhost:4100/?auth_token=<token>&api_origin=<origin>`, then accepts the "Continue as \<username\>?" confirmation the auth_token handler shows before adopting a token from a URL. Puter's `initgui` auth_token handler then runs the full auth setup: `puter.setAuthToken`, `puter.setAPIOrigin`, `/session/sync-cookie`, `update_auth_data`.
|
||||
2. Probes `window.api_origin` from Puter (server-templated) so the SDK's adopted origin can be asserted — the GUI always uses its own API origin and will not take one from the URL.
|
||||
3. Opens chromium once and navigates to `puter.localhost:4100/?auth_token=<token>`, then accepts the "Continue as \<username\>?" confirmation the auth_token handler shows before adopting a token from a URL. Puter's `initgui` auth_token handler then runs the full auth setup: `puter.setAuthToken`, `/session/sync-cookie`, `update_auth_data`.
|
||||
4. Saves cookies + localStorage to `tests/e2e/.auth/state.json` (gitignored, cached for 24h).
|
||||
|
||||
Every test context loads with that `storageState` → already signed in as admin → no per-test `/login`, no `/signup`, no rate limits.
|
||||
|
||||
@@ -90,17 +90,15 @@ export default async function globalSetup () {
|
||||
// 1) Direct API login → token (no browser, no /signup, no rate-limit risk).
|
||||
const token = await loginAsAdmin({ origin: PUTER_ORIGIN, username: ADMIN_USERNAME, password: ADMIN_PASSWORD });
|
||||
|
||||
// 2) Open Puter with ?auth_token=<token>&api_origin=<origin> so initgui's
|
||||
// auth_token flow runs the full setup (puter.setAuthToken,
|
||||
// puter.setAPIOrigin, /session/sync-cookie, update_auth_data) and persists
|
||||
// the API origin to localStorage.api_origin — otherwise the bundled SDK
|
||||
// keeps its production default (https://api.puter.com) and apps.create
|
||||
// hits the wrong server.
|
||||
// 2) Open Puter with ?auth_token=<token> so initgui's auth_token flow runs
|
||||
// the full setup (puter.setAuthToken, /session/sync-cookie,
|
||||
// update_auth_data). The GUI points the SDK at its own templated
|
||||
// api_origin — that is not overridable from the URL.
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Probe the GUI's templated api_origin first so we can pass it through.
|
||||
// Probe the GUI's templated api_origin so we can assert the SDK adopts it.
|
||||
await page.goto(PUTER_ORIGIN);
|
||||
const apiOrigin = await page.evaluate(() => window.api_origin || null);
|
||||
if ( ! apiOrigin ) {
|
||||
@@ -108,10 +106,7 @@ export default async function globalSetup () {
|
||||
throw new Error(`Could not determine api_origin from ${PUTER_ORIGIN} (window.api_origin was empty).`);
|
||||
}
|
||||
|
||||
await page.goto(
|
||||
`${PUTER_ORIGIN}/?auth_token=${encodeURIComponent(token)}` +
|
||||
`&api_origin=${encodeURIComponent(apiOrigin)}`,
|
||||
);
|
||||
await page.goto(`${PUTER_ORIGIN}/?auth_token=${encodeURIComponent(token)}`);
|
||||
|
||||
await page.waitForFunction(() => !!window.puter, null, { timeout: 60_000 });
|
||||
|
||||
@@ -122,13 +117,12 @@ export default async function globalSetup () {
|
||||
.click({ timeout: 60_000 });
|
||||
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
expectedApiOrigin => {
|
||||
// v2 key since the v1→v2 cutover; legacy key tolerated.
|
||||
const ls = (typeof localStorage !== 'undefined') ? (localStorage.getItem('auth_token_v2') || localStorage.getItem('auth_token')) : null;
|
||||
const lsApi = (typeof localStorage !== 'undefined') ? localStorage.getItem('api_origin') : null;
|
||||
return !!(ls && lsApi && window.auth_token && window.puter?.authToken && window.puter?.APIOrigin === lsApi);
|
||||
return !!(ls && window.auth_token && window.puter?.authToken && window.puter?.APIOrigin === expectedApiOrigin);
|
||||
},
|
||||
null,
|
||||
apiOrigin,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ const PUTER_READY_TIMEOUT = 60_000;
|
||||
export async function waitForPuterReady (page) {
|
||||
await page.waitForFunction(() => !!window.puter, null, { timeout: PUTER_READY_TIMEOUT });
|
||||
|
||||
// With storageState, sign-in should already be done. Wait for both auth
|
||||
// and the API origin to be picked up by the SDK from localStorage.
|
||||
// With storageState, sign-in should already be done. Wait for the token to
|
||||
// be picked up and the SDK pointed at the GUI's own API origin.
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
@@ -25,11 +25,10 @@ export async function waitForPuterReady (page) {
|
||||
const ls = (typeof localStorage !== 'undefined')
|
||||
? (localStorage.getItem('auth_token_v2') || localStorage.getItem('auth_token'))
|
||||
: null;
|
||||
const lsApi = (typeof localStorage !== 'undefined') ? localStorage.getItem('api_origin') : null;
|
||||
return !!(
|
||||
ls && lsApi &&
|
||||
ls &&
|
||||
window.auth_token && window.puter?.authToken &&
|
||||
window.puter?.APIOrigin === lsApi
|
||||
window.puter?.APIOrigin === window.api_origin
|
||||
);
|
||||
},
|
||||
null,
|
||||
|
||||
Reference in New Issue
Block a user