mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 15:07:17 +00:00
initial pjs helper stuff
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import kvjs from '@heyputer/kv.js';
|
||||
import APICallLogger from './lib/APICallLogger.js';
|
||||
import { fetchUrl } from './lib/PuterClient.js';
|
||||
import { isStoredTokenUsableForOrigin } from './lib/authTokenOrigin.js';
|
||||
import path from './lib/path.js';
|
||||
import localStorageMemory from './lib/polyfills/localStorage.js';
|
||||
@@ -666,12 +667,12 @@ const puterInit = function () {
|
||||
this.net = {
|
||||
generateWispV1URL: async () => {
|
||||
const { token: wispToken, server: wispServer } = await (
|
||||
await fetch(
|
||||
await fetchUrl(
|
||||
`${this.APIOrigin}/wisp/relay-token/create`,
|
||||
{
|
||||
method: 'POST',
|
||||
includePuterAuth: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
@@ -717,10 +718,10 @@ const puterInit = function () {
|
||||
|
||||
let had_error = false;
|
||||
try {
|
||||
const resp = await fetch(`${this.APIOrigin}/rao`, {
|
||||
const resp = await fetchUrl(`${this.APIOrigin}/rao`, {
|
||||
method: 'POST',
|
||||
includePuterAuth: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
Origin: location.origin, // This is ignored in the browser but needed for workers and nodejs
|
||||
},
|
||||
});
|
||||
@@ -1064,7 +1065,7 @@ const puterInit = function () {
|
||||
// everywhere else the JSON token response is all we need.
|
||||
const sameOrigin =
|
||||
globalThis.location?.origin === this.defaultGUIOrigin;
|
||||
const resp = await fetch(
|
||||
const resp = await fetchUrl(
|
||||
`${this.defaultGUIOrigin}/auth/migrate-token`,
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -1072,7 +1073,7 @@ const puterInit = function () {
|
||||
Authorization: `Bearer ${v1Token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: sameOrigin ? 'include' : 'omit',
|
||||
withCredentials: sameOrigin,
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* 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 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 { createDeferred, resolveReauth } from './utils.js';
|
||||
|
||||
/**
|
||||
* The single HTTP core for puter.js. `fetchUrl` is an XHR-based replacement for
|
||||
* `fetch()` — every request that used to call `fetch()` directly routes through
|
||||
* here so auth headers, streaming, API-call logging, and 401 reauth-replay live
|
||||
* in one place. XHR (not `fetch`) because `fetch` is inconsistent across the
|
||||
* platforms puter.js supports (browser / web-worker / service-worker / node)
|
||||
* and we ship a strong in-house XHR polyfill (`lib/polyfills/xhrshim.js`), so
|
||||
* `new XMLHttpRequest()` resolves to native XHR or the shim transparently.
|
||||
*
|
||||
* The interface is frozen: additive changes only. Retry, dedup, and pagination
|
||||
* options are reserved below and land in later sprint steps.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PuterResponse
|
||||
* A `fetch`-Response-like view over a completed (or streaming) XHR.
|
||||
* @property {boolean} ok - status in the 200-299 range.
|
||||
* @property {number} status
|
||||
* @property {string} statusText
|
||||
* @property {string} url - final response URL.
|
||||
* @property {{ get(name: string): (string|null) }} headers
|
||||
* @property {() => Promise<any>} json
|
||||
* @property {() => Promise<string>} text
|
||||
* @property {() => Promise<Blob>} blob
|
||||
* @property {() => Promise<ArrayBuffer>} arrayBuffer
|
||||
* @property {() => AsyncGenerator<any>} stream - parsed NDJSON lines; only
|
||||
* meaningful for `application/x-ndjson` responses.
|
||||
*/
|
||||
|
||||
const isNdjson = contentType => (contentType || '').includes('application/x-ndjson');
|
||||
|
||||
/** Read the XHR body as text regardless of the responseType it was sent with. */
|
||||
async function bodyText (xhr) {
|
||||
switch ( xhr.responseType ) {
|
||||
case 'blob': return await xhr.response.text();
|
||||
case 'arraybuffer': return new TextDecoder().decode(xhr.response);
|
||||
case 'json': return JSON.stringify(xhr.response);
|
||||
default: return xhr.responseText; // '' | 'text'
|
||||
}
|
||||
}
|
||||
|
||||
async function bodyBlob (xhr) {
|
||||
if ( xhr.responseType === 'blob' ) return xhr.response;
|
||||
const type = xhr.getResponseHeader('content-type') || 'application/octet-stream';
|
||||
if ( xhr.responseType === 'arraybuffer' ) return new Blob([xhr.response], { type });
|
||||
return new Blob([await bodyText(xhr)], { type });
|
||||
}
|
||||
|
||||
async function bodyArrayBuffer (xhr) {
|
||||
if ( xhr.responseType === 'arraybuffer' ) return xhr.response;
|
||||
return await (await bodyBlob(xhr)).arrayBuffer();
|
||||
}
|
||||
|
||||
async function bodyJson (xhr) {
|
||||
if ( xhr.responseType === 'json' ) return xhr.response;
|
||||
return JSON.parse(await bodyText(xhr));
|
||||
}
|
||||
|
||||
function makeResponse (xhr, stream) {
|
||||
const status = xhr.status;
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: xhr.statusText,
|
||||
url: xhr.responseURL || '',
|
||||
headers: { get: name => xhr.getResponseHeader(name) },
|
||||
text: () => bodyText(xhr),
|
||||
json: () => bodyJson(xhr),
|
||||
blob: () => bodyBlob(xhr),
|
||||
arrayBuffer: () => bodyArrayBuffer(xhr),
|
||||
stream: () => {
|
||||
if ( ! stream ) {
|
||||
throw new Error('stream() is only available for application/x-ndjson responses');
|
||||
}
|
||||
return stream;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function logRequest (logId, { result = null, error = null } = {}) {
|
||||
if ( ! logId || ! globalThis.puter?.apiCallLogger?.isEnabled() ) return;
|
||||
globalThis.puter.apiCallLogger.logRequest({ ...logId, result, error });
|
||||
}
|
||||
|
||||
/** Best-effort body for logging — parsed JSON where sensible, else a placeholder. */
|
||||
async function bodyForLog (xhr) {
|
||||
const contentType = xhr.getResponseHeader('content-type') || '';
|
||||
if ( xhr.responseType === '' || xhr.responseType === 'text' || contentType.includes('json') ) {
|
||||
try { return await bodyJson(xhr); } catch ( e ) {
|
||||
try { return await bodyText(xhr); } catch ( e2 ) { return null; }
|
||||
}
|
||||
}
|
||||
return `[${contentType || 'binary'}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* XHR-based `fetch()` replacement. Returns a `fetch`-Response-like object.
|
||||
*
|
||||
* fetch semantics: the promise resolves for any HTTP status (`ok` reflects
|
||||
* 2xx); it rejects only on network/abort errors. The one exception is a 401
|
||||
* carrying a reauth signal — the reauth flow is driven first and, on success,
|
||||
* the request is replayed once with the fresh token (transparent recovery). A
|
||||
* non-recoverable 401 resolves as an `ok: false` response like any other.
|
||||
*
|
||||
* @param {string} url - Full request URL (callers own origin composition).
|
||||
* @param {Object} [opts]
|
||||
* @param {boolean} [opts.includePuterAuth=false] - Add `Authorization: Bearer <puter.authToken>`.
|
||||
* @param {string} [opts.method='GET']
|
||||
* @param {Object} [opts.headers] - Extra request headers (undefined/null values skipped).
|
||||
* @param {string|Blob|ArrayBuffer|FormData|null} [opts.body]
|
||||
* @param {''|'text'|'json'|'blob'|'arraybuffer'} [opts.responseType='']
|
||||
* @param {boolean} [opts.withCredentials=true]
|
||||
* @param {AbortSignal} [opts.signal]
|
||||
* @param {{service: string, operation: string, params?: Object}} [opts.logContext]
|
||||
* Semantic context for the centralized API-call log. Omit to log generically.
|
||||
* @param {Object} [opts.retry] - Reserved for a later sprint step (ignored).
|
||||
* @param {Object} [opts.dedupe] - Reserved for a later sprint step (ignored).
|
||||
* @param {Object} [opts.paginate] - Reserved for a later sprint step (ignored).
|
||||
* @returns {Promise<PuterResponse>}
|
||||
*/
|
||||
function fetchUrl (url, opts = {}) {
|
||||
const {
|
||||
includePuterAuth = false,
|
||||
method = 'GET',
|
||||
headers = {},
|
||||
body = null,
|
||||
responseType = '',
|
||||
withCredentials = true,
|
||||
signal,
|
||||
logContext,
|
||||
_reauthReplayed = false, // internal one-shot guard for reauth replay
|
||||
} = opts;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url, true);
|
||||
xhr.withCredentials = withCredentials;
|
||||
// Text mode keeps NDJSON progress deltas available for stream(); callers
|
||||
// that need binary/parsed bodies pass an explicit responseType.
|
||||
xhr.responseType = responseType;
|
||||
|
||||
if ( includePuterAuth && globalThis.puter?.authToken ) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${globalThis.puter.authToken}`);
|
||||
}
|
||||
for ( const [ name, value ] of Object.entries(headers) ) {
|
||||
if ( value !== undefined && value !== null ) {
|
||||
xhr.setRequestHeader(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
if ( signal ) {
|
||||
if ( signal.aborted ) return reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
||||
signal.addEventListener('abort', () => xhr.abort(), { once: true });
|
||||
}
|
||||
|
||||
let logId = null;
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
logId = logContext ?? { service: 'fetchUrl', operation: `${method} ${url}`, params: { url, method } };
|
||||
}
|
||||
|
||||
// -- NDJSON streaming --
|
||||
// Detect a stream at HEADERS_RECEIVED and resolve early with a response
|
||||
// whose stream() yields parsed lines as onprogress deltas arrive. This
|
||||
// mirrors the streaming in driverCall_ (utils.js) and the xhrshim.
|
||||
let resolvedAsStream = false;
|
||||
let responseComplete = false;
|
||||
let signalStreamUpdate = null;
|
||||
const linesReceived = [];
|
||||
let carry = '';
|
||||
let consumedLength = 0;
|
||||
|
||||
const pushLines = text => {
|
||||
carry += text;
|
||||
let nl;
|
||||
while ( (nl = carry.indexOf('\n')) !== -1 ) {
|
||||
linesReceived.push(carry.slice(0, nl));
|
||||
carry = carry.slice(nl + 1);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if ( xhr.readyState === 2 && ! isNdjson(xhr.getResponseHeader('Content-Type')) ) return;
|
||||
if ( xhr.readyState === 2 ) {
|
||||
resolvedAsStream = true;
|
||||
const stream = (async function* () {
|
||||
while ( true ) {
|
||||
while ( linesReceived.length > 0 ) {
|
||||
const line = linesReceived.shift();
|
||||
if ( line.trim() === '' ) continue;
|
||||
yield JSON.parse(line);
|
||||
}
|
||||
if ( responseComplete ) break;
|
||||
const sig = createDeferred();
|
||||
signalStreamUpdate = sig.resolve;
|
||||
await sig.promise;
|
||||
}
|
||||
})();
|
||||
logRequest(logId, { result: '[stream]' });
|
||||
resolve(makeResponse(xhr, stream));
|
||||
}
|
||||
if ( xhr.readyState === 4 && resolvedAsStream ) {
|
||||
if ( carry.length > 0 ) { linesReceived.push(carry); carry = ''; }
|
||||
responseComplete = true;
|
||||
signalStreamUpdate?.();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onprogress = () => {
|
||||
if ( ! resolvedAsStream ) return;
|
||||
const fresh = xhr.responseText.slice(consumedLength);
|
||||
consumedLength = xhr.responseText.length;
|
||||
if ( ! fresh ) return;
|
||||
pushLines(fresh);
|
||||
signalStreamUpdate?.();
|
||||
};
|
||||
|
||||
// -- Buffered (non-stream) response --
|
||||
xhr.addEventListener('load', async function () {
|
||||
if ( resolvedAsStream ) return;
|
||||
|
||||
if ( this.status === 401 && ! _reauthReplayed ) {
|
||||
let parsed = null;
|
||||
try { parsed = await bodyJson(this); } catch ( e ) { parsed = null; }
|
||||
const reauth = await resolveReauth(parsed);
|
||||
if ( reauth?.action === 'replay' ) {
|
||||
try {
|
||||
return resolve(await fetchUrl(url, { ...opts, _reauthReplayed: true }));
|
||||
} catch ( e ) {
|
||||
return reject(e);
|
||||
}
|
||||
}
|
||||
// reauth 'reject'/null (incl. token_auth_failed handled inside
|
||||
// resolveReauth): surface the 401 as an ok:false response, the
|
||||
// same way fetch does for any error status.
|
||||
}
|
||||
|
||||
const resp = makeResponse(this);
|
||||
if ( logId ) {
|
||||
const logged = await bodyForLog(this);
|
||||
logRequest(logId, this.status >= 400
|
||||
? { error: logged ?? { message: this.statusText, status: this.status } }
|
||||
: { result: logged });
|
||||
}
|
||||
resolve(resp);
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', function (e) {
|
||||
logRequest(logId, { error: { message: 'Network error occurred', event: e.type } });
|
||||
reject(new TypeError(`Network request to ${url} failed`));
|
||||
});
|
||||
xhr.addEventListener('abort', function () {
|
||||
reject(signal?.reason ?? new DOMException('Aborted', 'AbortError'));
|
||||
});
|
||||
|
||||
xhr.send(body);
|
||||
});
|
||||
}
|
||||
|
||||
export { fetchUrl };
|
||||
@@ -0,0 +1,222 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fetchUrl } from './PuterClient.js';
|
||||
|
||||
// -- Controllable fake XMLHttpRequest --
|
||||
// Drives fetchUrl's event handlers deterministically. Each instance plays the
|
||||
// `program` set on the class, which the test uses to script the response.
|
||||
function installFakeXHR (program) {
|
||||
const instances = [];
|
||||
class FakeXHR extends EventTarget {
|
||||
constructor () {
|
||||
super();
|
||||
this.readyState = 0;
|
||||
this.status = 0;
|
||||
this.statusText = '';
|
||||
this.responseText = '';
|
||||
this.response = null;
|
||||
this.responseType = '';
|
||||
this.responseURL = '';
|
||||
this.withCredentials = false;
|
||||
this._reqHeaders = {};
|
||||
this._respHeaders = {};
|
||||
this.onreadystatechange = null;
|
||||
this.onprogress = null;
|
||||
instances.push(this);
|
||||
}
|
||||
open (method, url) { this.method = method; this.url = url; }
|
||||
setRequestHeader (name, value) { this._reqHeaders[name.toLowerCase()] = String(value); }
|
||||
getResponseHeader (name) { return this._respHeaders[name.toLowerCase()] ?? null; }
|
||||
abort () { this.dispatchEvent(new Event('abort')); }
|
||||
send (body) {
|
||||
this.reqBody = body;
|
||||
queueMicrotask(() => program(this));
|
||||
}
|
||||
// Test helpers the program uses to emit response phases.
|
||||
_setHeaders (status, headers = {}) {
|
||||
this.status = status;
|
||||
this.statusText = String(status);
|
||||
for ( const [ k, v ] of Object.entries(headers) ) this._respHeaders[k.toLowerCase()] = v;
|
||||
}
|
||||
_headersReceived () { this.readyState = 2; this.onreadystatechange?.(); }
|
||||
_progress (chunk) { this.responseText += chunk; this.readyState = 3; this.onprogress?.(); }
|
||||
_done () { this.readyState = 4; this.onreadystatechange?.(); this.dispatchEvent(new Event('load')); }
|
||||
_networkError () { this.dispatchEvent(new Event('error')); }
|
||||
}
|
||||
globalThis.XMLHttpRequest = FakeXHR;
|
||||
return instances;
|
||||
}
|
||||
|
||||
// A simple buffered JSON/text response.
|
||||
const respond = ({ status = 200, contentType = 'application/json', body = '' }) => xhr => {
|
||||
xhr._setHeaders(status, { 'content-type': contentType });
|
||||
xhr._headersReceived();
|
||||
xhr.responseText = typeof body === 'string' ? body : JSON.stringify(body);
|
||||
xhr._done();
|
||||
};
|
||||
|
||||
let savedXHR;
|
||||
beforeEach(() => { savedXHR = globalThis.XMLHttpRequest; });
|
||||
afterEach(() => {
|
||||
globalThis.XMLHttpRequest = savedXHR;
|
||||
delete globalThis.puter;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchUrl', () => {
|
||||
it('adds a Bearer header from the live puter.authToken when includePuterAuth', async () => {
|
||||
globalThis.puter = { authToken: 'tok-123' };
|
||||
const xhrs = installFakeXHR(respond({ body: { ok: true } }));
|
||||
await fetchUrl('https://api.example/whoami', { includePuterAuth: true });
|
||||
expect(xhrs[0]._reqHeaders['authorization']).toBe('Bearer tok-123');
|
||||
});
|
||||
|
||||
it('omits the Bearer header when includePuterAuth is false', async () => {
|
||||
globalThis.puter = { authToken: 'tok-123' };
|
||||
const xhrs = installFakeXHR(respond({ body: {} }));
|
||||
await fetchUrl('https://api.example/public');
|
||||
expect(xhrs[0]._reqHeaders['authorization']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('passes through custom headers and body, skipping nullish header values', async () => {
|
||||
const xhrs = installFakeXHR(respond({ body: {} }));
|
||||
await fetchUrl('https://api.example/x', {
|
||||
method: 'POST',
|
||||
headers: { 'puter-auth': 'w-tok', 'x-skip': undefined },
|
||||
body: 'payload',
|
||||
});
|
||||
expect(xhrs[0].method).toBe('POST');
|
||||
expect(xhrs[0]._reqHeaders['puter-auth']).toBe('w-tok');
|
||||
expect('x-skip' in xhrs[0]._reqHeaders).toBe(false);
|
||||
expect(xhrs[0].reqBody).toBe('payload');
|
||||
});
|
||||
|
||||
it('resolves ok:true on 200', async () => {
|
||||
installFakeXHR(respond({ status: 200, body: { a: 1 } }));
|
||||
const resp = await fetchUrl('https://api.example/x');
|
||||
expect(resp.ok).toBe(true);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(await resp.json()).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it('resolves (not rejects) with ok:false on 404 and 500', async () => {
|
||||
for ( const status of [ 404, 500 ] ) {
|
||||
installFakeXHR(respond({ status, body: { error: 'nope' } }));
|
||||
const resp = await fetchUrl('https://api.example/x');
|
||||
expect(resp.ok).toBe(false);
|
||||
expect(resp.status).toBe(status);
|
||||
expect(await resp.json()).toEqual({ error: 'nope' });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects on a network error', async () => {
|
||||
installFakeXHR(xhr => xhr._networkError());
|
||||
await expect(fetchUrl('https://api.example/x')).rejects.toThrow(/failed/);
|
||||
});
|
||||
|
||||
it('exposes text(), json(), and blob() accessors', async () => {
|
||||
installFakeXHR(respond({ contentType: 'application/json', body: { hello: 'world' } }));
|
||||
const resp = await fetchUrl('https://api.example/x');
|
||||
expect(await resp.text()).toBe('{"hello":"world"}');
|
||||
expect(await resp.json()).toEqual({ hello: 'world' });
|
||||
const blob = await resp.blob();
|
||||
expect(blob).toBeInstanceOf(Blob);
|
||||
expect(blob.type).toBe('application/json');
|
||||
expect(blob.size).toBe('{"hello":"world"}'.length);
|
||||
});
|
||||
|
||||
it('streams parsed NDJSON objects across chunk boundaries', async () => {
|
||||
installFakeXHR(xhr => {
|
||||
xhr._setHeaders(200, { 'content-type': 'application/x-ndjson' });
|
||||
xhr._headersReceived();
|
||||
// A JSON object split across two progress deltas, plus a full line.
|
||||
xhr._progress('{"n":1}\n{"n":');
|
||||
xhr._progress('2}\n{"n":3}\n');
|
||||
xhr._done();
|
||||
});
|
||||
const resp = await fetchUrl('https://api.example/stream');
|
||||
const got = [];
|
||||
for await ( const obj of resp.stream() ) got.push(obj);
|
||||
expect(got).toEqual([ { n: 1 }, { n: 2 }, { n: 3 } ]);
|
||||
});
|
||||
|
||||
describe('401 reauth', () => {
|
||||
it('triggers reauth once and replays with the fresh token', async () => {
|
||||
const triggerReauth = vi.fn(async () => { globalThis.puter.authToken = 'fresh'; });
|
||||
globalThis.puter = { authToken: 'stale', env: 'web', triggerReauth };
|
||||
|
||||
let call = 0;
|
||||
installFakeXHR(xhr => {
|
||||
call++;
|
||||
if ( call === 1 ) {
|
||||
// first attempt: 401 reauth_required
|
||||
return respond({ status: 401, body: { code: 'reauth_required', reason: 'x', auth_id: 'a' } })(xhr);
|
||||
}
|
||||
// replay carries the fresh token and succeeds
|
||||
expect(xhr._reqHeaders['authorization']).toBe('Bearer fresh');
|
||||
return respond({ status: 200, body: { ok: true } })(xhr);
|
||||
});
|
||||
|
||||
const resp = await fetchUrl('https://api.example/x', { includePuterAuth: true });
|
||||
expect(triggerReauth).toHaveBeenCalledTimes(1);
|
||||
expect(resp.ok).toBe(true);
|
||||
expect(await resp.json()).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('does not loop: a second 401 after replay surfaces as ok:false', async () => {
|
||||
const triggerReauth = vi.fn(async () => {});
|
||||
globalThis.puter = { authToken: 'stale', env: 'web', triggerReauth };
|
||||
|
||||
installFakeXHR(respond({ status: 401, body: { code: 'reauth_required' } }));
|
||||
const resp = await fetchUrl('https://api.example/x', { includePuterAuth: true });
|
||||
// reauth attempted exactly once; replayed request's 401 is returned.
|
||||
expect(triggerReauth).toHaveBeenCalledTimes(1);
|
||||
expect(resp.ok).toBe(false);
|
||||
expect(resp.status).toBe(401);
|
||||
});
|
||||
|
||||
it('plain 401 (no reauth code) resolves ok:false without triggering reauth', async () => {
|
||||
const triggerReauth = vi.fn();
|
||||
globalThis.puter = { authToken: 't', env: 'web', triggerReauth };
|
||||
installFakeXHR(respond({ status: 401, body: { message: 'Unauthorized' } }));
|
||||
const resp = await fetchUrl('https://api.example/x', { includePuterAuth: true });
|
||||
expect(triggerReauth).not.toHaveBeenCalled();
|
||||
expect(resp.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('API call logging', () => {
|
||||
const makeLogger = () => ({ isEnabled: () => true, logRequest: vi.fn() });
|
||||
|
||||
it('logs on success when the logger is enabled', async () => {
|
||||
const apiCallLogger = makeLogger();
|
||||
globalThis.puter = { apiCallLogger };
|
||||
installFakeXHR(respond({ status: 200, body: {} }));
|
||||
await fetchUrl('https://api.example/x');
|
||||
expect(apiCallLogger.logRequest).toHaveBeenCalledTimes(1);
|
||||
expect(apiCallLogger.logRequest.mock.calls[0][0].error).toBeNull();
|
||||
});
|
||||
|
||||
it('logs an error entry on a 4xx', async () => {
|
||||
const apiCallLogger = makeLogger();
|
||||
globalThis.puter = { apiCallLogger };
|
||||
installFakeXHR(respond({ status: 404, body: { code: 'not_found' } }));
|
||||
await fetchUrl('https://api.example/x');
|
||||
expect(apiCallLogger.logRequest).toHaveBeenCalledTimes(1);
|
||||
const entry = apiCallLogger.logRequest.mock.calls[0][0];
|
||||
expect(entry.error).toMatchObject({ code: 'not_found' });
|
||||
expect(entry.result).toBeNull();
|
||||
});
|
||||
|
||||
it('uses logContext for semantic service/operation when provided', async () => {
|
||||
const apiCallLogger = makeLogger();
|
||||
globalThis.puter = { apiCallLogger };
|
||||
installFakeXHR(respond({ status: 200, body: { u: 1 } }));
|
||||
await fetchUrl('https://api.example/whoami', {
|
||||
logContext: { service: 'auth', operation: 'whoami', params: {} },
|
||||
});
|
||||
const entry = apiCallLogger.logRequest.mock.calls[0][0];
|
||||
expect(entry).toMatchObject({ service: 'auth', operation: 'whoami' });
|
||||
expect(entry.result).toEqual({ u: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -155,6 +155,64 @@ function replayXhrAfterReauth (response, success_cb, error_cb, resolve_func, rej
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared 401 reauth policy for a parsed response body. Drives the env-specific
|
||||
* reauth flow on the Puter class and tells the caller what to do next, so the
|
||||
* generic XHR path (`handle_resp`) and the fetch replacement (`fetchUrl`) apply
|
||||
* the exact same policy. The driver-call handler (`driverCall_`) keeps its own
|
||||
* replay because it must preserve streaming/transform semantics on retry.
|
||||
*
|
||||
* Recognised backend signals:
|
||||
* - `reauth_required` (v2 `authProbe`): legacy v1 tokens, revoked sessions,
|
||||
* and expired sessions beyond the silent re-mint window.
|
||||
* - `token_auth_failed` (legacy `APIError.create('token_auth_failed')`):
|
||||
* token no longer valid, prompt re-login (web env only).
|
||||
*
|
||||
* @param {Object} resp - The parsed response body.
|
||||
* @returns {Promise<{action: 'replay'}|{action: 'reject', error: Object}|null>}
|
||||
* `replay` when the caller should re-issue the request once with the fresh
|
||||
* token, `reject` with the error to surface, or `null` when this is not a
|
||||
* reauth-recoverable 401 and the caller should handle it normally.
|
||||
*/
|
||||
async function resolveReauth (resp) {
|
||||
if ( resp?.code === 'reauth_required' ) {
|
||||
try {
|
||||
await puter.triggerReauth({
|
||||
reason: resp.reason,
|
||||
auth_id: resp.auth_id,
|
||||
});
|
||||
return { action: 'replay' };
|
||||
} catch ( e ) {
|
||||
return {
|
||||
action: 'reject',
|
||||
error: {
|
||||
status: 401,
|
||||
code: 'reauth_required',
|
||||
reason: resp.reason,
|
||||
auth_id: resp.auth_id,
|
||||
message: e?.message || 'Reauthentication required',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if ( resp?.code === 'token_auth_failed' && puter.env === 'web' ) {
|
||||
try {
|
||||
puter.resetAuthToken();
|
||||
await puter.ui.authenticateWithPuter();
|
||||
} catch (e) {
|
||||
return {
|
||||
action: 'reject',
|
||||
error: {
|
||||
error: {
|
||||
code: 'auth_canceled', message: 'Authentication canceled',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an HTTP response by invoking appropriate callback functions and resolving or rejecting a promise.
|
||||
*
|
||||
@@ -176,50 +234,17 @@ async function handle_resp (success_cb, error_cb, resolve_func, reject_func, res
|
||||
const resp = await parseResponse(response);
|
||||
// error - unauthorized
|
||||
if ( response.status === 401 ) {
|
||||
// v2 reauth signal. The backend `authProbe` middleware returns
|
||||
// `401 { code: 'reauth_required', reason, auth_id }` for legacy
|
||||
// v1 tokens, revoked sessions, and expired sessions beyond the
|
||||
// silent re-mint window. Drive the env-specific reauth flow on
|
||||
// the Puter class, then replay the original request with the
|
||||
// new token.
|
||||
if ( resp?.code === 'reauth_required' ) {
|
||||
try {
|
||||
await puter.triggerReauth({
|
||||
reason: resp.reason,
|
||||
auth_id: resp.auth_id,
|
||||
});
|
||||
if ( replayXhrAfterReauth(response, success_cb, error_cb, resolve_func, reject_func) ) {
|
||||
return;
|
||||
}
|
||||
} catch ( e ) {
|
||||
const err = {
|
||||
status: 401,
|
||||
code: 'reauth_required',
|
||||
reason: resp.reason,
|
||||
auth_id: resp.auth_id,
|
||||
message: e?.message || 'Reauthentication required',
|
||||
};
|
||||
if ( error_cb && typeof error_cb === 'function' ) error_cb(err);
|
||||
return reject_func(err);
|
||||
}
|
||||
}
|
||||
// Backend signals "token is no longer valid, prompt re-login" via
|
||||
// the legacy `token_auth_failed` code (matches v1 backend's
|
||||
// APIError.create('token_auth_failed') shape). Trigger the same
|
||||
// reset + re-auth flow the driver-call handler uses, so stale or
|
||||
// legacy-shaped tokens auto-recover instead of bubbling a raw
|
||||
// "Unauthorized" up to every caller.
|
||||
if ( resp?.code === 'token_auth_failed' && puter.env === 'web' ) {
|
||||
try {
|
||||
puter.resetAuthToken();
|
||||
await puter.ui.authenticateWithPuter();
|
||||
} catch (e) {
|
||||
return reject_func({
|
||||
error: {
|
||||
code: 'auth_canceled', message: 'Authentication canceled',
|
||||
},
|
||||
});
|
||||
const reauth = await resolveReauth(resp);
|
||||
if ( reauth?.action === 'replay' ) {
|
||||
// Replay the original request with the fresh token. If the replay
|
||||
// can't be scheduled (no captured request, or already retried),
|
||||
// fall through to the generic Unauthorized rejection below.
|
||||
if ( replayXhrAfterReauth(response, success_cb, error_cb, resolve_func, reject_func) ) {
|
||||
return;
|
||||
}
|
||||
} else if ( reauth?.action === 'reject' ) {
|
||||
if ( error_cb && typeof error_cb === 'function' ) error_cb(reauth.error);
|
||||
return reject_func(reauth.error);
|
||||
}
|
||||
// if error callback is provided, call it
|
||||
if ( error_cb && typeof error_cb === 'function' )
|
||||
@@ -786,5 +811,5 @@ const isVideoInput = (url) => {
|
||||
};
|
||||
|
||||
export {
|
||||
arrayBufferToDataUri, blob_to_url, blobToDataUri, driverCall, handle_error, handle_resp, initXhr, isVideoInput, make_driver_method, parseResponse, setupXhrEventHandlers, uuidv4,
|
||||
arrayBufferToDataUri, blob_to_url, blobToDataUri, createDeferred, driverCall, handle_error, handle_resp, initXhr, isVideoInput, make_driver_method, parseResponse, resolveReauth, setupXhrEventHandlers, uuidv4,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as utils from '../lib/utils.js';
|
||||
import { fetchUrl } from '../lib/PuterClient.js';
|
||||
import getAbsolutePathForApp from './FileSystem/utils/getAbsolutePathForApp.js';
|
||||
|
||||
const normalizeTTSProvider = (value) => {
|
||||
@@ -59,10 +60,10 @@ class AI {
|
||||
*/
|
||||
async listModels (provider) {
|
||||
// Prefer the public API endpoint and fall back to the legacy driver call if needed.
|
||||
const headers = this.authToken ? { Authorization: `Bearer ${this.authToken}` } : {};
|
||||
|
||||
const tryFetchModels = async () => {
|
||||
const resp = await fetch(`${this.APIOrigin }/puterai/chat/models/details`, { headers });
|
||||
const resp = await fetchUrl(`${this.APIOrigin }/puterai/chat/models/details`, {
|
||||
includePuterAuth: !! this.authToken,
|
||||
});
|
||||
if ( ! resp.ok ) return null;
|
||||
const data = await resp.json();
|
||||
const models = Array.isArray(data?.models) ? data.models : [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as utils from '../lib/utils.js';
|
||||
import { fetchUrl } from '../lib/PuterClient.js';
|
||||
|
||||
class Apps {
|
||||
/**
|
||||
@@ -228,13 +229,9 @@ class Apps {
|
||||
};
|
||||
}
|
||||
|
||||
const resp = await fetch(
|
||||
const resp = await fetchUrl(
|
||||
`${puter.APIOrigin}/apps/nameAvailable?name=${encodeURIComponent(name)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${puter.authToken}`,
|
||||
},
|
||||
},
|
||||
{ includePuterAuth: true },
|
||||
);
|
||||
const result = await resp.json();
|
||||
if ( ! resp.ok ) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as utils from '../lib/utils.js';
|
||||
import { fetchUrl } from '../lib/PuterClient.js';
|
||||
import PuterDialog from './PuterDialog.js';
|
||||
import { hasUserActivation, openAuthPopup } from '../lib/auth-popup.js';
|
||||
|
||||
@@ -73,7 +74,7 @@ class Auth {
|
||||
(async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const result = await fetch(`${this.APIOrigin}/login/wait`, {
|
||||
const result = await fetchUrl(`${this.APIOrigin}/login/wait`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -249,79 +250,19 @@ class Auth {
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${this.APIOrigin}/whoami`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resp.json();
|
||||
|
||||
// Log the response
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'whoami',
|
||||
params: {},
|
||||
result: result,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
// Log the error
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'whoami',
|
||||
params: {},
|
||||
error: {
|
||||
message: error.message || error.toString(),
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const resp = await fetchUrl(`${this.APIOrigin}/whoami`, {
|
||||
includePuterAuth: true,
|
||||
logContext: { service: 'auth', operation: 'whoami', params: {} },
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
async getMonthlyUsage () {
|
||||
try {
|
||||
const resp = await fetch(`${this.APIOrigin}/metering/usage`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resp.json();
|
||||
|
||||
// Log the response
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'usage',
|
||||
params: {},
|
||||
result: result,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
// Log the error
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'usage',
|
||||
params: {},
|
||||
error: {
|
||||
message: error.message || error.toString(),
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const resp = await fetchUrl(`${this.APIOrigin}/metering/usage`, {
|
||||
includePuterAuth: true,
|
||||
logContext: { service: 'auth', operation: 'usage', params: {} },
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
async getDetailedAppUsage (appId) {
|
||||
@@ -329,79 +270,19 @@ class Auth {
|
||||
throw new Error('appId is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${this.APIOrigin}/metering/usage/${appId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resp.json();
|
||||
|
||||
// Log the response
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'detailed_app_usage',
|
||||
params: { appId },
|
||||
result: result,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
// Log the error
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'detailed_app_usage',
|
||||
params: { appId },
|
||||
error: {
|
||||
message: error.message || error.toString(),
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const resp = await fetchUrl(`${this.APIOrigin}/metering/usage/${appId}`, {
|
||||
includePuterAuth: true,
|
||||
logContext: { service: 'auth', operation: 'detailed_app_usage', params: { appId } },
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
async getGlobalUsage () {
|
||||
try {
|
||||
const resp = await fetch(`${this.APIOrigin}/metering/globalUsage`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resp.json();
|
||||
|
||||
// Log the response
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'global_usage',
|
||||
params: {},
|
||||
result: result,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
// Log the error
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'auth',
|
||||
operation: 'global_usage',
|
||||
params: {},
|
||||
error: {
|
||||
message: error.message || error.toString(),
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const resp = await fetchUrl(`${this.APIOrigin}/metering/globalUsage`, {
|
||||
includePuterAuth: true,
|
||||
logContext: { service: 'auth', operation: 'global_usage', params: {} },
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fetchUrl } from '../lib/PuterClient.js';
|
||||
|
||||
class FetchDriverCallBackend {
|
||||
constructor ({ getAPIOrigin, getAuthToken }) {
|
||||
this.getAPIOrigin = getAPIOrigin;
|
||||
@@ -5,39 +7,22 @@ class FetchDriverCallBackend {
|
||||
this.response_handlers = this.constructor.response_handlers;
|
||||
}
|
||||
|
||||
// Dispatched by response content type. The handlers consume the
|
||||
// fetchUrl PuterResponse: stream() yields parsed NDJSON lines, json()/blob()
|
||||
// read the buffered body.
|
||||
static response_handlers = {
|
||||
'application/x-ndjson': async resp => {
|
||||
const Stream = async function* Stream (readableStream) {
|
||||
const reader = readableStream.getReader();
|
||||
let value, done;
|
||||
while ( !done ) {
|
||||
({ value, done } = await reader.read());
|
||||
if ( done ) break;
|
||||
const parts = (new TextDecoder().decode(value).split('\n'));
|
||||
for ( const part of parts ) {
|
||||
if ( part.trim() === '' ) continue;
|
||||
yield JSON.parse(part);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Stream(resp.body);
|
||||
},
|
||||
'application/json': async resp => {
|
||||
return await resp.json();
|
||||
},
|
||||
'application/octet-stream': async resp => {
|
||||
return await resp.blob();
|
||||
},
|
||||
'application/x-ndjson': resp => resp.stream(),
|
||||
'application/json': resp => resp.json(),
|
||||
'application/octet-stream': resp => resp.blob(),
|
||||
};
|
||||
|
||||
async call ({ driver, method_name, parameters }) {
|
||||
try {
|
||||
const resp = await fetch(`${this.getAPIOrigin()}/drivers/call`, {
|
||||
const resp = await fetchUrl(`${this.getAPIOrigin()}/drivers/call`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;actually=json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
'interface': driver.iface_name,
|
||||
...(driver.service_name
|
||||
@@ -170,42 +155,13 @@ class Drivers {
|
||||
}
|
||||
|
||||
async list () {
|
||||
try {
|
||||
const resp = await fetch(`${this.APIOrigin}/lsmod`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ this.authToken}`,
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const list = await resp.json();
|
||||
|
||||
// Log the response
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'drivers',
|
||||
operation: 'list',
|
||||
params: {},
|
||||
result: list.interfaces,
|
||||
});
|
||||
}
|
||||
|
||||
return list.interfaces;
|
||||
} catch ( error ) {
|
||||
// Log the error
|
||||
if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
|
||||
globalThis.puter.apiCallLogger.logRequest({
|
||||
service: 'drivers',
|
||||
operation: 'list',
|
||||
params: {},
|
||||
error: {
|
||||
message: error.message || error.toString(),
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const resp = await fetchUrl(`${this.APIOrigin}/lsmod`, {
|
||||
method: 'POST',
|
||||
includePuterAuth: true,
|
||||
logContext: { service: 'drivers', operation: 'list', params: {} },
|
||||
});
|
||||
const list = await resp.json();
|
||||
return list.interfaces;
|
||||
}
|
||||
|
||||
async get (iface_name, service_name) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fetchUrl } from '../../lib/PuterClient.js';
|
||||
|
||||
export default puter => class Batch {
|
||||
constructor () {
|
||||
this.form = new FormData();
|
||||
@@ -34,15 +36,11 @@ export default puter => class Batch {
|
||||
this.form.append('operation', JSON.stringify(operation));
|
||||
}
|
||||
|
||||
// Send Request
|
||||
const res = await fetch(`${puter.APIOrigin}/batch`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${puter.authToken}`,
|
||||
...(['web', 'app'].includes(puter.env) ? {
|
||||
Origin: 'https://puter.work',
|
||||
} : {}),
|
||||
},
|
||||
// Send Request. The Content-Type (multipart boundary) is set by the
|
||||
// transport from the FormData body, so it is not passed explicitly.
|
||||
const res = await fetchUrl(`${puter.APIOrigin}/batch`, {
|
||||
method: 'POST',
|
||||
includePuterAuth: true,
|
||||
body: this.form,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from '../../../lib/path.js';
|
||||
import { fetchUrl } from '../../../lib/PuterClient.js';
|
||||
import * as utils from '../../../lib/utils.js';
|
||||
import { showUsageLimitDialog } from '../../../modules/UsageLimitDialog.js';
|
||||
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
|
||||
@@ -178,10 +179,9 @@ const toRequestError = (response, body, fallbackMessage) => {
|
||||
};
|
||||
|
||||
const postJson = async (apiOrigin, authToken, endpoint, payload) => {
|
||||
const response = await fetch(`${apiOrigin}${endpoint}`, {
|
||||
const response = await fetchUrl(`${apiOrigin}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: createApiHeaders(authToken),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const body = await parseFetchResponseBody(response);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as utils from '../lib/utils.js';
|
||||
import { fetchUrl } from '../lib/PuterClient.js';
|
||||
|
||||
const createDeferred = () => {
|
||||
let resolve;
|
||||
@@ -54,7 +55,7 @@ class KV {
|
||||
(async () => {
|
||||
await this.gui_cache_init.promise;
|
||||
this.gui_cache_init = null;
|
||||
const resp = await fetch(`${this.APIOrigin}/drivers/call`, {
|
||||
const resp = await fetchUrl(`${this.APIOrigin}/drivers/call`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;actually=json',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fetchUrl } from '../lib/PuterClient.js';
|
||||
|
||||
class PuterPeerServerConnectionEvent extends Event {
|
||||
conn;
|
||||
user;
|
||||
@@ -389,11 +391,11 @@ class Peer {
|
||||
if ( this.#turnFailed ) return;
|
||||
if ( this.#turnServers && Date.now() - this.#turnStartedAt < this.#turnTTL * 1000 ) return;
|
||||
|
||||
const response = await fetch(`${this.APIOrigin}/peer/generate-turn`, {
|
||||
const response = await fetchUrl(`${this.APIOrigin}/peer/generate-turn`, {
|
||||
method: 'POST',
|
||||
includePuterAuth: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -411,7 +413,7 @@ class Peer {
|
||||
|
||||
async #loadMetadata () {
|
||||
if ( this.#signallerUrl ) return;
|
||||
const response = await fetch(`${this.APIOrigin}/peer/signaller-info`);
|
||||
const response = await fetchUrl(`${this.APIOrigin}/peer/signaller-info`);
|
||||
if ( ! response.ok ) {
|
||||
throw new Error('Failed to get signaller info from Puter.');
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fetchUrl } from '../lib/PuterClient.js';
|
||||
|
||||
export default class Perms {
|
||||
constructor (puter) {
|
||||
this.puter = puter;
|
||||
@@ -12,10 +14,10 @@ export default class Perms {
|
||||
}
|
||||
async req_ (route, body) {
|
||||
try {
|
||||
const resp = await fetch(this.APIOrigin + route, {
|
||||
const resp = await fetchUrl(this.APIOrigin + route, {
|
||||
method: body ? 'POST' : 'GET',
|
||||
includePuterAuth: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
|
||||
@@ -71,6 +71,9 @@ export class WorkersHandler {
|
||||
req.headers.set('puter-auth', puter.authToken);
|
||||
}
|
||||
req.headers.delete('x-puter-no-auth');
|
||||
// Passthrough to a user worker URL: takes the fetch Request interface and
|
||||
// returns the raw fetch Response as public API, so it stays on fetch
|
||||
// rather than the XHR-based fetchUrl (whose response is a narrower shape).
|
||||
return fetch(req);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import EventListener from '../../lib/EventListener.js';
|
||||
import { fetchUrl } from '../../lib/PuterClient.js';
|
||||
import { errors } from './parsers.js';
|
||||
import { PWispHandler } from './PWispHandler.js';
|
||||
const texten = new TextEncoder();
|
||||
@@ -27,10 +28,10 @@ export class PSocket extends EventListener {
|
||||
}
|
||||
if ( ! wispInfo.handler ) {
|
||||
// first launch -- lets init the socket
|
||||
const { token: wispToken, server: wispServer } = (await (await fetch(`${puter.APIOrigin }/wisp/relay-token/create`, {
|
||||
const { token: wispToken, server: wispServer } = (await (await fetchUrl(`${puter.APIOrigin }/wisp/relay-token/create`, {
|
||||
method: 'POST',
|
||||
includePuterAuth: !! puter.authToken,
|
||||
headers: {
|
||||
Authorization: puter.authToken ? `Bearer ${puter.authToken}` : '',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
|
||||
Reference in New Issue
Block a user