UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

125 lines • 5.16 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { abortable } from "../../utils/abort.js"; import { isSystemClock, SystemClock } from "../../utils/clock.js"; /** * Caches a short-lived access token, refreshing before expiry. * * Subclasses implement `refreshPair` with their provider's grant. An * in-flight refresh is shared so concurrent callers cost one round-trip, * and `bufferSeconds` refreshes early so a token never expires * mid-request. * * Expiry is read on `clock`, which also times the waits between retries of * the calls the manager signs. A mount hands it the workspace's clock * through `bindClock` and gives it back through `releaseClock`; absent is * the system clock. */ export class TokenManager { accessToken = null; expiresAt = 0; inflight = null; bufferSeconds; clock; /** Seconds a call the manager signs may stall; absent is the client default. */ timeout; holders = new Set(); constructor(bufferSeconds = 300, clock, timeout) { this.bufferSeconds = bufferSeconds; this.clock = clock ?? new SystemClock(); this.timeout = timeout; } /** * Read expiry and time retry waits on `clock` while `owner` holds the * manager. A workspace holds it from the moment its mounts take its clock * until it unmounts the VFS or closes; holding again, or letting go twice, * changes nothing. Expiry and waits can follow one clock only, so while * another owner holds the manager a different clock is refused; two system * clocks agree. A refreshed token's expiry was read on the clock it * replaces, so it is dropped and the next call refreshes; a seeded token * that never expires is kept. * * @param owner who holds it, released by the same key. * @throws Error when another owner holds the manager on another clock. */ bindClock(clock, owner) { const same = clock === this.clock || (isSystemClock(clock) && isSystemClock(this.clock)); if (!same) { if ([...this.holders].some((held) => held !== owner)) { throw new Error('this client is held by a workspace on another clock; ' + 'unmount or uninstall it there, or give both one clock'); } this.clock = clock; if (this.expiresAt !== Number.POSITIVE_INFINITY) this.expiresAt = 0; } this.holders.add(owner); } /** Let `owner` go; once none holds, another clock may bind. */ releaseClock(owner) { this.holders.delete(owner); } /** * Preload the cache with a token minted outside the refresh flow. * `expiresAt` is epoch seconds after which `getToken` refreshes again; * pass `Number.POSITIVE_INFINITY` for a token the manager can never * replace itself. */ seed(token, expiresAt) { this.accessToken = token; this.expiresAt = expiresAt; } /** * The token, refreshed when it is due. `signal` is the waiting call's: its * stop ends its own wait, and a refresh it started answers to it. */ async getToken(signal) { for (;;) { if (this.accessToken !== null && this.clock.now() < this.expiresAt) { return this.accessToken; } const shared = this.inflight; if (shared === null) break; // A waiter's own stop ends its wait alone. The refresh answers to the // call that started it: when that call stopped, a waiter still running // refreshes for itself, as the next one to take Python's lock does. try { return await abortable(shared.token, signal); } catch (err) { if (shared.signal?.aborted !== true || signal?.aborted === true) throw err; if (this.inflight === shared) this.inflight = null; } } const own = { token: this.refresh(), signal }; this.inflight = own; try { return await own.token; } finally { if (this.inflight === own) this.inflight = null; } } async refresh() { const [token, expiresIn] = await this.refreshPair(); this.accessToken = token; this.expiresAt = this.clock.now() + expiresIn - this.bufferSeconds; return token; } } //# sourceMappingURL=oauth.js.map