@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
342 lines (328 loc) • 10.6 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/google/index.js
var google_exports = {};
__export(google_exports, {
GoogleAccount: () => GoogleAccount,
GoogleGrant: () => GoogleGrant,
createGoogleOAuth2: () => createGoogleOAuth2,
default: () => google_default
});
module.exports = __toCommonJS(google_exports);
// src/consts.js
var vault = /* @__PURE__ */ new WeakMap();
// src/class/OAuth2Client.js
var import_props = require("@randajan/props");
var OAuth2Client = class {
constructor(Grant, options = {}) {
const grant = new Grant(this, options);
(0, import_props.virtuals)(this, {
name: (_) => grant.name,
uidKey: (_) => grant.uidKey
});
vault.set(this, grant);
}
account(credentials, ...args) {
const grant = vault.get(this);
const c = grant.getCredentials ? grant.getCredentials(credentials, ...args) : credentials;
return c instanceof Promise ? c.then((cr) => grant.createAccount(cr)) : grant.createAccount(c);
}
getInitAuthURL(options = {}) {
const grant = vault.get(this);
try {
return grant.getInitAuthURL(options);
} catch (err) {
return grant.fallbackRedirect(1, err);
}
}
getExitAuthURL({ code, state }, context) {
const grant = vault.get(this);
try {
return grant.getExitAuthURL({ code, state }, context);
} catch (err) {
return grant.fallbackRedirect(2, err);
}
}
};
// src/google/GoogleAccount.js
var import_googleapis = require("googleapis");
var import_props3 = require("@randajan/props");
// src/class/OAuth2Account.js
var import_props2 = require("@randajan/props");
// src/tools.js
var extendURL = (url, query = {}) => {
const u = new URL(url);
for (let i in query) {
if (query[i] != null) {
u.searchParams.append(i, query[i]);
}
}
return u.toString();
};
var isValidURL = (str) => {
try {
new URL(str);
} catch (e) {
return false;
}
return true;
};
var validateURL = (required, url, errProp) => {
if (!url && !required) {
return;
}
if (isValidURL(url)) {
return url;
}
throw new Error(`${errProp} is not a valid URL`);
};
var validateFn = (required, fn, errProp) => {
if (!fn && !required) {
return;
}
if (typeof fn === "function") {
return fn;
}
throw new Error(`${errProp} is not a valid function`);
};
var formatCredentials = (credentials = {}) => {
const c = { ...credentials };
if (c.expires_in && !c.expiry_date) {
c.expiry_date = Date.now() + c.expires_in * 1e3;
}
if (c.refresh_token && !c.expiry_date) {
throw new Error(`OAuth2 credentials 'refresh_token' must be provided with 'expiry_date'`);
}
if (!c.access_token && !c.refresh_token) {
throw new Error(`OAuth2 credentials 'access_token' of 'refresh_token' must be provided`);
}
return c;
};
var strToBase64 = (str) => Buffer.from(str, "utf8").toString("base64");
var strFromBase64 = (strEncoded) => Buffer.from(strEncoded, "base64").toString("utf8");
var objToBase64 = (obj) => strToBase64(JSON.stringify(obj));
var objFromBase64 = (objEncoded) => JSON.parse(strFromBase64(objEncoded));
// src/class/OAuth2Account.js
var OAuth2Account = class {
constructor(client, credentials = {}) {
credentials = formatCredentials(credentials);
(0, import_props2.solids)(this, {
client,
credentials
}, false);
}
async uid() {
const profile = await this.profile();
const { name, uidKey } = this.client;
const id = profile?.[uidKey];
if (id) {
return `${name}:${id}`;
}
}
async profile() {
return {};
}
async tokens() {
return {};
}
async scopes(scopes) {
const grant = vault.get(this.client);
return grant.effaceScopes(scopes);
}
};
// src/google/GoogleAccount.js
var GoogleAccount = class extends OAuth2Account {
constructor(client, credentials = {}) {
super(client, credentials);
const grant = vault.get(client);
const auth = grant.createAuth();
auth.setCredentials(this.credentials);
auth.on("tokens", (_) => {
grant.onRenew(this);
});
(0, import_props3.solids)(this, {
auth
});
}
oauth2() {
return import_googleapis.google.oauth2({ auth: this.auth, version: "v2" });
}
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 super.scopes(auth.credentials.scope);
}
const { token } = await auth.getAccessToken();
if (!token) {
return [];
}
const info = await auth.getTokenInfo(token);
if (!info) {
return [];
}
return super.scopes(info.scopes);
}
};
// src/google/GoogleGrant.js
var import_googleapis2 = require("googleapis");
// 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/class/OAuth2Grant.js
var OAuth2Grant = class {
static name = "";
static uidKey = "";
static scopePrefix = "";
static scopesCommon = [];
static scopesNoPrefix = [];
static Account = OAuth2Account;
constructor(client, opt = {}) {
const { name, uidKey, scopesCommon } = this.constructor;
this.client = client;
this.name = opt.name || name;
this.uidKey = opt.uidKey || uidKey;
this.isOffline = !!opt.isOffline;
this.clientId = opt.clientId;
this.clientSecret = opt.clientSecret;
this.redirectUri = validateURL(true, opt.redirectUri, "options.redirectUri");
this.fallbackUri = validateURL(true, opt.fallbackUri, "options.fallbackUri");
this.landingUri = validateURL(false, opt.landingUri, "options.landingUri");
this.onAuth = validateFn(true, opt.onAuth, "options.onAuth");
this.onRenew = validateFn(true, opt.onRenew, "options.onRenew");
this.getCredentials = validateFn(false, opt.getCredentials, "options.getCredentials");
this.optExtra = opt.extra || {};
this.scopesRequired = this.effaceScopes([...scopesCommon, ...opt.scopes || []]);
}
createAccount(credentials) {
const { client, constructor: { Account } } = this;
return new Account(client, credentials);
}
effaceScope(scope, withPrefix = false) {
const { scopePrefix, scopesNoPrefix } = this.constructor;
if (scope == null) {
return;
}
scope = String(scope).replace(/[\s\n\r]+/g, " ").trim().toLowerCase();
if (!scopePrefix || scopesNoPrefix.includes(scope)) {
return scope;
}
const sw = scope.startsWith(scopePrefix);
return sw === withPrefix ? scope : withPrefix ? scopePrefix + scope : scope.substring(scopePrefix.length);
}
effaceScopes(scopes, withPrefix = false, withRequired = false) {
const { scopesRequired } = this;
if (typeof scopes === "string") {
scopes = scopes.split(" ");
}
if (!Array.isArray(scopes)) {
scopes = [];
}
if (withRequired) {
scopes = scopes.concat(scopesRequired);
}
const r = /* @__PURE__ */ new Set();
for (let scope of scopes) {
if (scope) {
scope = this.effaceScope(scope, withPrefix);
}
if (scope) {
r.add(scope);
}
}
return [...r];
}
generateAuthUrl(scope, state, extra = {}) {
}
getInitAuthURL(options = {}) {
const { landingUri, state: stateObj, scopes, extra } = options;
if ((landingUri || !this.landingUri) && !isValidURL(landingUri)) {
throw new RedirectError(1, "Bad request. Missing valid 'landingUri'");
}
const scope = this.effaceScopes(scopes, true, true);
const state = objToBase64([landingUri || this.landingUri, stateObj]);
return this.generateAuthUrl(scope, state, extra || {});
}
async swapCodeForTokens(code) {
}
async getExitAuthURL({ code, state }, context) {
if (!code) {
throw new RedirectError(201, "Bad request. Missing 'code'");
}
const [landingUri, stateObj] = objFromBase64(state);
if (!isValidURL(landingUri)) {
throw new RedirectError(202, "Bad request. Missing valid 'state'");
}
const tokens = await this.swapCodeForTokens(code);
const account = this.createAccount(tokens);
const customUri = await this.onAuth(account, { context, landingUri, state: stateObj });
return customUri || landingUri;
}
fallbackRedirect(majorCode, err) {
const c = RedirectError.is(err) ? err.code : 0;
return extendURL(this.fallbackUri, { errorCode: majorCode * 100 + c, errorMessage: err.message });
}
};
// src/google/GoogleGrant.js
var GoogleGrant = class extends OAuth2Grant {
static name = "google";
static uidKey = "id";
static scopePrefix = "https://www.googleapis.com/auth/";
static scopesCommon = ["openid", "userinfo.profile", "userinfo.email"];
static scopesNoPrefix = ["openid"];
static Account = GoogleAccount;
constructor(client, opt = {}) {
super(client, opt);
const { optExtra, clientId, clientSecret, redirectUri } = this;
this.optAuth = { ...optExtra, clientId, clientSecret, redirectUri };
this.auth = this.createAuth();
}
createAuth() {
return new import_googleapis2.google.auth.OAuth2(this.optAuth);
}
generateAuthUrl(scope, state, extra) {
return this.auth.generateAuthUrl({
...extra,
access_type: this.isOffline ? "offline" : "online",
scope,
state
});
}
async swapCodeForTokens(code) {
const { tokens } = await this.auth.getToken(code);
return tokens;
}
};
// src/google/index.js
var createGoogleOAuth2 = (options = {}) => new OAuth2Client(GoogleGrant, options);
var google_default = createGoogleOAuth2;
//# sourceMappingURL=index.js.map