feat: refactor api() to accept optional baseUrl for cross-origin requests

Add 4th parameter `baseUrl` to `api()`. When provided, strips trailing
slash and prepends to path, and sets `credentials: 'include'` for
cross-origin cookie sending. Behavior is unchanged when no baseUrl is
given (backward compatible). Export `api` in module.exports.

All 135 tests pass.

🤖 Generated with Amplifier <https://github.com/microsoft/amplifier>

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
Brian Krabach
2026-03-30 22:12:57 -07:00
parent d0eb91dca7
commit 93c5170581
2 changed files with 61 additions and 2 deletions
+9 -2
View File
@@ -157,13 +157,18 @@ function isMobile() {
}
// ─── Fetch wrapper ────────────────────────────────────────────────────────────
async function api(method, path, body) {
async function api(method, path, body, baseUrl) {
const opts = { method, headers: {} };
if (body !== undefined) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(body);
}
const res = await fetch(path, opts);
let url = path;
if (baseUrl) {
url = baseUrl.replace(/\/+$/, '') + path;
opts.credentials = 'include';
}
const res = await fetch(url, opts);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
@@ -1711,6 +1716,8 @@ if (typeof module !== 'undefined' && module.exports) {
// Server settings
loadServerSettings,
patchServerSetting,
// Fetch wrapper
api,
// Header + button with inline name input
showNewSessionInput,
showFabSessionInput,
+52
View File
@@ -1928,6 +1928,58 @@ test('buildSidebarHTML includes sidebar-delete button with data-session attribut
assert.ok(html.includes('data-session="my-session"'), 'sidebar-delete button must have data-session attribute');
});
// --- api ---
test('api with no baseUrl uses relative path', async () => {
const calls = [];
const origFetch = globalThis.fetch;
globalThis.fetch = async (url, opts) => {
calls.push({ url, opts });
return { ok: true };
};
await app.api('GET', '/api/sessions');
assert.strictEqual(calls.length, 1, 'should call fetch once');
assert.strictEqual(calls[0].url, '/api/sessions', 'url should be relative path');
assert.ok(!calls[0].opts.credentials, 'credentials should not be set without baseUrl');
globalThis.fetch = origFetch;
});
test('api with baseUrl prepends it to path and sets credentials include', async () => {
const calls = [];
const origFetch = globalThis.fetch;
globalThis.fetch = async (url, opts) => {
calls.push({ url, opts });
return { ok: true };
};
await app.api('GET', '/api/sessions', undefined, 'https://remote.example.com');
assert.strictEqual(calls.length, 1, 'should call fetch once');
assert.strictEqual(calls[0].url, 'https://remote.example.com/api/sessions', 'url should prepend baseUrl');
assert.strictEqual(calls[0].opts.credentials, 'include', 'credentials should be include for cross-origin');
globalThis.fetch = origFetch;
});
test('api with baseUrl and trailing slash does not double-slash', async () => {
const calls = [];
const origFetch = globalThis.fetch;
globalThis.fetch = async (url, opts) => {
calls.push({ url, opts });
return { ok: true };
};
await app.api('GET', '/api/sessions', undefined, 'https://remote.example.com/');
assert.strictEqual(calls.length, 1, 'should call fetch once');
assert.strictEqual(calls[0].url, 'https://remote.example.com/api/sessions', 'trailing slash on baseUrl should not create double-slash');
globalThis.fetch = origFetch;
});
test('createNewSession polls for session before auto-opening (not immediate setTimeout openSession)', () => {
// The old behavior was: setTimeout(() => openSession(...), 500) immediately after POST.
// The new behavior must use a polling interval to wait for the session to appear in