@randajan/oauth2-client
Version:
Lightweight Node.js helper that streamlines OAuth 2.0 and service-account authentication for all Google APIs, giving downstream packages hassle-free token acquisition and refresh
205 lines (198 loc) • 6.1 kB
JavaScript
import {
extendURL,
isValidURL,
objFromBase64,
objToBase64,
validateFn,
validateURL
} from "../chunk-S4SZTRR7.js";
// src/google/scopes.js
var _scopePrefix = "https://www.googleapis.com/auth/";
var _defaultScopes = ["openid", "userinfo.profile", "userinfo.email"];
var effaceScope = (scope, full = false) => {
scope = String(scope).replace(/[\s\n\r]+/g, " ").trim().toLowerCase();
if (scope === "openid") {
return scope;
}
const sw = scope.startsWith(_scopePrefix);
return sw === full ? scope : full ? _scopePrefix + scope : scope.substring(_scopePrefix.length);
};
var effaceScopes = (scopes, full = false, ensureScopes = []) => {
if (typeof scopes === "string") {
scopes = scopes.split(" ");
}
if (!Array.isArray(scopes)) {
scopes = ensureScopes;
} else {
scopes = scopes.concat(ensureScopes);
}
const r = /* @__PURE__ */ new Set();
for (let scope of scopes) {
if (scope) {
scope = effaceScope(scope, full);
}
if (scope) {
r.add(scope);
}
}
return [...r];
};
// src/google/Client.js
import { google as google2 } from "googleapis";
// src/consts.js
var vault = /* @__PURE__ */ new WeakMap();
// src/google/Account.js
import { google } from "googleapis";
import { solids } from "@randajan/props";
var GoogleAccount = class {
constructor(client, credentials = {}) {
const { access_token, refresh_token, expiry_date } = credentials;
if (refresh_token && !expiry_date) {
throw new Error(`OAuth2 account credentials 'refresh_token' must be provided with 'expiry_date'`);
}
if (!access_token && !refresh_token) {
throw new Error(`OAuth2 account credentials 'access_token' of 'refresh_token' must be provided`);
}
const { createAuth, onRenew } = vault.get(client);
const auth = createAuth();
auth.setCredentials(credentials);
auth.on("tokens", (_) => {
onRenew(this);
});
solids(this, {
client,
auth
});
}
oauth2() {
return google.oauth2({ auth: this.auth, version: "v2" });
}
async uid() {
const { id } = await this.profile();
return `google:${id}`;
}
async profile() {
const { data } = await this.oauth2().userinfo.get();
return data;
}
async tokens() {
const { token } = await this.auth.getAccessToken();
return { ...this.auth.credentials, access_token: token };
}
async scopes() {
const { auth } = this;
if (auth.credentials?.scope) {
return effaceScopes(auth.credentials.scope);
}
const { token } = await auth.getAccessToken();
if (!token) {
return [];
}
const info = await auth.getTokenInfo(token);
if (!info) {
return [];
}
return effaceScopes(info.scopes);
}
};
// src/errors.js
var RedirectError = class _RedirectError extends Error {
static is(any) {
return any instanceof _RedirectError;
}
constructor(code, message, options) {
super(message, options);
this.code = code;
}
};
// src/google/Client.js
var GoogleOAuth2 = class {
constructor(options = {}) {
const {
isOffline,
scopes,
clientId,
clientSecret,
redirectUri,
landingUri,
fallbackUri,
onAuth,
onRenew,
getCredentials,
extra
} = options;
const _p = {};
_p.isOffline = !!isOffline;
_p.defaultScopes = effaceScopes(scopes, true, _defaultScopes);
_p.landingUri = validateURL(false, landingUri, "options.landingUri");
_p.fallbackUri = validateURL(true, fallbackUri, "options.fallbackUri");
_p.onAuth = validateFn(true, onAuth, "options.onAuth");
_p.onRenew = validateFn(true, onRenew, "options.onRenew");
_p.getCredentials = validateFn(false, getCredentials, "options.getCredentials");
const commonOptions = {
...extra || {},
clientId,
clientSecret,
redirectUri: validateURL(true, redirectUri, "options.redirectUri")
};
_p.createAuth = (_) => new google2.auth.OAuth2(commonOptions);
_p.auth = _p.createAuth();
this._fallbackRedirect = (majorCode, err) => {
const c = RedirectError.is(err) ? err.code : 0;
return extendURL(_p.fallbackUri, { errorCode: majorCode * 100 + c, errorMessage: err.message });
};
vault.set(this, _p);
}
account(credentials, ...args) {
const { getCredentials } = vault.get(this);
const c = getCredentials ? getCredentials(credentials, ...args) : credentials;
return c instanceof Promise ? c.then((cr) => new GoogleAccount(this, cr)) : new GoogleAccount(this, c);
}
_getInitAuthURL(options = {}) {
const _p = vault.get(this);
const { landingUri, state, scopes, extra } = options;
if ((landingUri || !_p.landingUri) && !isValidURL(landingUri)) {
throw new RedirectError(1, "Bad request. Missing valid 'landingUri'");
}
return _p.auth.generateAuthUrl({
...extra || {},
access_type: _p.isOffline ? "offline" : "online",
scope: effaceScopes(scopes, true, _p.defaultScopes),
state: objToBase64([landingUri || _p.landingUri, state])
});
}
getInitAuthURL(options = {}) {
try {
return this._getInitAuthURL(options);
} catch (err) {
return this._fallbackRedirect(1, err);
}
}
async _getExitAuthURL({ code, state }, context) {
const _p = vault.get(this);
if (!code) {
throw new RedirectError(201, "Bad request. Missing 'code'");
}
const [landingUri, passedState] = objFromBase64(state);
if (!isValidURL(landingUri)) {
throw new RedirectError(202, "Bad request. Missing valid 'state'");
}
const { tokens } = await _p.auth.getToken(code);
const account = new GoogleAccount(this, tokens);
const customUri = await _p.onAuth(account, { context, landingUri, state: passedState });
return customUri || landingUri;
}
getExitAuthURL({ code, state }, context) {
try {
return this._getExitAuthURL({ code, state }, context);
} catch (err) {
return this._fallbackRedirect(2, err);
}
}
};
export {
GoogleAccount,
GoogleOAuth2,
effaceScopes
};
//# sourceMappingURL=index.js.map