"use strict"; 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/index.ts var src_exports = {}; __export(src_exports, { plugin: () => plugin }); module.exports = __toCommonJS(src_exports); // src/grants/authorizationCode.ts var import_node_crypto = require("node:crypto"); // src/getAccessToken.ts var import_node_fs = require("node:fs"); async function getAccessToken(ctx, { accessTokenUrl, scope, audience, params, grantType, credentialsInBody, clientId, clientSecret }) { console.log("Getting access token", accessTokenUrl); const httpRequest = { method: "POST", url: accessTokenUrl, bodyType: "application/x-www-form-urlencoded", body: { form: [ { name: "grant_type", value: grantType }, ...params ] }, headers: [ { name: "User-Agent", value: "yaak" }, { name: "Accept", value: "application/x-www-form-urlencoded, application/json" }, { name: "Content-Type", value: "application/x-www-form-urlencoded" } ] }; if (scope) httpRequest.body.form.push({ name: "scope", value: scope }); if (scope) httpRequest.body.form.push({ name: "audience", value: audience }); if (credentialsInBody) { httpRequest.body.form.push({ name: "client_id", value: clientId }); httpRequest.body.form.push({ name: "client_secret", value: clientSecret }); } else { const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); httpRequest.headers.push({ name: "Authorization", value }); } const resp = await ctx.httpRequest.send({ httpRequest }); const body = resp.bodyPath ? (0, import_node_fs.readFileSync)(resp.bodyPath, "utf8") : ""; if (resp.status < 200 || resp.status >= 300) { throw new Error("Failed to fetch access token with status=" + resp.status + " and body=" + body); } let response; try { response = JSON.parse(body); } catch { response = Object.fromEntries(new URLSearchParams(body)); } if (response.error) { throw new Error("Failed to fetch access token with " + response.error); } return response; } // src/getOrRefreshAccessToken.ts var import_node_fs2 = require("node:fs"); // src/store.ts async function storeToken(ctx, contextId, response) { if (!response.access_token) { throw new Error(`Token not found in response`); } const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : null; const token = { response, expiresAt }; await ctx.store.set(tokenStoreKey(contextId), token); return token; } async function getToken(ctx, contextId) { return ctx.store.get(tokenStoreKey(contextId)); } async function deleteToken(ctx, contextId) { return ctx.store.delete(tokenStoreKey(contextId)); } async function resetDataDirKey(ctx, contextId) { const key = (/* @__PURE__ */ new Date()).toISOString(); return ctx.store.set(dataDirStoreKey(contextId), key); } async function getDataDirKey(ctx, contextId) { const key = await ctx.store.get(dataDirStoreKey(contextId)) ?? "default"; return `${contextId}::${key}`; } function tokenStoreKey(context_id) { return ["token", context_id].join("::"); } function dataDirStoreKey(context_id) { return ["data_dir", context_id].join("::"); } // src/getOrRefreshAccessToken.ts async function getOrRefreshAccessToken(ctx, contextId, { scope, accessTokenUrl, credentialsInBody, clientId, clientSecret, forceRefresh }) { const token = await getToken(ctx, contextId); if (token == null) { return null; } const now = Date.now(); const isExpired = token.expiresAt && now > token.expiresAt; if (!isExpired && !forceRefresh) { return token; } if (!token.response.refresh_token) { return null; } const httpRequest = { method: "POST", url: accessTokenUrl, bodyType: "application/x-www-form-urlencoded", body: { form: [ { name: "grant_type", value: "refresh_token" }, { name: "refresh_token", value: token.response.refresh_token } ] }, headers: [ { name: "User-Agent", value: "yaak" }, { name: "Accept", value: "application/x-www-form-urlencoded, application/json" }, { name: "Content-Type", value: "application/x-www-form-urlencoded" } ] }; if (scope) httpRequest.body.form.push({ name: "scope", value: scope }); if (credentialsInBody) { httpRequest.body.form.push({ name: "client_id", value: clientId }); httpRequest.body.form.push({ name: "client_secret", value: clientSecret }); } else { const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); httpRequest.headers.push({ name: "Authorization", value }); } const resp = await ctx.httpRequest.send({ httpRequest }); if (resp.status === 401) { console.log("Unauthorized refresh_token request"); await deleteToken(ctx, contextId); return null; } const body = resp.bodyPath ? (0, import_node_fs2.readFileSync)(resp.bodyPath, "utf8") : ""; if (resp.status < 200 || resp.status >= 300) { throw new Error("Failed to refresh access token with status=" + resp.status + " and body=" + body); } let response; try { response = JSON.parse(body); } catch { response = Object.fromEntries(new URLSearchParams(body)); } if (response.error) { throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`); } const newResponse = { ...response, // Assign a new one or keep the old one, refresh_token: response.refresh_token ?? token.response.refresh_token }; return storeToken(ctx, contextId, newResponse); } // src/grants/authorizationCode.ts var PKCE_SHA256 = "S256"; var PKCE_PLAIN = "plain"; var DEFAULT_PKCE_METHOD = PKCE_SHA256; async function getAuthorizationCode(ctx, contextId, { authorizationUrl: authorizationUrlRaw, accessTokenUrl, clientId, clientSecret, redirectUri, scope, state, audience, credentialsInBody, pkce }) { const token = await getOrRefreshAccessToken(ctx, contextId, { accessTokenUrl, scope, clientId, clientSecret, credentialsInBody }); if (token != null) { return token; } const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`); authorizationUrl.searchParams.set("response_type", "code"); authorizationUrl.searchParams.set("client_id", clientId); if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri); if (scope) authorizationUrl.searchParams.set("scope", scope); if (state) authorizationUrl.searchParams.set("state", state); if (audience) authorizationUrl.searchParams.set("audience", audience); if (pkce) { const verifier = pkce.codeVerifier || createPkceCodeVerifier(); const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD; authorizationUrl.searchParams.set("code_challenge", createPkceCodeChallenge(verifier, challengeMethod)); authorizationUrl.searchParams.set("code_challenge_method", challengeMethod); } return new Promise(async (resolve, reject) => { const authorizationUrlStr = authorizationUrl.toString(); console.log("Authorizing", authorizationUrlStr); let foundCode = false; let { close } = await ctx.window.openUrl({ url: authorizationUrlStr, label: "oauth-authorization-url", dataDirKey: await getDataDirKey(ctx, contextId), async onClose() { if (!foundCode) { reject(new Error("Authorization window closed")); } }, async onNavigate({ url: urlStr }) { const url = new URL(urlStr); if (url.searchParams.has("error")) { return reject(new Error(`Failed to authorize: ${url.searchParams.get("error")}`)); } const code = url.searchParams.get("code"); if (!code) { return; } foundCode = true; close(); const response = await getAccessToken(ctx, { grantType: "authorization_code", accessTokenUrl, clientId, clientSecret, scope, audience, credentialsInBody, params: [ { name: "code", value: code }, ...redirectUri ? [{ name: "redirect_uri", value: redirectUri }] : [] ] }); try { resolve(await storeToken(ctx, contextId, response)); } catch (err) { reject(err); } } }); }); } function createPkceCodeVerifier() { return encodeForPkce((0, import_node_crypto.randomBytes)(32)); } function createPkceCodeChallenge(verifier, method) { if (method === "plain") { return verifier; } const hash = encodeForPkce((0, import_node_crypto.createHash)("sha256").update(verifier).digest()); return hash.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); } function encodeForPkce(bytes) { return bytes.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); } // src/grants/clientCredentials.ts async function getClientCredentials(ctx, contextId, { accessTokenUrl, clientId, clientSecret, scope, audience, credentialsInBody }) { const token = await getToken(ctx, contextId); if (token) { } const response = await getAccessToken(ctx, { grantType: "client_credentials", accessTokenUrl, audience, clientId, clientSecret, scope, credentialsInBody, params: [] }); return storeToken(ctx, contextId, response); } // src/grants/implicit.ts function getImplicit(ctx, contextId, { authorizationUrl: authorizationUrlRaw, responseType, clientId, redirectUri, scope, state, audience }) { return new Promise(async (resolve, reject) => { const token = await getToken(ctx, contextId); if (token) { } const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`); authorizationUrl.searchParams.set("response_type", "token"); authorizationUrl.searchParams.set("client_id", clientId); if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri); if (scope) authorizationUrl.searchParams.set("scope", scope); if (state) authorizationUrl.searchParams.set("state", state); if (audience) authorizationUrl.searchParams.set("audience", audience); if (responseType.includes("id_token")) { authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1)); } const authorizationUrlStr = authorizationUrl.toString(); let foundAccessToken = false; let { close } = await ctx.window.openUrl({ url: authorizationUrlStr, label: "oauth-authorization-url", async onClose() { if (!foundAccessToken) { reject(new Error("Authorization window closed")); } }, async onNavigate({ url: urlStr }) { const url = new URL(urlStr); if (url.searchParams.has("error")) { return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`)); } const hash = url.hash.slice(1); const params = new URLSearchParams(hash); const accessToken = params.get("access_token"); if (!accessToken) { return; } foundAccessToken = true; close(); const response = Object.fromEntries(params); try { resolve(await storeToken(ctx, contextId, response)); } catch (err) { reject(err); } } }); }); } // src/grants/password.ts async function getPassword(ctx, contextId, { accessTokenUrl, clientId, clientSecret, username, password, credentialsInBody, audience, scope }) { const token = await getOrRefreshAccessToken(ctx, contextId, { accessTokenUrl, scope, clientId, clientSecret, credentialsInBody }); if (token != null) { return token; } const response = await getAccessToken(ctx, { accessTokenUrl, clientId, clientSecret, scope, audience, grantType: "password", credentialsInBody, params: [ { name: "username", value: username }, { name: "password", value: password } ] }); return storeToken(ctx, contextId, response); } // src/index.ts var grantTypes = [ { label: "Authorization Code", value: "authorization_code" }, { label: "Implicit", value: "implicit" }, { label: "Resource Owner Password Credential", value: "password" }, { label: "Client Credentials", value: "client_credentials" } ]; var defaultGrantType = grantTypes[0].value; function hiddenIfNot(grantTypes2, ...other) { return (_ctx, { values }) => { const hasGrantType = grantTypes2.find((t) => t === String(values.grantType ?? defaultGrantType)); const hasOtherBools = other.every((t) => t(values)); const show = hasGrantType && hasOtherBools; return { hidden: !show }; }; } var authorizationUrls = [ "https://github.com/login/oauth/authorize", "https://account.box.com/api/oauth2/authorize", "https://accounts.google.com/o/oauth2/v2/auth", "https://api.imgur.com/oauth2/authorize", "https://bitly.com/oauth/authorize", "https://gitlab.example.com/oauth/authorize", "https://medium.com/m/oauth/authorize", "https://public-api.wordpress.com/oauth2/authorize", "https://slack.com/oauth/authorize", "https://todoist.com/oauth/authorize", "https://www.dropbox.com/oauth2/authorize", "https://www.linkedin.com/oauth/v2/authorization", "https://MY_SHOP.myshopify.com/admin/oauth/access_token" ]; var accessTokenUrls = [ "https://github.com/login/oauth/access_token", "https://api-ssl.bitly.com/oauth/access_token", "https://api.box.com/oauth2/token", "https://api.dropboxapi.com/oauth2/token", "https://api.imgur.com/oauth2/token", "https://api.medium.com/v1/tokens", "https://gitlab.example.com/oauth/token", "https://public-api.wordpress.com/oauth2/token", "https://slack.com/api/oauth.access", "https://todoist.com/oauth/access_token", "https://www.googleapis.com/oauth2/v4/token", "https://www.linkedin.com/oauth/v2/accessToken", "https://MY_SHOP.myshopify.com/admin/oauth/authorize" ]; var plugin = { authentication: { name: "oauth2", label: "OAuth 2.0", shortLabel: "OAuth 2", actions: [ { label: "Copy Current Token", async onSelect(ctx, { contextId }) { const token = await getToken(ctx, contextId); if (token == null) { await ctx.toast.show({ message: "No token to copy", color: "warning" }); } else { await ctx.clipboard.copyText(token.response.access_token); await ctx.toast.show({ message: "Token copied to clipboard", icon: "copy", color: "success" }); } } }, { label: "Delete Token", async onSelect(ctx, { contextId }) { if (await deleteToken(ctx, contextId)) { await ctx.toast.show({ message: "Token deleted", color: "success" }); } else { await ctx.toast.show({ message: "No token to delete", color: "warning" }); } } }, { label: "Clear Window Session", async onSelect(ctx, { contextId }) { await resetDataDirKey(ctx, contextId); } } ], args: [ { type: "select", name: "grantType", label: "Grant Type", hideLabel: true, defaultValue: defaultGrantType, options: grantTypes }, // Always-present fields { type: "text", name: "clientId", label: "Client ID", optional: true }, { type: "text", name: "clientSecret", label: "Client Secret", optional: true, password: true, dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]) }, { type: "text", name: "authorizationUrl", optional: true, label: "Authorization URL", dynamic: hiddenIfNot(["authorization_code", "implicit"]), placeholder: authorizationUrls[0], completionOptions: authorizationUrls.map((url) => ({ label: url, value: url })) }, { type: "text", name: "accessTokenUrl", optional: true, label: "Access Token URL", placeholder: accessTokenUrls[0], dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]), completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url })) }, { type: "text", name: "redirectUri", label: "Redirect URI", optional: true, dynamic: hiddenIfNot(["authorization_code", "implicit"]) }, { type: "text", name: "state", label: "State", optional: true, dynamic: hiddenIfNot(["authorization_code", "implicit"]) }, { type: "text", name: "audience", label: "Audience", optional: true }, { type: "checkbox", name: "usePkce", label: "Use PKCE", dynamic: hiddenIfNot(["authorization_code"]) }, { type: "select", name: "pkceChallengeMethod", label: "Code Challenge Method", options: [{ label: "SHA-256", value: PKCE_SHA256 }, { label: "Plain", value: PKCE_PLAIN }], defaultValue: DEFAULT_PKCE_METHOD, dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce) }, { type: "text", name: "pkceCodeVerifier", label: "Code Verifier", placeholder: "Automatically generated if not provided", optional: true, dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce) }, { type: "text", name: "username", label: "Username", optional: true, dynamic: hiddenIfNot(["password"]) }, { type: "text", name: "password", label: "Password", password: true, optional: true, dynamic: hiddenIfNot(["password"]) }, { type: "select", name: "responseType", label: "Response Type", defaultValue: "token", options: [ { label: "Access Token", value: "token" }, { label: "ID Token", value: "id_token" }, { label: "ID and Access Token", value: "id_token token" } ], dynamic: hiddenIfNot(["implicit"]) }, { type: "accordion", label: "Advanced", inputs: [ { type: "text", name: "scope", label: "Scope", optional: true }, { type: "text", name: "headerPrefix", label: "Header Prefix", optional: true, defaultValue: "Bearer" }, { type: "select", name: "credentials", label: "Send Credentials", defaultValue: "body", options: [ { label: "In Request Body", value: "body" }, { label: "As Basic Authentication", value: "basic" } ] } ] }, { type: "accordion", label: "Access Token Response", async dynamic(ctx, { contextId }) { const token = await getToken(ctx, contextId); if (token == null) { return { hidden: true }; } return { label: "Access Token Response", inputs: [ { type: "editor", defaultValue: JSON.stringify(token.response, null, 2), hideLabel: true, readOnly: true, language: "json" } ] }; } } ], async onApply(ctx, { values, contextId }) { const headerPrefix = stringArg(values, "headerPrefix"); const grantType = stringArg(values, "grantType"); const credentialsInBody = values.credentials === "body"; let token; if (grantType === "authorization_code") { const authorizationUrl = stringArg(values, "authorizationUrl"); const accessTokenUrl = stringArg(values, "accessTokenUrl"); token = await getAuthorizationCode(ctx, contextId, { accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`, clientId: stringArg(values, "clientId"), clientSecret: stringArg(values, "clientSecret"), redirectUri: stringArgOrNull(values, "redirectUri"), scope: stringArgOrNull(values, "scope"), audience: stringArgOrNull(values, "audience"), state: stringArgOrNull(values, "state"), credentialsInBody, pkce: values.usePkce ? { challengeMethod: stringArg(values, "pkceChallengeMethod"), codeVerifier: stringArgOrNull(values, "pkceCodeVerifier") } : null }); } else if (grantType === "implicit") { const authorizationUrl = stringArg(values, "authorizationUrl"); token = await getImplicit(ctx, contextId, { authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`, clientId: stringArg(values, "clientId"), redirectUri: stringArgOrNull(values, "redirectUri"), responseType: stringArg(values, "responseType"), scope: stringArgOrNull(values, "scope"), audience: stringArgOrNull(values, "audience"), state: stringArgOrNull(values, "state") }); } else if (grantType === "client_credentials") { const accessTokenUrl = stringArg(values, "accessTokenUrl"); token = await getClientCredentials(ctx, contextId, { accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, clientId: stringArg(values, "clientId"), clientSecret: stringArg(values, "clientSecret"), scope: stringArgOrNull(values, "scope"), audience: stringArgOrNull(values, "audience"), credentialsInBody }); } else if (grantType === "password") { const accessTokenUrl = stringArg(values, "accessTokenUrl"); token = await getPassword(ctx, contextId, { accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, clientId: stringArg(values, "clientId"), clientSecret: stringArg(values, "clientSecret"), username: stringArg(values, "username"), password: stringArg(values, "password"), scope: stringArgOrNull(values, "scope"), audience: stringArgOrNull(values, "audience"), credentialsInBody }); } else { throw new Error("Invalid grant type " + grantType); } const headerValue = `${headerPrefix} ${token.response.access_token}`.trim(); return { setHeaders: [{ name: "Authorization", value: headerValue }] }; } } }; function stringArgOrNull(values, name) { const arg = values[name]; if (arg == null || arg == "") return null; return `${arg}`; } function stringArg(values, name) { const arg = stringArgOrNull(values, name); if (!arg) return ""; return arg; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { plugin });