UNPKG

@authx/http-proxy-resource

Version:

The AuthX proxy for resources is a flexible HTTP proxy designed to sit in front of a resource.

112 lines 4.09 kB
import { EventEmitter } from "events"; class NoTokenError extends Error { } class TokenDataCacheEntry { basicToken; conf; errorHandler; lastRefreshRequestTime; lastValidRefreshTime = null; forceRefresh = false; currentToken = null; nextToken = null; constructor(basicToken, conf, errorHandler) { this.basicToken = basicToken; this.conf = conf; this.errorHandler = errorHandler; this.lastRefreshRequestTime = Date.now(); } get token() { if (this.currentToken === null) { this.startFetchNextToken(); this.currentToken = this.nextToken; } else if (this.forceRefresh || (this.conf.timeSource() - this.lastRefreshRequestTime) / 1_000 > this.conf.tokenRefreshSeconds) { this.startFetchNextToken(); } if (!this.currentToken) throw new Error("currentToken was unexpectedly null"); return this.currentToken; } get secondsSinceLastRefresh() { return ((this.conf.timeSource() - (this.lastValidRefreshTime ?? this.lastRefreshRequestTime)) / 1_000); } startFetchNextToken() { this.nextToken = this.fetchNextToken(); this.nextToken.then(() => { this.currentToken = this.nextToken; this.lastValidRefreshTime = this.conf.timeSource(); }, (err) => { if (err instanceof NoTokenError) { // This error is cachable, so replace the cache with it this.currentToken = this.nextToken; this.lastValidRefreshTime = this.conf.timeSource(); } else { this.forceRefresh = true; } this.errorHandler(err); }); this.lastRefreshRequestTime = this.conf.timeSource(); } async fetchNextToken() { const response = await this.conf.fetchFunc(`${this.conf.authxUrl}/graphql`, { headers: { "Content-Type": "application/json", Authorization: this.basicToken, }, method: "POST", body: JSON.stringify({ query: "query { viewer { access id user { id } } }", }), }); if (![200, 401].includes(response.status)) { throw new Error(`Unexpected response code from AuthX ${response.status}`); } const responseJson = await response.json(); const ret = responseJson?.data?.viewer; if (!ret) throw new NoTokenError("Response did not include a valid token"); if (!Array.isArray(ret?.access)) throw new NoTokenError("Response did not include scopes"); if (!ret?.id) throw new NoTokenError("Response did not include an authorization id"); if (!ret?.user?.id) throw new NoTokenError("Response did not include a user id"); return ret; } } export class TokenDataCache extends EventEmitter { conf; cache = new Map(); constructor(conf) { super(); this.conf = conf; } getToken(basicToken) { for (const k of [...this.cache.keys()]) { const cacheValue = this.cache.get(k); if (cacheValue && cacheValue.secondsSinceLastRefresh > this.conf.tokenExpirySeconds) { this.cache.delete(k); } } if (!this.cache.has(basicToken)) { this.cache.set(basicToken, new TokenDataCacheEntry(basicToken, this.conf, (err) => { // Only trigger the error event on truly unexpected errors. Other cases are simply handled as auth failures if (!(err instanceof NoTokenError)) { this.emit("error", err); } })); } const ret = this.cache.get(basicToken)?.token; if (!ret) throw new Error("Unexpectedly unable to find TokenDataCacheEntry, cache is invalid"); return ret; } } //# sourceMappingURL=TokenDataCache.js.map