---
-- Vault module
--
-- This module can be used to resolve, parse and verify vault references.
--
-- @module kong.vault


local require = require


local concurrency = require "kong.concurrency"
local constants = require "kong.constants"
local arguments = require "kong.api.arguments"
local lrucache = require "resty.lrucache"
local isempty = require "table.isempty"
local buffer = require "string.buffer"
local clone = require "table.clone"
local cjson = require("cjson.safe").new()


local yield = require("kong.tools.yield").yield
local get_updated_now_ms = require("kong.tools.time").get_updated_now_ms
local replace_dashes = require("kong.tools.string").replace_dashes


local ngx = ngx
local get_phase = ngx.get_phase
local max = math.max
local min = math.min
local fmt = string.format
local sub = string.sub
local byte = string.byte
local type = type
local sort = table.sort
local pcall = pcall
local lower = string.lower
local pairs = pairs
local ipairs = ipairs
local concat = table.concat
local md5_bin = ngx.md5_bin
local tostring = tostring
local tonumber = tonumber
local decode_args = ngx.decode_args
local unescape_uri = ngx.unescape_uri
local parse_url = require("socket.url").parse
local parse_path = require("socket.url").parse_path
local encode_base64url = require("ngx.base64").encode_base64url
local decode_json = cjson.decode


local NEGATIVELY_CACHED_VALUE = "\0"
local ROTATION_INTERVAL = tonumber(os.getenv("KONG_VAULT_ROTATION_INTERVAL"), 10) or 60
local DAO_MAX_TTL = constants.DATABASE.DAO_MAX_TTL


local BRACE_START = byte("{")
local BRACE_END = byte("}")
local COLON = byte(":")
local SLASH = byte("/")
local BYT_V = byte("v")
local BYT_A = byte("a")
local BYT_U = byte("u")
local BYT_L = byte("l")
local BYT_T = byte("t")


local VAULT_QUERY_OPTS = { workspace = ngx.null }


---
-- Checks if the passed in reference looks like a reference.
-- Valid references start with '{vault://' and end with '}'.
--
-- @local
-- @function is_reference
-- @tparam string reference reference to check
-- @treturn boolean `true` is the passed in reference looks like a reference, otherwise `false`
local function is_reference(reference)
  return type(reference)     == "string"
     and byte(reference, 1)  == BRACE_START
     and byte(reference, 2)  == BYT_V
     and byte(reference, 3)  == BYT_A
     and byte(reference, 4)  == BYT_U
     and byte(reference, 5)  == BYT_L
     and byte(reference, 6)  == BYT_T
     and byte(reference, 7)  == COLON
     and byte(reference, 8)  == SLASH
     and byte(reference, 9)  == SLASH
     and byte(reference, -1) == BRACE_END
end


---
-- Parses and decodes the passed in reference and returns a table
-- containing its components.
--
-- Given a following resource:
-- ```lua
-- "{vault://env/cert/key?prefix=SSL_#1}"
-- ```
--
-- This function will return following table:
--
-- ```lua
-- {
--   name     = "env",  -- name of the Vault entity or Vault strategy
--   resource = "cert", -- resource where secret is stored
--   key      = "key",  -- key to lookup if the resource is secret object
--   config   = {       -- if there are any config options specified
--     prefix = "SSL_"
--   },
--   version  = 1       -- if the version is specified
-- }
-- ```
--
-- @local
-- @function parse_reference
-- @tparam string reference reference to parse
-- @treturn table|nil a table containing each component of the reference, or `nil` on error
-- @treturn string|nil error message on failure, otherwise `nil`
local function parse_reference(reference)
  if not is_reference(reference) then
    return nil, fmt("not a reference [%s]", tostring(reference))
  end

  local url, err = parse_url(sub(reference, 2, -2))
  if not url then
    return nil, fmt("reference is not url (%s) [%s]", err, reference)
  end

  local name = url.host
  if not name then
    return nil, fmt("reference url is missing host [%s]", reference)
  end

  local path = url.path
  if not path then
    return nil, fmt("reference url is missing path [%s]", reference)
  end

  local resource = sub(path, 2)
  if resource == "" then
    return nil, fmt("reference url has empty path [%s]", reference)
  end

  local version = url.fragment
  if version then
    version = tonumber(version, 10)
    if not version then
      return nil, fmt("reference url has invalid version [%s]", reference)
    end
  end

  local key
  local parts = parse_path(resource)
  local count = #parts
  if count == 0 then
    return nil, fmt("reference url has invalid path [%s]", reference)
  elseif count == 1 then
    resource = unescape_uri(parts[1])
  elseif parts.is_directory then
    resource = unescape_uri(concat(parts, "/", 1, count))
  else
    resource = unescape_uri(concat(parts, "/", 1, count - 1))
    key = unescape_uri(parts[count])
  end

  local config
  local query = url.query
  if query and query ~= "" then
    config = decode_args(query)
  end

  return {
    name = url.host,
    resource = resource,
    key = key,
    config = config,
    version = version,
  }
end


---
-- Create a instance of PDK Vault module
--
-- @local
-- @function new
-- @tparam table self a PDK instance
-- @treturn table a new instance of Vault
local function new(self)
  -- Don't put this onto the top level of the file unless you're prepared for a surprise
  local Schema = require "kong.db.schema"

  local ROTATION_MUTEX_OPTS = {
    name = "vault-rotation",
    exptime = ROTATION_INTERVAL * 1.5, -- just in case the lock is not properly released
    timeout = 0, -- we don't want to wait for release as we run a recurring timer
  }

  local LRU = lrucache.new(1000)
  local RETRY_LRU = lrucache.new(1000)

  local SECRETS_CACHE = ngx.shared.kong_secrets
  local SECRETS_CACHE_MIN_TTL = ROTATION_INTERVAL * 2

  local INIT_SECRETS = {}
  local INIT_WORKER_SECRETS = {}
  local STRATEGIES = {}
  local SCHEMAS = {}
  local CONFIGS = {}

  local BUNDLED_VAULTS = constants.BUNDLED_VAULTS
  local VAULT_NAMES
  do
    local vaults = self and self.configuration and self.configuration.loaded_vaults
    if vaults then
      VAULT_NAMES = {}

      for name in pairs(vaults) do
        VAULT_NAMES[name] = true
      end

    else
      VAULT_NAMES = BUNDLED_VAULTS and clone(BUNDLED_VAULTS) or {}
    end
  end


  ---
  -- Calculates hash for a string.
  --
  -- @local
  -- @function calculate_hash
  -- @tparam string str a string to hash
  -- @treturn string md5 hash as base64url encoded string
  local function calculate_hash(str)
    return encode_base64url(md5_bin(str))
  end


  ---
  -- Builds cache key from reference and configuration hash.
  --
  -- @local
  -- @function build_cache_key
  -- @tparam string reference the vault reference string
  -- @tparam string config_hash the configuration hash
  -- @treturn string the cache key for shared dictionary cache
  local function build_cache_key(reference, config_hash)
    return config_hash .. "." .. reference
  end


  ---
  -- Parses cache key back to a reference and a configuration hash.
  --
  -- @local
  -- @function parse_cache_key
  -- @tparam string cache_key the cache key used for shared dictionary cache
  -- @treturn string|nil the vault reference string
  -- @treturn string|nil a string describing an error if there was one
  -- @treturn string the configuration hash
  local function parse_cache_key(cache_key)
    local buf = buffer.new():set(cache_key)
    local config_hash = buf:get(22)
    local divider = buf:get(1)
    local reference = buf:get()
    if divider ~= "." or not is_reference(reference) then
      return nil, "invalid cache key (" .. cache_key .. ")"
    end
    return reference, nil, config_hash
  end


  ---
  -- This function extracts a key and returns its value from a JSON object.
  --
  -- It first decodes the JSON string into a Lua table, then checks for the presence and type of a specific key.
  --
  -- @local
  -- @function extract_key_from_json_string
  -- @tparam string json_string the JSON string to be parsed and decoded
  -- @tparam string key the specific subfield to be searched for within the JSON object
  -- @treturn string|nil the value associated with the specified key in the JSON object
  -- @treturn string|nil a string describing an error if there was one
  local function extract_key_from_json_string(json_string, key)
    -- Note that this function will only find keys in flat maps.
    -- Deeper nested structures are not supported.
    local json, err = decode_json(json_string)
    if type(json) ~= "table" then
      return nil, fmt("unable to json decode value (%s): %s", json, err)
    end

    json_string = json[key]
    if json_string == nil then
      return nil, fmt("subfield %s not found in JSON secret", key)
    elseif type(json_string) ~= "string" then
      return nil, fmt("unexpected %s value in JSON secret for subfield %s", type(json_string), key)
    end

    return json_string
  end


  ---
  -- This function adjusts the 'time-to-live' (TTL) according to the configuration provided in 'vault_config'.
  --
  -- If the TTL is not a number or if it falls outside of the configured minimum or maximum TTL,
  -- it will be adjusted accordingly. The adjustment happens on Vault strategy returned TTL values only.
  --
  -- @local
  -- @function adjust_ttl
  -- @tparam number|nil ttl The time-to-live value to be adjusted.
  -- @tparam table|nil config the configuration table for the vault,
  -- which may contain 'ttl', 'min_ttl', and 'max_ttl' fields.
  -- @treturn number returns the adjusted TTL:
  -- * if the initial TTL is not a number, it returns the 'ttl' field from the 'vault_config' table or 0 if it doesn't exist.
  -- * if the initial TTL is greater than 'max_ttl' from 'vault_config', it returns 'max_ttl'.
  -- * if the initial TTL is less than 'min_ttl' from 'vault_config', it returns 'min_ttl'.
  -- * otherwise, it returns the given TTL.
  local function adjust_ttl(ttl, config)
    if type(ttl) ~= "number" then
      return config and config.ttl or DAO_MAX_TTL
    end

    if ttl <= 0 then
      -- for simplicity, we don't support never expiring keys
      return DAO_MAX_TTL
    end

    local max_ttl = config and config.max_ttl
    if max_ttl and max_ttl > 0 and ttl > max_ttl then
      return max_ttl
    end

    local min_ttl = config and config.min_ttl
    if min_ttl and ttl < min_ttl then
      return min_ttl
    end

    return ttl
  end


  ---
  -- Decorates normal strategy with a caching strategy when rotating secrets.
  --
  -- With vault strategies we support JSON string responses, that means that
  -- the vault can return n-number of related secrets, for example Postgres
  -- username and password. The references could look like:
  --
  -- - {vault://my-vault/postgres/username}
  -- - {vault://my-vault/postgres/password}
  --
  -- For LRU cache we use ´{vault://my-vault/postgres/username}` as a cache
  -- key and for SHM we use `<config-hash>.{vault://my-vault/postgres/username}`
  -- as a cache key. What we send to vault are:
  --
  -- 1. the config table
  -- 2. the resource to lookup
  -- 3. the version of secret
  --
  -- In the above references in both cases the `resource` is `postgres` and we
  -- never send `/username` or `/password` to vault strategy. Thus the proper
  -- cache key for vault strategy is: `<config-hash>.<resource>.<version>`.
  -- This means that we can call the vault strategy just once, and not twice
  -- to resolve both references. This also makes sure we get both secrets in
  -- atomic way.
  --
  -- The caching strategy wraps the strategy so that call to it can be cached
  -- when e.g. looping through secrets on rotation. Again that ensures atomicity,
  -- and reduces calls to actual vault.
  --
  -- @local
  -- @function get_caching_strategy
  -- @treturn function returns a function that takes `strategy` and `config_hash`
  -- as an argument, that returns a decorated strategy.
  --
  -- @usage
  -- local caching_strategy = get_caching_strategy()
  -- for _, reference in ipairs({ "{vault://my-vault/postgres/username}",
  --                              "{vault://my-vault/postgres/username}", })
  -- do
  --   local strategy, err, config, _, parsed_reference, config_hash = get_strategy(reference)
  --   strategy = caching_strategy(strategy, config_hash)
  --   local value, err, ttl = strategy.get(config, parsed_reference.resource, parsed_reference.version)
  -- end
  local function get_caching_strategy()
    local cache = {}
    return function(strategy, config_hash)
      return {
        get = function(config, resource, version)
          local cache_key = fmt("%s.%s.%s", config_hash, resource or "", version or "")
          local data = cache[cache_key]
          if data then
            return data[1], data[2], data[3]
          end

          local value, err, ttl = strategy.get(config, resource, version)

          cache[cache_key] = {
            value,
            err,
            ttl,
          }

          return value, err, ttl
        end
      }
    end
  end


  ---
  -- Build schema aware configuration out of base configuration and the configuration overrides
  -- (e.g. configuration parameters stored in a vault reference).
  --
  -- It infers and validates configuration fields, and only returns validated fields
  -- in the returned config. It also calculates a deterministic configuration hash
  -- that will can used to build  shared dictionary's cache key.
  --
  -- @local
  -- @function get_vault_config_and_hash
  -- @tparam string name the name of vault strategy
  -- @tparam table schema the scheme of vault strategy
  -- @tparam table base_config the base configuration
  -- @tparam table|nil config_overrides the configuration overrides
  -- @treturn table validated and merged configuration from base configuration and config overrides
  -- @treturn string calculated hash of the configuration
  --
  -- @usage
  -- local config, hash = get_vault_config_and_hash("env", schema, { prefix = "DEFAULT_" },
  --                                                               { prefix = "MY_PREFIX_" })
  local get_vault_config_and_hash do
    local CONFIG_HASH_BUFFER = buffer.new(100)
    get_vault_config_and_hash = function(name, schema, base_config, config_overrides)
      CONFIG_HASH_BUFFER:reset():putf("%s;", name)
      local config = {}
      config_overrides = config_overrides or config
      for k, f in schema:each_field() do
        local v = config_overrides[k] or base_config[k]
        v = arguments.infer_value(v, f)
        if v ~= nil and schema:validate_field(f, v) then
          config[k] = v
          CONFIG_HASH_BUFFER:putf("%s=%s;", k, v)
        end
      end
      return config, calculate_hash(CONFIG_HASH_BUFFER:get())
    end
  end


  ---
  -- Fetches the strategy and schema for a given vault.
  --
  -- This function fetches the associated strategy and schema from the `STRATEGIES` and `SCHEMAS` tables,
  -- respectively. If the strategy or schema isn't found in the tables, it attempts to initialize them
  -- from the Lua modules.
  --
  -- @local
  -- @function get_vault_strategy_and_schema
  -- @tparam string name the name of the vault to fetch the strategy and schema for
  -- @treturn table|nil the fetched or required strategy for the given vault
  -- @treturn string|nil an error message, if an error occurred while fetching or requiring the strategy or schema
  -- @treturn table|nil the vault strategy's configuration schema.
  local function get_vault_strategy_and_schema(name)
    local strategy = STRATEGIES[name]
    local schema = SCHEMAS[name]

    if strategy then
      return strategy, nil, schema
    end

    local vaults = self and (self.db and self.db.vaults)
    if vaults and vaults.strategies then
      strategy = vaults.strategies[name]
      if not strategy then
        return nil, fmt("could not find vault (%s)", name)
      end

      schema = vaults.schema.subschemas[name]
      if not schema then
        return nil, fmt("could not find vault schema (%s): %s", name, strategy)
      end

      schema = Schema.new(schema.fields.config)

    else
      local ok
      ok, strategy = pcall(require, fmt("kong.vaults.%s", name))
      if not ok then
        return nil, fmt("could not find vault (%s): %s", name, strategy)
      end

      local def
      ok, def = pcall(require, fmt("kong.vaults.%s.schema", name))
      if not ok then
        return nil, fmt("could not find vault schema (%s): %s", name, def)
      end

      schema = Schema.new(require("kong.db.schema.entities.vaults"))

      local err
      ok, err = schema:new_subschema(name, def)
      if not ok then
        return nil, fmt("could not load vault sub-schema (%s): %s", name, err)
      end

      schema = schema.subschemas[name]
      if not schema then
        return nil, fmt("could not find vault sub-schema (%s)", name)
      end

      if type(strategy.init) == "function" then
        strategy.init()
      end

      schema = Schema.new(schema.fields.config)
    end

    STRATEGIES[name] = strategy
    SCHEMAS[name] = schema

    return strategy, nil, schema
  end


  ---
  -- This function retrieves the base configuration for the default vault
  -- using the vault strategy name.
  --
  -- The vault configuration is stored in Kong configuration from which this
  -- function derives the default base configuration for the vault strategy.
  --
  -- @local
  -- @function get_vault_name_and_config_by_name
  -- @tparam string name The unique name of the vault strategy
  -- @treturn string name of the vault strategy (same as the input string)
  -- @treturn nil this never fails so it always returns `nil`
  -- @treturn table|nil the vault strategy's base config derived from Kong configuration
  --
  -- @usage
  -- local name, err, base_config = get_vault_name_and_config_by_name("env")
  local function get_vault_name_and_config_by_name(name)
    -- base config stays the same so we can cache it
    local base_config = CONFIGS[name]
    if not base_config then
      base_config = {}
      if self and self.configuration then
        local configuration = self.configuration
        local env_name = replace_dashes(name)
        local _, err, schema = get_vault_strategy_and_schema(name)
        if not schema then
          return nil, err
        end
        for k, f in schema:each_field() do
          -- n is the entry in the kong.configuration table, for example
          -- KONG_VAULT_ENV_PREFIX will be found in kong.configuration
          -- with a key "vault_env_prefix". Environment variables are
          -- thus turned to lowercase and we just treat any "-" in them
          -- as "_". For example if your custom vault was called "my-vault"
          -- then you would configure it with KONG_VAULT_MY_VAULT_<setting>
          -- or in kong.conf, where it would be called
          -- "vault_my_vault_<setting>".
          local n = lower(fmt("vault_%s_%s", env_name, replace_dashes(k)))
          local v = configuration[n]
          v = arguments.infer_value(v, f)
          -- TODO: should we be more visible with validation errors?
          -- In general it would be better to check the references
          -- and not just a format when they are stored with admin
          -- API, or in case of process secrets, when the kong is
          -- started. So this is a note to remind future us.
          -- Because current validations are less strict, it is fine
          -- to ignore it here.
          if v ~= nil and schema:validate_field(f, v) then
            base_config[k] = v
          elseif f.required and f.default ~= nil then
            base_config[k] = f.default
          end
        end
        CONFIGS[name] = base_config
      end
    end

    return name, nil, base_config
  end


  ---
  -- This function retrieves a vault entity by its prefix from configuration
  -- database, and returns the strategy name and the base configuration.
  --
  -- It either fetches the vault from a cache or directly from a configuration
  -- database. The vault entity is expected to be found in a database (db) or
  -- cache. If not found, an error message is returned.
  --
  -- @local
  -- @function get_vault_name_and_config_by_prefix
  -- @tparam string prefix the unique identifier of the vault entity to be retrieved
  -- @treturn string|nil name of the vault strategy
  -- @treturn string|nil a string describing an error if there was one
  -- @treturn table|nil the vault entity config
  --
  -- @usage
  -- local name, err, base_config = get_vault_name_and_config_by_prefix("my-vault")
  local function get_vault_name_and_config_by_prefix(prefix)
    if not (self and self.db) then
      return nil, "unable to retrieve config from db"
    end

    -- find a vault - it can be either a named vault that needs to be loaded from the cache, or the
    -- vault type accessed by name
    local cache = self.core_cache
    local vaults = self.db.vaults
    local vault, err

    if cache then
      local vault_cache_key = vaults:cache_key(prefix)
      vault, err = cache:get(vault_cache_key, nil, vaults.select_by_prefix, vaults, prefix, VAULT_QUERY_OPTS)

    else
      vault, err = vaults:select_by_prefix(prefix, VAULT_QUERY_OPTS)
    end

    if not vault then
      if err then
        return nil, fmt("could not find vault (%s): %s", prefix, err)
      end

      return nil, fmt("could not find vault (%s)", prefix)
    end

    return vault.name, nil, vault.config
  end


  ---
  -- Function `get_vault_name_and_base_config` retrieves name of the strategy
  -- and its base configuration using name (for default vaults) or prefix for
  -- database stored vault entities.
  --
  -- @local
  -- @function get_vault_name_and_base_config
  -- @tparam string name_or_prefix name of the vault strategy or prefix of the vault entity
  -- @treturn string|nil name of the vault strategy
  -- @treturn string|nil a string describing an error if there was one
  -- @treturn table|nil the base configuration
  --
  -- @usage
  -- local name, err, base_config = get_vault_name_and_base_config("env")
  local function get_vault_name_and_base_config(name_or_prefix)
    if VAULT_NAMES[name_or_prefix] then
      return get_vault_name_and_config_by_name(name_or_prefix)
    end

    return get_vault_name_and_config_by_prefix(name_or_prefix)
  end


  ---
  -- Function `get_strategy` processes a reference to retrieve a strategy and configuration settings.
  --
  -- The function first parses the reference. Then, it gets the strategy, the schema, and the base configuration
  -- settings for the vault based on the parsed reference. It checks the license type if required by the strategy.
  -- Finally, it gets the configuration and the cache key of the reference.
  --
  -- @local
  -- @function get_strategy
  -- @tparam string reference the reference to be used to load strategy and its settings.
  -- @tparam table|nil strategy the strategy used to fetch the secret
  -- @treturn string|nil a string describing an error if there was one
  -- @treturn table|nil the vault configuration for the reference
  -- @treturn string|nil the cache key for shared dictionary for the reference
  -- @treturn table|nil the parsed reference
  --
  -- @usage
  -- local strategy, err, config, cache_key, parsed_reference = get_strategy(reference)
  local function get_strategy(reference)
    local parsed_reference, err = parse_reference(reference)
    if not parsed_reference then
      return nil, err
    end

    local name, err, base_config = get_vault_name_and_base_config(parsed_reference.name)
    if not name then
      return nil, err
    end

    local strategy, err, schema = get_vault_strategy_and_schema(name)
    if not strategy then
      return nil, err
    end

    if strategy.license_required and self.licensing and self.licensing:license_type() == "free" then
      return nil, "vault " .. name .. " requires a license to be used"
    end

    local config, config_hash = get_vault_config_and_hash(name, schema, base_config, parsed_reference.config)
    local cache_key = build_cache_key(reference, config_hash)

    return strategy, nil, config, cache_key, parsed_reference, config_hash
  end


  ---
  -- Invokes a provided strategy to fetch a secret.
  --
  -- This function invokes a strategy provided to it to retrieve a secret from a vault.
  -- The secret returned by the strategy must be a string containing a string value,
  -- or JSON string containing the required key with a string value.
  --
  -- @local
  -- @function invoke_strategy
  -- @tparam table strategy the strategy used to fetch the secret
  -- @tparam config the configuration required by the strategy
  -- @tparam parsed_reference a table containing the resource name, the version of the secret
  -- to be fetched, and optionally a key to search on returned JSON string
  -- @treturn string|nil the value of the secret, or `nil`
  -- @treturn string|nil a string describing an error if there was one
  -- @treturn number|nil a ttl (time to live) of the fetched secret if there was one
  --
  -- @usage
  -- local value, err, ttl = invoke_strategy(strategy, config, parsed_reference)
  local function invoke_strategy(strategy, config, parsed_reference)
    local value, err, ttl = strategy.get(config, parsed_reference.resource, parsed_reference.version)
    if value == nil then
      if err then
        return nil, fmt("no value found (%s)", err)
      end

      return nil, "no value found"

    elseif type(value) ~= "string" then
      return nil, fmt("value returned from vault has invalid type (%s), string expected", type(value))
    end

    -- in vault reference, the secret can have multiple values, each stored under a key.
    -- The vault returns a JSON string that contains an object which can be indexed by the key.
    local key = parsed_reference.key
    if key then
      value, err = extract_key_from_json_string(value, key)
      if not value then
        return nil, fmt("could not get subfield value: %s", err)
      end
    end

    return value, nil, ttl
  end

  ---
  -- Function `get_cache_value_and_ttl` returns a value for caching and its ttl
  --
  -- @local
  -- @function get_cache_value_and_ttl
  -- @tparam string value the vault returned value for a reference
  -- @tparam table config the configuration settings to be used
  -- @tparam[opt] number ttl the possible vault returned ttl
  -- @treturn string value to be stored in shared dictionary
  -- @treturn number shared dictionary ttl
  -- @treturn number lru ttl
  -- @usage local cache_value, shdict_ttl, lru_ttl = get_cache_value_and_ttl(value, config, ttl)
  local function get_cache_value_and_ttl(value, config, ttl)
    local cache_value, shdict_ttl, lru_ttl
    if value then
      cache_value = value

      -- adjust ttl to the minimum and maximum values configured
      ttl = adjust_ttl(ttl, config)

      if config.resurrect_ttl then
        lru_ttl = min(ttl + config.resurrect_ttl, DAO_MAX_TTL)
        shdict_ttl = max(lru_ttl, SECRETS_CACHE_MIN_TTL)

      else
        lru_ttl = ttl
        -- shared dict ttl controls when the secret
        -- value will be refreshed by `rotate_secrets`
        -- timer. If a secret whose remaining time is less
        -- than `config.resurrect_ttl`(or DAO_MAX_TTL
        -- if not configured), it could possibly
        -- be updated in every cycle of `rotate_secrets`.
        --
        -- The shdict_ttl should be
        -- `config.ttl` + `config.resurrect_ttl`
        -- to make sure the secret value persists for
        -- at least `config.ttl` seconds.
        -- When `config.resurrect_ttl` is not set and
        -- `config.ttl` is not set, shdict_ttl will be
        -- DAO_MAX_TTL * 2; when `config.resurrect_ttl`
        -- is not set but `config.ttl` is set, shdict_ttl
        -- will be ttl + DAO_MAX_TTL
        shdict_ttl = ttl + DAO_MAX_TTL
      end

    else
      cache_value = NEGATIVELY_CACHED_VALUE

      -- negatively cached values will be rotated on each rotation interval
      shdict_ttl = max(config.neg_ttl or 0, SECRETS_CACHE_MIN_TTL)
    end

    return cache_value, shdict_ttl, lru_ttl
  end


  ---
  -- Function `get_from_vault` retrieves a value from the vault using the provided strategy.
  --
  -- The function first retrieves a value from the vault and its optionally returned ttl.
  -- It then adjusts the ttl within configured bounds, stores the value in the SHDICT cache
  -- with a ttl that includes a resurrection time, and stores the value in the LRU cache with
  -- the adjusted ttl.
  --
  -- @local
  -- @function get_from_vault
  -- @tparam string reference the vault reference string
  -- @tparam table strategy the strategy to be used to retrieve the value from the vault
  -- @tparam table config the configuration settings to be used
  -- @tparam string cache_key the cache key used for shared dictionary cache
  -- @tparam table parsed_reference the parsed reference
  -- @treturn string|nil the retrieved value from the vault, of `nil`
  -- @treturn string|nil a string describing an error if there was one
  -- @treturn boolean|nil whether to resurrect value in case vault errors or doesn't return value
  -- @usage local value, err = get_from_vault(reference, strategy, config, cache_key, parsed_reference)
  local function get_from_vault(reference, strategy, config, cache_key, parsed_reference, resurrect)
    local value, err, ttl = invoke_strategy(strategy, config, parsed_reference)
    if resurrect and value == nil then
      local resurrected_value = SECRETS_CACHE:get(cache_key)
      if resurrected_value then
        return resurrected_value

      else
        return nil, fmt("could not get value from external vault (%s)", err)
      end
    end

    local cache_value, shdict_ttl, lru_ttl = get_cache_value_and_ttl(value, config, ttl)
    local ok, cache_err = SECRETS_CACHE:safe_set(cache_key, cache_value, shdict_ttl)
    if not ok then
      return nil, cache_err
    end

    if cache_value == NEGATIVELY_CACHED_VALUE then
      return nil, fmt("could not get value from external vault (%s)", err)
    end

    LRU:set(reference, cache_value, lru_ttl)

    return cache_value
  end


  ---
  -- Function `get` retrieves a value from local (LRU), shared dictionary (SHDICT) cache.
  --
  -- If the value is not found in these caches and `cache_only` is not `truthy`,
  -- it attempts to retrieve the value from a vault.
  --
  -- On init worker phase the resolving of the secrets is postponed to a timer,
  -- and in this case the function returns `""` when it fails to find a value
  -- in a cache. This is because of current limitations in platform that disallows
  -- using cosockets/coroutines in that phase.
  --
  -- @local
  -- @function get
  -- @tparam string reference the reference key to lookup
  -- @tparam[opt] boolean cache_only optional boolean flag (if set to `true`,
  -- the function will not attempt to retrieve the value from the vault)
  -- @treturn string the retrieved value corresponding to the provided reference,
  -- or `nil` (when found negatively cached, or in case of an error)
  -- @treturn string a string describing an error if there was one
  --
  -- @usage
  -- local value, err = get(reference, cache_only)
  local function get(reference, cache_only)
    -- the LRU stale value is ignored
    local value = LRU:get(reference)
    if value then
      return value
    end

    local strategy, err, config, cache_key, parsed_reference = get_strategy(reference)
    if not strategy then
      -- this can fail on init as the lmdb cannot be accessed and secondly,
      -- because the data is not yet inserted into LMDB when using KONG_DECLARATIVE_CONFIG.
      if get_phase() == "init" then
        if not INIT_SECRETS[cache_key] then
          INIT_SECRETS[reference] = true
          INIT_SECRETS[#INIT_SECRETS + 1] = reference
        end

        return ""
      end

      return nil, err
    end

    value = SECRETS_CACHE:get(cache_key)
    if value == NEGATIVELY_CACHED_VALUE then
      return nil
    end

    if not value then
      if cache_only then
        return nil, "could not find cached value"
      end

      -- this can fail on init worker as there is no cosockets / coroutines available
      if  get_phase() == "init_worker" then
        if not INIT_WORKER_SECRETS[cache_key] then
          INIT_WORKER_SECRETS[cache_key] = true
          INIT_WORKER_SECRETS[#INIT_WORKER_SECRETS + 1] = cache_key
        end

        return ""
      end

      return get_from_vault(reference, strategy, config, cache_key, parsed_reference)
    end

    -- if we have something in the node-level cache, but not in the worker-level
    -- cache, we should update the worker-level cache. Use the remaining TTL from the SHDICT
    local lru_ttl = (SECRETS_CACHE:ttl(cache_key) or 0) - (config.resurrect_ttl or DAO_MAX_TTL)
    -- only do that when the TTL is greater than 0.
    if lru_ttl > 0 then
      LRU:set(reference, value, lru_ttl)
    end

    return value
  end


  ---
  -- In place updates record's field from a cached reference.
  --
  -- @local
  -- @function update_from_cache
  -- @tparam string reference reference to look from the caches
  -- @tparam table record record which field is updated from caches
  -- @tparam string field name of the field
  --
  -- @usage
  -- local record = { field = "old-value" }
  -- update_from_cache("{vault://env/example}", record, "field" })
  local function update_from_cache(reference, record, field)
    local value, err = get(reference, true)
    if err then
      self.log.warn("error updating secret reference ", reference, ": ", err)
    end

    record[field] = value or ""
  end


  ---
  -- Recurse over config and calls the callback for each found reference.
  --
  -- @local
  -- @function recurse_config_refs
  -- @tparam table config config table to recurse.
  -- @tparam function callback callback to call on each reference.
  -- @treturn table config that might have been updated, depending on callback.
  local function recurse_config_refs(config, callback)
    -- silently ignores other than tables
    if type(config) ~= "table" then
      return config
    end

    for key, value in pairs(config) do
      if key ~= "$refs" and type(value) == "table" then
        recurse_config_refs(value, callback)
      end
    end

    local references = config["$refs"]
    if type(references) ~= "table" or isempty(references) then
      return config
    end

    for name, reference in pairs(references) do
      if type(reference) == "string" then -- a string reference
        callback(reference, config, name)

      elseif type(reference) == "table" then -- array, set or map of references
        for key, ref in pairs(reference) do
          callback(ref, config[name], key)
        end
      end
    end

    return config
  end


  ---
  -- Function `update` recursively updates a configuration table.
  --
  -- This function recursively in-place updates a configuration table by
  -- replacing reference fields with values fetched from a cache. The references
  -- are specified in a `$refs` field.
  --
  -- If a reference cannot be fetched from the cache, the corresponding field is
  -- set to nil and an warning is logged.
  --
  -- @local
  -- @function update
  -- @tparam table config a table representing the configuration to update (if `config`
  -- is not a table, the function immediately returns it without any modifications)
  -- @treturn table the config table (with possibly updated values).
  --
  -- @usage
  -- local config = update(config)
  -- OR
  -- update(config)
  local function update(config)
    return recurse_config_refs(config, update_from_cache)
  end


  ---
  -- Function `get_references` recursively iterates over options and returns
  -- all the references in an array. The same reference is in array only once.
  --
  -- @local
  -- @function get_references
  -- @tparam table options the options to look for the references
  -- @tparam[opt] table references internal variable that is used for recursion
  -- @tparam[opt] collected references internal variable that is used for recursion
  -- @treturn table an array of collected references
  --
  -- @usage
  -- local references = get_references({
  --   username = "john",
  --   password = "doe",
  --   ["$refs"] = {
  --     username = "{vault://aws/database/username}",
  --     password = "{vault://aws/database/password}",
  --   }
  -- })
  local function get_references(options, references, collected)
    references = references or {}
    collected = collected or { n = 0 }

    if type(options) ~= "table" then
      return references
    end

    for key, value in pairs(options) do
      if key ~= "$refs" and type(value) == "table" then
        get_references(value, references, collected)
      end
    end

    local refs = options["$refs"]
    if type(refs) ~= "table" or isempty(refs) then
      return references
    end

    for _, reference in pairs(refs) do
      if type(reference) == "string" then -- a string reference
        if not collected[reference] then
          collected[reference] = true
          collected.n = collected.n + 1
          references[collected.n] = reference
        end

      elseif type(reference) == "table" then -- array, set or map of references
        for _, ref in pairs(reference) do
          if not collected[ref] then
            collected[ref] = true
            collected.n = collected.n + 1
            references[collected.n] = ref
          end
        end
      end
    end

    return references
  end


  ---
  -- Function `get_sorted_references` recursively iterates over options and returns
  -- all the references in an sorted array. The same reference is in array only once.
  --
  -- @local
  -- @function get_sorted_references
  -- @tparam table options the options to look for the references
  -- @treturn table|nil an sorted array of collected references, return `nil` in case no references were found.
  --
  -- @usage
  -- local references = get_sorted_references({
  --   username = "john",
  --   password = "doe",
  --   ["$refs"] = {
  --     username = "{vault://aws/database/username}",
  --     password = "{vault://aws/database/password}",
  --   }
  -- })
  local function get_sorted_references(options)
    local references = get_references(options)
    if isempty(references) then
      return
    end

    sort(references)

    return references
  end


  ---
  -- Function `rotate_reference` rotates a secret reference.
  --
  -- @local
  -- @function rotate_reference
  -- @tparam string reference the reference to rotate
  -- @tparam function the caching strategy created with `get_caching_strategy` function
  -- @treturn true|nil `true` after successfully rotating a secret, otherwise `nil`
  -- @treturn string|nil a string describing an error if there was one
  local function rotate_reference(reference, caching_strategy)
    local strategy, err, config, new_cache_key, parsed_reference, config_hash = get_strategy(reference)
    if not strategy then
      return nil, fmt("could not parse reference %s (%s)", reference, err)
    end

    strategy = caching_strategy(strategy, config_hash)

    local ok, err = get_from_vault(reference, strategy, config, new_cache_key, parsed_reference)
    if not ok then
      return nil, fmt("could not retrieve value for reference %s (%s)", reference, err)
    end

    return true
  end


  ---
  -- Function `rotate_references` rotates the references passed in as an array.
  --
  -- @local
  -- @function rotate_references
  -- @tparam table references an array of references to rotate
  -- @treturn boolean `true` after it has finished rotation over all the references
  local function rotate_references(references)
    local phase = get_phase()
    local caching_strategy = get_caching_strategy()
    for _, reference in ipairs(references) do
      yield(true, phase)

      local ok, err = rotate_reference(reference, caching_strategy)
      if not ok then
        self.log.warn(err)
      end
    end

    return true
  end


  ---
  -- Function `execute_callback` updates options and then executes the callback
  --
  -- @local
  -- @function execute_callback
  -- @tparam function callback the callback to execute
  -- @tparam table the callback options to be passed to callback (after updating them)
  -- @treturn any the callback return value
  -- @treturn string|nil a string describing an error if there was one
  local function execute_callback(callback, options)
    update(options)
    return callback(options)
  end


  ---
  -- Function `try` attempts to execute a provided callback function with the provided options.
  --
  -- If the callback function fails, the `try` function will attempt to resolve references and update
  -- the values in the options table before re-attempting the callback function.
  --
  -- @local
  -- @function try
  -- @tparam function callback the callback function to execute that takes options table as its argument
  -- @tparam table options the options table to provide to the callback function.
  -- @treturn any the result of the callback function if it succeeds, otherwise `nil`
  -- @treturn string|nil a string describing an error if there was one
  --
  -- @usage
  -- local function connect(options)
  --   return database_connect(options)
  -- end
  --
  -- local connection, err = try(connect, {
  --   username = "john",
  --   password = "doe",
  --   ["$refs"] = {
  --     username = "{vault://aws/database/username}",
  --     password = "{vault://aws/database/password}",
  --   }
  -- })
  local function try(callback, options)
    local references = get_sorted_references(options)
    if not references then
      -- We cannot retry, so let's just call the callback and return
      return callback(options)
    end

    local name = "vault.try:" .. calculate_hash(concat(references, "."))
    local old_updated_at = RETRY_LRU:get(name) or 0

    -- Try to execute the callback with the current options
    local res = execute_callback(callback, options)
    if res then
      return res -- If the callback succeeds, return the result
    end

    -- Check if options were updated while executing callback
    local new_updated_at = RETRY_LRU:get(name) or 0
    if old_updated_at ~= new_updated_at then
      return execute_callback(callback, options)
    end

    -- Is it worth to have node level mutex instead?
    -- If so, the RETRY_LRU also needs to be node level.
    concurrency.with_coroutine_mutex({
      name = name,
      timeout = ROTATION_INTERVAL,
    }, function()
      -- Check if references were updated while waiting for a lock
      new_updated_at = RETRY_LRU:get(name) or 0
      if old_updated_at ~= new_updated_at then
        return -- already updated
      end

      rotate_references(references)
      RETRY_LRU:set(name, get_updated_now_ms())
    end)

    -- Call the callback the second time
    -- (may be same options as before, but not worth to optimize)
    return execute_callback(callback, options)
  end


  ---
  -- Function `rotate_secret` rotates a secret reference.
  --
  -- @local
  -- @function rotate_secret
  -- @tparam string old_cache_key old cache key
  -- @tparam function the caching strategy created with `get_caching_strategy` function
  -- @treturn true|nil `true` after successfully rotating a secret, otherwise `nil`
  -- @treturn string|nil a string describing an error if there was one
  local function rotate_secret(old_cache_key, caching_strategy)
    local reference, err = parse_cache_key(old_cache_key)
    if not reference then
      -- invalid cache keys are removed (in general should never happen)
      SECRETS_CACHE:delete(old_cache_key)
      return nil, err
    end

    local strategy, err, config, new_cache_key, parsed_reference, config_hash = get_strategy(reference)
    if not strategy then
      -- invalid cache keys are removed (e.g. a vault entity could have been removed)
      SECRETS_CACHE:delete(old_cache_key)
      return nil, fmt("could not parse reference %s (%s)", reference, err)
    end

    if old_cache_key ~= new_cache_key then
      -- config has changed, thus the old cache key can be removed
      SECRETS_CACHE:delete(old_cache_key)
    end

    -- The ttl for this key, is the TTL + the resurrect time
    -- If the TTL is still greater than the resurrect time
    -- we don't have to rotate the secret, except it if it
    -- negatively cached.
    local resurrect
    local ttl = SECRETS_CACHE:ttl(new_cache_key)
    if ttl and SECRETS_CACHE:get(new_cache_key) ~= NEGATIVELY_CACHED_VALUE then
      local resurrect_ttl = max(config.resurrect_ttl or DAO_MAX_TTL, SECRETS_CACHE_MIN_TTL)
      -- the secret is still within ttl, no need to refresh
      if ttl > resurrect_ttl then
        return true
      end

      -- the secret is still within resurrect ttl time, so when we try to refresh the secret
      -- we do not forciblly override it with a negative value, so that the cached value
      -- can be resurrected
      resurrect = ttl > SECRETS_CACHE_MIN_TTL
    end

    strategy = caching_strategy(strategy, config_hash)

    -- try to refresh the secret, according to the remaining time the cached value may or may not be refreshed.
    local ok, err = get_from_vault(reference, strategy, config, new_cache_key, parsed_reference, resurrect)
    if not ok then
      return nil, fmt("could not retrieve value for reference %s (%s)", reference, err)
    end

    return true
  end


  ---
  -- Function `rotate_secrets` rotates the secrets.
  --
  -- It iterates over all keys in the secrets and, if a key corresponds to a reference and the
  -- ttl of the key is less than or equal to the resurrection period, it refreshes the value
  -- associated with the reference.
  --
  -- @local
  -- @function rotate_secrets
  -- @tparam table secrets the secrets to rotate
  -- @treturn boolean `true` after it has finished iterating over all keys in the secrets
  local function rotate_secrets(secrets)
    local phase = get_phase()
    local caching_strategy = get_caching_strategy()
    for _, cache_key in ipairs(secrets) do
      yield(true, phase)

      local ok, err = rotate_secret(cache_key, caching_strategy)
      if not ok then
        self.log.notice(err)
      end
    end

    return true
  end


  ---
  -- Function `rotate_secrets_cache` rotates the secrets in the shared dictionary cache.
  --
  -- @local
  -- @function rotate_secrets_cache
  -- @treturn boolean `true` after it has finished iterating over all keys in the shared dictionary cache
  local function rotate_secrets_cache()
    return rotate_secrets(SECRETS_CACHE:get_keys(0))
  end


  ---
  -- Function `rotate_secrets_init_worker` rotates the secrets in init worker cache
  --
  -- On init worker the secret resolving is postponed to a timer because init worker
  -- cannot cosockets / coroutines, and there is no other workaround currently.
  --
  -- @local
  -- @function rotate_secrets_init_worker
  -- @treturn boolean `true` after it has finished iterating over all keys in the init worker cache
  local function rotate_secrets_init_worker()
    local _, err, err2
    if INIT_SECRETS then
      _, err = rotate_references(INIT_SECRETS)
    end

    if INIT_WORKER_SECRETS then
      _, err2 = rotate_secrets(INIT_WORKER_SECRETS)
    end

    if err or err2 then
      return nil, err or err2
    end

    return true
  end


  ---
  -- A secrets rotation timer handler.
  --
  -- Uses a node-level mutex to prevent multiple threads/workers running it the same time.
  --
  -- @local
  -- @function rotate_secrets_timer
  -- @tparam boolean premature `true` if server is shutting down
  -- @tparam[opt] boolean init `true` when this is a one of init_worker timer run
  -- By default rotates the secrets in shared dictionary cache.
  local function rotate_secrets_timer(premature, init)
    if premature then
      return true
    end

    local ok, err = concurrency.with_worker_mutex(ROTATION_MUTEX_OPTS, init and rotate_secrets_init_worker or rotate_secrets_cache)
    if not ok and err ~= "timeout" then
      self.log.err("rotating secrets failed (", err, ")")
    end

    if init then
      INIT_SECRETS = nil
      INIT_WORKER_SECRETS = nil
    end

    return true
  end


  ---
  -- Flushes LRU caches and forcibly rotates the secrets.
  --
  -- This is only ever executed on traditional nodes.
  --
  -- @local
  -- @function handle_vault_crud_event
  -- @tparam table data event data
  local function handle_vault_crud_event(data)
    local cache = self.core_cache
    if cache then
      local vaults = self.db.vaults
      local old_entity = data.old_entity
      local old_prefix
      if old_entity then
        old_prefix = old_entity.prefix
        if old_prefix and old_prefix ~= ngx.null then
          cache:invalidate(vaults:cache_key(old_prefix))
        end
      end

      local entity = data.entity
      if entity then
        local prefix = entity.prefix
        if prefix and prefix ~= ngx.null and prefix ~= old_prefix then
          cache:invalidate(vaults:cache_key(prefix))
        end
      end
    end

    LRU:flush_all()

    -- refresh all the secrets
    local _, err = self.timer:named_at("secret-rotation-on-crud-event", 0, rotate_secrets_timer)
    if err then
      self.log.err("could not schedule timer to rotate vault secret references on crud event: ", err)
    end
  end


  local function should_register_crud_event()
    local conf = self.configuration

    local not_dbless = conf.database ~= "off"  -- postgres
    local dp_with_rpc_sync = conf.role == "data_plane" and
                             conf.cluster_rpc_sync

    return not_dbless or dp_with_rpc_sync
  end

  local initialized
  ---
  -- Initializes vault.
  --
  -- Registers event handlers and starts a recurring secrets
  -- rotation timer. It does nothing on control planes.
  --
  -- @local
  -- @function init_worker
  local function init_worker()
    if initialized then
      return
    end

    initialized = true

    if should_register_crud_event() then
      self.worker_events.register(handle_vault_crud_event, "crud", "vaults")
    end

    local _, err = self.timer:named_every("secret-rotation", ROTATION_INTERVAL, rotate_secrets_timer)
    if err then
      self.log.err("could not schedule timer to rotate vault secret references: ", err)
    end

    local _, err = self.timer:named_at("secret-rotation-on-init", 0, rotate_secrets_timer, true)
    if err then
      self.log.err("could not schedule timer to rotate vault secret references on init: ", err)
    end
  end


  ---
  -- Called on `init` phase, and stores value in secrets cache.
  --
  -- @local
  -- @function init_in_cache_from_value
  -- @tparam string reference a vault reference.
  -- @tparan value string value that is stored in secrets cache.
  local function init_in_cache_from_value(reference, value)
    local strategy, err, config, cache_key = get_strategy(reference)
    if not strategy then
      return nil, err
    end

    -- doesn't support vault returned ttl, but none of the vaults supports it,
    -- and the support for vault returned ttl might be removed later.
    local cache_value, shdict_ttl, lru_ttl = get_cache_value_and_ttl(value, config)

    local ok, cache_err = SECRETS_CACHE:safe_set(cache_key, cache_value, shdict_ttl)
    if not ok then
      return nil, cache_err
    end

    if cache_value ~= NEGATIVELY_CACHED_VALUE then
      LRU:set(reference, cache_value, lru_ttl)
    end

    return true
  end


  ---
  -- Called on `init` phase, and used to warmup secrets cache.
  --
  -- @local
  -- @function init_in_cache
  -- @tparam string reference a vault reference.
  -- @tparan table record a table that is a container for de-referenced value.
  -- @tparam field string field name in a record to which to store the de-referenced value.
  local function init_in_cache(reference, record, field)
    local value, err = init_in_cache_from_value(reference, record[field])
    if not value then
      self.log.warn("error caching secret reference ", reference, ": ", err)
    end
  end


  ---
  -- Called on `init` phase, and used to warmup secrets cache.
  -- @local
  -- @function init
  local function init()
    recurse_config_refs(self.configuration, init_in_cache)
  end


  local _VAULT = {} -- the public PDK interfaces


  ---
  -- Flush vault LRU cache and start a timer to rotate secrets.
  --
  -- @local
  -- @function kong.vault.flush
  --
  -- @usage
  -- kong.vault.flush()
  function _VAULT.flush()
    LRU:flush_all()

    -- refresh all the secrets
    local _, err = self.timer:named_at("secret-rotation-on-flush", 0, rotate_secrets_timer)
    if err then
      self.log.err("could not schedule timer to rotate vault secret references: ", err)
    end
  end


  ---
  -- Checks if the passed in reference looks like a reference.
  -- Valid references start with '{vault://' and end with '}'.
  --
  -- If you need more thorough validation,
  -- use `kong.vault.parse_reference`.
  --
  -- @function kong.vault.is_reference
  -- @tparam string reference reference to check
  -- @treturn boolean `true` is the passed in reference looks like a reference, otherwise `false`
  --
  -- @usage
  -- kong.vault.is_reference("{vault://env/key}") -- true
  -- kong.vault.is_reference("not a reference")   -- false
  function _VAULT.is_reference(reference)
    return is_reference(reference)
  end


  ---
  -- Parses and decodes the passed in reference and returns a table
  -- containing its components.
  --
  -- Given a following resource:
  -- ```lua
  -- "{vault://env/cert/key?prefix=SSL_#1}"
  -- ```
  --
  -- This function will return following table:
  --
  -- ```lua
  -- {
  --   name     = "env",  -- name of the Vault entity or Vault strategy
  --   resource = "cert", -- resource where secret is stored
  --   key      = "key",  -- key to lookup if the resource is secret object
  --   config   = {       -- if there are any config options specified
  --     prefix = "SSL_"
  --   },
  --   version  = 1       -- if the version is specified
  -- }
  -- ```
  --
  -- @function kong.vault.parse_reference
  -- @tparam string reference reference to parse
  -- @treturn table|nil a table containing each component of the reference, or `nil` on error
  -- @treturn string|nil error message on failure, otherwise `nil`
  --
  -- @usage
  -- local ref, err = kong.vault.parse_reference("{vault://env/cert/key?prefix=SSL_#1}") -- table
  function _VAULT.parse_reference(reference)
    return parse_reference(reference)
  end


  ---
  -- Resolves the passed in reference and returns the value of it.
  --
  -- @function kong.vault.get
  -- @tparam string reference  reference to resolve
  -- @treturn string|nil resolved value of the reference
  -- @treturn string|nil error message on failure, otherwise `nil`
  --
  -- @usage
  -- local value, err = kong.vault.get("{vault://env/cert/key}")
  function _VAULT.get(reference)
    return get(reference)
  end


  ---
  -- Helper function for secret rotation based on TTLs. Currently experimental.
  --
  -- @function kong.vault.update
  -- @tparam table options options containing secrets and references (this function modifies the input options)
  -- @treturn table options with updated secret values
  --
  -- @usage
  -- local options = kong.vault.update({
  --   cert = "-----BEGIN CERTIFICATE-----...",
  --   key = "-----BEGIN RSA PRIVATE KEY-----...",
  --   cert_alt = "-----BEGIN CERTIFICATE-----...",
  --   key_alt = "-----BEGIN EC PRIVATE KEY-----...",
  --   ["$refs"] = {
  --     cert = "{vault://aws/cert}",
  --     key = "{vault://aws/key}",
  --     cert_alt = "{vault://aws/cert-alt}",
  --     key_alt = "{vault://aws/key-alt}",
  --   }
  -- })
  --
  -- -- or
  --
  -- local options = {
  --   cert = "-----BEGIN CERTIFICATE-----...",
  --   key = "-----BEGIN RSA PRIVATE KEY-----...",
  --   cert_alt = "-----BEGIN CERTIFICATE-----...",
  --   key_alt = "-----BEGIN EC PRIVATE KEY-----...",
  --   ["$refs"] = {
  --     cert = "{vault://aws/cert}",
  --     key = "{vault://aws/key}",
  --     cert_alt = "{vault://aws/cert-alt}",
  --     key_alt = "{vault://aws/key-alt}",
  --   }
  -- }
  -- kong.vault.update(options)
  function _VAULT.update(options)
    return update(options)
  end


  ---
  -- Helper function for automatic secret rotation. Currently experimental.
  --
  -- @function kong.vault.try
  -- @tparam function callback callback function
  -- @tparam table options options containing credentials and references
  -- @treturn string|nil return value of the callback function
  -- @treturn string|nil error message on failure, otherwise `nil`
  --
  -- @usage
  -- local function connect(options)
  --   return database_connect(options)
  -- end
  --
  -- local connection, err = kong.vault.try(connect, {
  --   username = "john",
  --   password = "doe",
  --   ["$refs"] = {
  --     username = "{vault://aws/database-username}",
  --     password = "{vault://aws/database-password}",
  --   }
  -- })
  function _VAULT.try(callback, options)
    return try(callback, options)
  end


  ---
  -- Initializes vault.
  --
  -- Registers event handlers (on non-dbless nodes) and starts a recurring secrets
  -- rotation timer. Does nothing on control planes.
  --
  -- @local
  -- @function kong.vault.init_worker
  function _VAULT.init_worker()
    init_worker()
  end


  ---
  -- Warmups vault caches from config.
  --
  -- @local
  -- @function kong.vault.warmup
  function _VAULT.warmup(input)
    for k, v in pairs(input) do
      local kt = type(k)
      if kt == "table" then
        _VAULT.warmup(k)
      elseif kt == "string" and is_reference(k) then
        get(k)
      end
      local vt = type(v)
      if vt == "table" then
        _VAULT.warmup(v)
      elseif vt == "string" and is_reference(v) then
        get(v)
      end
    end
  end

  if get_phase() == "init" then
    init()
  end

  return _VAULT
end


return {
  new = new,
  is_reference = is_reference,
  parse_reference = parse_reference,
}
