OpenFeed.Config (OpenFeed v0.1.0)

Copy Markdown View Source

Everything this library needs to talk to OpenFeed, as an explicit struct.

This package is config-injected, not config-reading: nothing here reads Application.get_env/2. Every entry point takes a OpenFeed.Config.

That is a deliberate design choice. It means the FAPI 2.0 surface can be tested against a stub with no application environment at all, and it means an application can hold more than one OpenFeed registration (one per tenant, say) without this library needing to know. Deciding where the values come from — runtime.exs, a secrets manager, a database row — is the caller's job.

Building one

OpenFeed.Config.new!(
  client_id: System.fetch_env!("OPENFEED_CLIENT_ID"),
  redirect_uri: "https://my.app/openfeed/callback",
  key_store: {OpenFeed.KeyStore.File, path: "/run/secrets/openfeed.jwk"}
)

Client authentication

OpenFeed offers two client profiles, and the difference matters:

  • :private_key_jwt (default) — the Recommended profile. You hold a keypair, register its public JWKS with OpenFeed, and no client secret is ever transmitted. The same key signs DPoP proofs. Requires a :key_store.
  • :client_secret_basic — the Legacy profile. Requires :client_secret, and gets you bearer tokens rather than DPoP sender-constrained ones.

Prefer :private_key_jwt unless you have a reason not to.

Testing

:req_options is merged into every HTTP request this library makes, which is the seam for stubbing:

OpenFeed.Config.new!(
  client_id: "test",
  issuer: "https://auth.example.test",
  req_options: [plug: {Req.Test, MyStub}],
  ...
)

Summary

Functions

Default sharing-api base URL (https://api.openfeed.au).

Default issuer (https://auth.openfeed.au).

Build and validate a config.

Like new/1 but raises ArgumentError on invalid config.

The scopes to request, expanded to full scope strings.

Types

key_store()

@type key_store() :: {module(), keyword()}

t()

@type t() :: %OpenFeed.Config{
  api_base: String.t(),
  auth_method: :private_key_jwt | :client_secret_basic,
  client_id: String.t() | nil,
  client_secret: String.t() | nil,
  issuer: String.t(),
  key_store: key_store() | nil,
  provider_name: atom(),
  redirect_uri: String.t() | nil,
  req_options: keyword(),
  scopes: [atom() | String.t()]
}

Functions

default_api_base()

@spec default_api_base() :: String.t()

Default sharing-api base URL (https://api.openfeed.au).

default_issuer()

@spec default_issuer() :: String.t()

Default issuer (https://auth.openfeed.au).

new(opts)

@spec new(keyword() | map()) :: {:ok, t()} | {:error, {:invalid_config, String.t()}}

Build and validate a config.

Returns {:error, {:invalid_config, message}} rather than raising, so callers can surface configuration problems as ordinary errors.

new!(opts)

@spec new!(keyword() | map()) :: t()

Like new/1 but raises ArgumentError on invalid config.

scopes(config)

@spec scopes(t()) :: [String.t()]

The scopes to request, expanded to full scope strings.

See OpenFeed.Scopes.expand/2.