Powered by AppSignal & Oban Pro

Contributing to ExkPasswd

notebooks/contributing.livemd

Contributing to ExkPasswd

Mix.install([
  {:exk_passwd, path: Path.join(__DIR__, "..")}
])

This notebook is a small architecture tour. The terminal quality gates remain the source of truth for a contribution.

Generation path

config =
  ExkPasswd.Config.new!(
    num_words: 3,
    word_length: 4..6,
    separator: "-",
    case_transform: :capitalize
  )

password = ExkPasswd.Password.create(config)
report = ExkPasswd.Entropy.calculate(password, config)

%{config: config, password: password, report: report}

The main responsibilities are:

  • Config and Config.Schema: strict construction and validation
  • Random and Buffer: unbiased cryptographic range generation
  • Dictionary: EFF indexes and :persistent_term custom dictionaries
  • Password: generation orchestration
  • Transform: extension protocol and built-in transforms
  • Entropy and Strength: output-space analysis and project ratings
  • Batch: buffered, unique, and parallel generation APIs

Inspect dictionary ranges

%{
  total_eff_words: ExkPasswd.Dictionary.size(),
  eligible_4_to_6: ExkPasswd.Dictionary.count_between(4, 6),
  sample: ExkPasswd.Dictionary.random_word_between(4, 6)
}

Common EFF ranges are precomputed. Custom and uncommon ranges use a by-length fallback; avoid claiming that every dictionary API has the same complexity.

Exercise custom input validation

results = [
  ExkPasswd.Config.new(num_words: 0),
  ExkPasswd.Config.new(unknown: true),
  ExkPasswd.Config.new(padding: :invalid)
]

results

Expected caller errors should be tagged tuples from new/1 or ArgumentError from bang/public generation functions. Do not let malformed maps fail later with unrelated KeyError or protocol errors.

Add a custom dictionary

:ok = ExkPasswd.Dictionary.load_custom(:contributor_demo, ["alpha", "bravo", "delta", "hotel"])

custom =
  ExkPasswd.Config.new!(
    dictionary: :contributor_demo,
    word_length: 5..5,
    num_words: 3,
    separator: "-"
  )

ExkPasswd.generate(custom)

Custom dictionaries are NFC-normalized and reject duplicates. Loading writes to :persistent_term; it belongs in application startup code, not a hot path.

Add a transform

defmodule ContributorSuffix do
  defstruct suffix: ""

  defimpl ExkPasswd.Transform do
    def apply(%{suffix: suffix}, word, _config), do: word <> suffix
    def entropy_bits(_, _config), do: 0.0
  end
end
transformed =
  ExkPasswd.Config.new!(
    num_words: 2,
    meta: %{transforms: [%ContributorSuffix{suffix: "."}]}
  )

ExkPasswd.generate(transformed)

Document whether a transform is deterministic, whether it can merge source words, and which scripts it covers. Add regression tests for every linguistic example shown in public documentation.

Required checks

Run these from the project root:

mix format
mix compile --warnings-as-errors
mix credo --strict
mix test
mix coveralls.html
mix dialyzer
mix doctor
mix docs
mix deps.audit

Use Conventional Commits. Do not update benchmark claims without recording a fresh run and its environment. Security-sensitive randomness changes need tests for boundary values, rejection behavior, buffered and unbuffered paths, and invalid inputs.