defmodule Mix.Tasks.Npm.Token do
@shortdoc "Manage npm auth tokens"
@moduledoc """
Display or verify npm authentication token status.
mix npm.token # Show token status
mix npm.token --verify # Verify token with registry
The token is read from the `NPM_TOKEN` environment variable
or from `.npmrc` auth configuration.
"""
use Mix.Task
@impl true
def run(args) do
Application.ensure_all_started(:req)
{opts, _, _} = OptionParser.parse(args, strict: [verify: :boolean])
token = resolve_token()
case token do
nil ->
Mix.shell().info("No npm token configured.")
Mix.shell().info("Set NPM_TOKEN or add authToken to .npmrc")
token ->
masked = mask_token(token)
Mix.shell().info("Token: #{masked}")
Mix.shell().info("Source: #{token_source()}")
if opts[:verify], do: verify_token(token)
end
end
defp resolve_token do
NPM.Config.auth_token() || read_npmrc_token()
end
defp read_npmrc_token do
case File.read(".npmrc") do
{:ok, content} ->
config = NPM.Config.parse_npmrc(content)
config["//registry.npmjs.org/:_authToken"]
{:error, _} ->
nil
end
end
defp token_source do
cond do
System.get_env("NPM_TOKEN") -> "NPM_TOKEN environment variable"
Application.get_env(:duskmoon_npm, :token) -> "application config"
true -> ".npmrc file"
end
end
defp mask_token(token) when byte_size(token) <= 8, do: "****"
defp mask_token(token) do
String.slice(token, 0, 4) <> "****" <> String.slice(token, -4, 4)
end
defp verify_token(token) do
url = "#{NPM.Registry.registry_url()}/-/whoami"
case Req.get(url, headers: [authorization: "Bearer #{token}"]) do
{:ok, %{status: 200, body: %{"username" => user}}} ->
Mix.shell().info("Authenticated as: #{user}")
{:ok, %{status: 401}} ->
Mix.shell().error("Token is invalid or expired.")
{:ok, %{status: status}} ->
Mix.shell().error("Unexpected response: #{status}")
{:error, reason} ->
Mix.shell().error("Verification failed: #{inspect(reason)}")
end
end
end