Current section

Files

Jump to
Raw

mix.exs

defmodule AshOnetime.MixProject do
  use Mix.Project

  @version "1.4.2"
  @source_url "https://github.com/baselabs/ash_onetime"

  def project do
    [
      app: :ash_onetime,
      version: @version,
      # Elixir requirement — the PUBLIC support window, not a build pin: `~> 1.20`
      # admits >= 1.20.0 and < 2.0.0. The compatibility evidence below names
      # the exact runtime exercised; this requirement is not a build pin.
      # This repo's OWN exact toolchain identity is enforced separately:
      # .tool-versions and CI's ELIXIR_VERSION/OTP_VERSION pin the build every
      # gate runs on (currently 1.20.4 / OTP 29.0.3), and config/config.exs
      # refuses a foreign OTP build before anything compiles. Lockstep rule:
      # this requirement, .tool-versions, and CI's ELIXIR_VERSION/OTP_VERSION
      # move together in ONE commit — the pinned toolchain must satisfy this
      # requirement; divergence between the three is a defect.
      elixir: "~> 1.20",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      description: description(),
      package: package(),
      docs: docs(),
      # :oban/:plug/:igniter/:mint/:stream_data/:ash_sql are all runtime: false, so
      # dialyxir no longer seeds the PLT with them on its own, while the guarded
      # integration modules still analyze against their beams — they must be added
      # explicitly. :ash_sql stays seeded transitively through ash_postgres's own
      # app spec; :mint needs no explicit entry because nothing in lib/ compiles
      # against it (it binds only inside the optional installer closure).
      dialyzer: [plt_add_apps: [:ecto_sql, :ex_unit, :mix, :oban, :plug, :igniter]],
      test_paths: ["test"],
      test_ignore_filters: [&String.starts_with?(&1, "test/compile_fixtures/")]
    ]
  end

  def application do
    [extra_applications: [:crypto, :logger]]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_env), do: ["lib"]

  defp deps do
    [
      {:ash, ash_requirement()},
      # Security floor (ADR 0004): AshPostgres 2.13.0 is where EEF-CVE-2026-78699 is
      # fixed, and AshPostgres still admits older ash_sql releases — the explicit
      # ash_sql floor below closes that gap for consumers. `runtime: false` keeps
      # AshPostgres the owner of ash_sql's startup; the < 1.0.0 cap is the same
      # matrix-extension posture as the Ash < 4.0.0 bound (a breaking major is
      # proven in CI before the cap moves).
      {:ash_postgres, "~> 2.13"},
      {:ash_sql, "~> 0.7 and >= 0.7.1", runtime: false},
      {:spark, "~> 2.7"},
      {:ecto_sql, "~> 3.14"},
      {:postgrex, "~> 0.22"},
      {:jason, "~> 1.4"},
      {:telemetry, "~> 1.4"},
      # The three optional integrations (Plug module, Oban workers, Igniter installer) are
      # host-owned surfaces: the host lists the dep itself, so the host's own app spec starts
      # it. `runtime: false` keeps an attached optional out of ash_onetime's generated
      # `applications` list — otherwise every host that carries the dep inherits it as a
      # runtime application of ash_onetime (the app-spec leak flagged by consumer-side audit).
      # stream_data is property-test-only here; it cannot take `only: :test` because ash
      # itself requires stream_data unrestricted, and Mix rejects a direct entry whose
      # :only excludes an env a sibling needs it in — so it is kept resolvable in every env
      # but kept out of the runtime applications list.
      {:plug, "~> 1.20", optional: true, runtime: false},
      {:oban, "~> 2.23", optional: true, runtime: false},
      {:igniter, "~> 0.8 and >= 0.8.4", optional: true, runtime: false},
      # Constrain the installer's HTTP closure without adding HTTP to core
      # consumers: mint reaches a tree only through igniter → req → finch, so
      # an optional requirement binds exactly the closure that carries it.
      # Security floor (ADR 0004): Mint 1.10.2 backports the 1.11.0 fixes for
      # EEF-CVE-2026-91043/-92103 (HTTP/2 memory exhaustion) and -94194
      # (HTTP/1 response smuggling). Preserve that patched 1.10 release for
      # consumers while the development lock exercises the latest 1.x.
      {:mint, "~> 1.10 and >= 1.10.2", optional: true, runtime: false},
      {:ex_doc, "~> 0.40", only: :dev, runtime: false},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
      {:stream_data, "~> 1.4", runtime: false}
    ]
  end

  # `>= 3.34.3 and < 4.0.0` is the consumer requirement. The October 6, 2026
  # package-wide advisory inventory (ADR 0004) finds EEF-CVE-2026-94201
  # (unsafe atom-attribute filters can exhaust the BEAM atom table) fixed
  # only in 3.34.3. The bulk private-argument fix for -93477 shipped in
  # 3.33.11; that version remains affected by -94201.
  # The CI compatibility matrix sets ASH_ONETIME_ASH_VERSION to pin
  # one exact Ash per cell (the floor and each later minor); `latest`/unset keeps the floating
  # requirement so the newest published Ash is exercised. The namespaced var name is extremely
  # unlikely to collide with anything in a consumer's environment, so a published build sees
  # the full requirement. A pin is validated at project-config evaluation time: it must be a
  # version inside the published range, else Mix.raise fires — a publish with an out-of-range
  # pin exported would otherwise silently freeze a wrong exact requirement into the package.
  @ash_floor "3.34.3"

  defp ash_requirement do
    case System.get_env("ASH_ONETIME_ASH_VERSION") do
      version when version in [nil, "", "latest"] -> ">= #{@ash_floor} and < 4.0.0"
      version -> pinned_ash_requirement(version)
    end
  end

  defp pinned_ash_requirement(version) do
    with {:ok, parsed} <- Version.parse(version),
         true <- parsed.pre == [],
         true <- is_nil(parsed.build),
         true <- Version.compare(parsed, @ash_floor) != :lt,
         true <- Version.compare(parsed, "4.0.0") == :lt do
      "== #{version}"
    else
      _ ->
        Mix.raise("""
        ASH_ONETIME_ASH_VERSION must be a plain release version inside the published \
        Ash range >= #{@ash_floor} and < 4.0.0 (no pre-release or build-metadata \
        suffix — SemVer orders them inside the range while Hex would never resolve \
        them for the floating requirement), or "latest"/unset for the floating \
        requirement; got: #{inspect(version)}. Unset the variable or pin an in-range \
        release version — publishing with an out-of-range pin exported would freeze \
        a wrong exact requirement into the package.\
        """)
    end
  end

  defp description do
    "An Ash extension for explicit idempotency and one-time nonce semantics"
  end

  defp package do
    [
      files: [
        "lib",
        "priv/templates",
        "documentation",
        "mix.exs",
        "README.md",
        "CHANGELOG.md",
        "CONTRIBUTING.md",
        "SECURITY.md",
        "LICENSE",
        "usage-rules.md"
      ],
      licenses: ["MIT"],
      links: %{"GitHub" => @source_url},
      maintainers: ["Russ Palermo"],
      keywords: ["ash", "idempotency", "nonce", "anti-replay", "replay-protection"]
    ]
  end

  defp docs do
    [
      main: "readme",
      source_ref: "v#{@version}",
      source_url: @source_url,
      extras: [
        "README.md",
        "documentation/getting-started.md",
        "documentation/dsl.md",
        "documentation/idempotency.md",
        "documentation/one-time-nonces.md",
        "documentation/external-effects.md",
        "documentation/replay.md",
        "documentation/custom-lifecycle.md",
        "documentation/transaction-owned-admission.md",
        "documentation/errors.md",
        "documentation/operations.md",
        "documentation/security.md",
        "documentation/recipes.md",
        "documentation/phoenix.md",
        "documentation/telemetry.md",
        "documentation/upgrading.md",
        "documentation/faq.md",
        "documentation/livebooks/idempotency.livemd",
        "documentation/livebooks/nonces.livemd",
        "documentation/livebooks/external-recovery.livemd",
        "documentation/dsls/DSL-AshOnetime.Resource.md",
        "CONTRIBUTING.md",
        "SECURITY.md",
        "usage-rules.md",
        "CHANGELOG.md"
      ]
    ]
  end
end