# `Attesto.Scope`
[🔗](https://github.com/XukuLLC/attesto/blob/v0.13.0/lib/attesto/scope.ex#L1)

Scope grant-form matching for OAuth-style `<resource>.<action>` scopes.

Attesto does not define *which* scopes exist - that catalog is your
application's policy. It defines the *algebra*: given a catalog of
concrete scope strings, what counts as a legal grant form, and whether
a granted set covers a required scope. Build a catalog once with
`new_catalog/1` and thread it through the matching functions.

A scope is a dotted string of the form `<resource>.<action>`
(e.g. `trackers.read`).

## Grant forms

A *granted* scope (one stored on a credential, or carried in a JWT's
`scope` claim) may be:

  * a concrete catalog entry such as `trackers.read`;
  * the resource-level wildcard `<resource>.*` (e.g. `webhooks.*`),
    which grants every catalog action under that resource;
  * the full wildcard `*`, which grants every catalog scope. Reserved
    for system-issued credentials; customer-facing issuance must not
    surface or accept it.

Two validators encode this asymmetry:

  * `valid_grant_form?/2` accepts `*` and is the right check for
    system-issued credentials.
  * `customer_grant_form?/2` rejects `*` and is the check a public
    token endpoint or credential changeset must use.

Wildcard grants are parsed strictly: only `<resource>.*` (exactly one
dot, no other segments) is accepted. Deep forms like `trackers.read.*`
are rejected so a grant is never silently broadened past a single
resource.

## Required scopes

A *required* scope (one a protected endpoint declares) MUST be a
concrete catalog entry - passing a wildcard form as the requirement
returns `false`, since a wildcard requirement would be ambiguous. Even
a `*`-granted credential only authorizes catalog entries, so an
uncatalogued endpoint requirement is never granted.

`grants_all?/3` raises `ArgumentError` on a nil or empty required-scope
list so a misconfigured authorization declaration (an endpoint that
forgot to declare its required scope) fails loudly instead of silently
authorizing every caller.

## Why strings, not atoms

Scopes round-trip through HTTP requests, JWT claims, and database
columns as strings; they are never coerced to atoms (a denial-of-service
vector for externally-influenced values).

# `t`

```elixir
@type t() :: %Attesto.Scope{
  entries: MapSet.t(String.t()),
  resources: MapSet.t(String.t())
}
```

# `customer_grant_form?`

```elixir
@spec customer_grant_form?(t(), term()) :: boolean()
```

Returns `true` iff `scope` is a legal granted form for a
**customer-facing** credential: a concrete catalog entry or a
resource-level wildcard `<resource>.*` whose resource appears in the
catalog. The full wildcard `*` is rejected.

# `entries`

```elixir
@spec entries(t()) :: [String.t()]
```

The concrete scope strings in the catalog, sorted.

# `grants?`

```elixir
@spec grants?(t(), [String.t()] | nil, String.t()) :: boolean()
```

Returns `true` iff the `granted` scope list covers the `required`
scope.

`required` MUST be a concrete catalog entry; passing a wildcard form
returns `false`. A nil or empty grant list returns `false`. Granted
entries that are not valid grant forms are ignored - they cannot grant
anything, even by accident. Even the full wildcard `*` only covers
scopes actually in the catalog, so a typo or uncatalogued endpoint
requirement is never authorized.

# `grants_all?`

```elixir
@spec grants_all?(t(), [String.t()] | nil, [String.t(), ...]) :: boolean()
```

Returns `true` iff `granted` covers every entry in `required`.

Raises `ArgumentError` on a nil or empty `required` list so a
misconfigured authorization declaration fails loudly instead of
silently authorizing every caller.

# `known?`

```elixir
@spec known?(t(), term()) :: boolean()
```

Returns `true` iff `scope` is a concrete catalog entry (no wildcards).

# `new_catalog`

```elixir
@spec new_catalog([String.t()]) :: t()
```

Build a catalog from the list of concrete scope strings your API
understands. Computes the distinct resources (left-of-dot segments)
once so per-request matching is allocation-light.

# `resources`

```elixir
@spec resources(t()) :: [String.t()]
```

The distinct resources present in the catalog, sorted.

# `unknown`

```elixir
@spec unknown(t(), [String.t()] | nil) :: [String.t()]
```

Returns the subset of `requested` scopes that are NOT valid
customer-facing grant forms. Used at a token endpoint to surface
`invalid_scope` (RFC 6749 §5.2) without leaking which scopes are
catalogued. Rejects the system-only `*` form.

# `valid_grant_form?`

```elixir
@spec valid_grant_form?(t(), term()) :: boolean()
```

Returns `true` iff `scope` is a legal granted form for a
**system-issued** credential: a concrete catalog entry, the full
wildcard `*`, or a resource-level wildcard `<resource>.*` whose
resource appears in the catalog.

Customer-facing surfaces MUST use `customer_grant_form?/2` instead - it
rejects the system-only `*` form.

# `valid_token?`

```elixir
@spec valid_token?(term()) :: boolean()
```

Returns `true` iff `value` is a syntactically-valid RFC 6749
scope-token: a non-empty string of printable ASCII excluding space,
double-quote, and backslash. This is a *wire-format* check independent
of any catalog: it rejects a value like `"documents.read positions.read"`
that, embedded in a space-delimited `scope` claim, would be
indistinguishable from two separate grants.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
