Metadata-Version: 2.5
Name: guest-auth
Version: 0.2.0
Summary: A static-allowlist invite-token gate for pre-production demos and invite-only previews (pure-ASGI middleware for Starlette / FastAPI apps).
Project-URL: Homepage, https://github.com/ecoop/guest-auth
Project-URL: Repository, https://github.com/ecoop/guest-auth
Project-URL: Issues, https://github.com/ecoop/guest-auth/issues
Author: Eric Cooper
License: MIT License
        
        Copyright (c) 2026 Eric Cooper
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: asgi,auth,demo,fastapi,invite,starlette
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: starlette>=0.35
Provides-Extra: dev
Requires-Dist: fastapi>=0.100; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# guest-auth

**Not a replacement for real authentication.** A static-allowlist invite-token gate for pre-production demos and invite-only previews. Pure-ASGI middleware that plugs into any Starlette / FastAPI app in ~5 lines.

Give a tester a link like `https://your-app.example.com/?token=tok_abc123`; the middleware validates the token against an allowlist you own, exchanges it for an `httpOnly` cookie, and attaches an identity (`token` + human-readable `recipient` label) to the request via a `ContextVar` that reaches sync endpoints in the threadpool as well.

The library was extracted from [Pitchcraft](https://github.com/ecoop/pitchcraft) and is consumed there in production; [Rulebook](https://github.com/ecoop/rulebook) and JobScout are scheduled to adopt it.

**Adopting this in a new app?** See [`docs/integration.md`](docs/integration.md) for the DI pattern, the `app_state.py` template, gotchas (init order, pure-ASGI vs `BaseHTTPMiddleware`, ContextVar propagation), and the constructor reference.

---

## What this is NOT

Naming a library `*-auth` invites expectations it doesn't meet. To be explicit:

- **No password handling, no MFA, no OAuth / OIDC, no account lifecycle.** The credential is an opaque token you generate and hand to a tester.
- **No token rotation, expiry, revocation-list, or signed cookies.** The cookie is httpOnly + Secure + SameSite=Lax with a 30-day convenience lifetime; revoking access means removing the token from the allowlist and redeploying.
- **No rate limiting.** Compose one separately (e.g. [`llm-guardrails`](https://github.com/ecoop/llm-guardrails) ships an IP rate limiter).
- **No policy engine.** The identity can carry `role` / `scopes` claims (see below), but guest-auth never interprets or enforces them — no capability vocabulary, no role→permission bundles, no `require_x` decorator. That's authz *over* the claims and it lives in your app.
- **Not audited for adversarial threat models.** This is a gate to keep pre-production URLs off the open web and attribute per-tester activity, not a substitute for real identity infrastructure. If you're gating production PII or payment flows, use something else.

The value the library provides — a well-scoped ASGI middleware that publishes a per-request identity ContextVar that reaches sync endpoints — is genuinely useful and hard to get right (the "pure-ASGI vs `BaseHTTPMiddleware`" trap is subtle). Everything above is deferred, not planned.

---

## Install

```bash
pip install guest-auth
```

Requires Python 3.11+. The only runtime dependency is `starlette`, which any ASGI host already has.

---

## Quick example

```python
from dataclasses import dataclass, field
from fastapi import FastAPI
from guest_auth import InviteAuthMiddleware, get_current_guest


@dataclass
class Settings:
    demo_mode: bool = True
    invite_tokens: dict = field(
        default_factory=lambda: {"tok_abc123": "Jane Tester"}
    )


settings = Settings()
app = FastAPI()


@app.get("/")
def home():
    guest = get_current_guest()
    return {"welcome": guest.recipient if guest else "anonymous"}


app.add_middleware(
    InviteAuthMiddleware,
    config=settings,
    # Optional — pre-rendered HTML for the 401 / welcome page.
    # Omit to use the built-in "This site is currently invite-only." body.
    welcome_html="<h1>Preview build</h1><p>Ask jane@example.com for a link.</p>",
)
```

Now:

- `GET /?token=tok_abc123` → 302 to `/`, sets `guest_session` cookie.
- `GET /` with the cookie → returns `{"welcome": "Jane Tester"}`.
- `GET /` without a cookie → 401 with the welcome page.
- Flip `settings.demo_mode = False` → gate becomes a complete pass-through with no restart.

---

## Core concepts

### `GuestAuthConfig` (Protocol)

The middleware takes a `config` object that satisfies:

```python
class GuestAuthConfig(Protocol):
    demo_mode: bool
    invite_tokens: Mapping[str, str]  # token → recipient label
```

Both attributes are read at request time, so mutating a live `config` instance (a pydantic `BaseSettings`, a dataclass, whatever) takes effect on the next request without rebuilding the middleware.

### `GuestIdentity` + `get_current_guest()`

On a successful cookie match, the middleware sets a request-scoped `ContextVar` with `GuestIdentity(token=..., recipient=..., role=..., scopes=...)`. Anywhere downstream — sync or async, including code paths in Starlette's threadpool — `get_current_guest()` returns it or `None`.

Because the middleware is pure-ASGI (not `BaseHTTPMiddleware`), the `ContextVar` survives into the threadpool that runs `def` (sync) endpoints. See the [integration doc](docs/integration.md#gotchas) for why this matters.

### Claims: `role` and `scopes`

Optionally, the identity carries authorization claims. guest-auth defines their
**structure** and never their **meaning** — any string is a role, any strings are
scopes:

```python
from guest_auth import GuestClaims

def resolve_claims(token: str) -> GuestClaims:
    return GuestClaims(role="level5", scopes=("rules", "faq"))  # your store

app.add_middleware(
    InviteAuthMiddleware, config=settings, claims_resolver=resolve_claims
)
```

The resolver may be sync or async; a sync one runs in Starlette's threadpool, so
a resolver that reads a bucket doesn't stall the event loop. Caching is the
resolver's job.

Two contracts to know: resolution failure is **soft** (log + empty claims, never
a 401), and consequently `role=None` means **unresolved, not unprivileged** — map
it to your own floor explicitly. Full detail in the [integration doc](docs/integration.md#claims-role--scopes).

### `PathScopedContextVarMiddleware` (bonus)

An adjacent generic that ships in the same package: match a regex against `scope["path"]`, publish an extracted value on a caller-supplied `ContextVar` for the duration of the request. Same pure-ASGI rationale as the auth middleware. Use for `/api/things/{id}/…` style path-scoped ContextVars (session IDs, tenant IDs, whatever).

---

## Minting invite tokens

Installing the package also installs `guest-auth-tokens`, a small offline helper
for the chore every app behind this middleware repeats: turn a list of names into
tokens and links you can send people. Stdlib only — it adds no dependency.

Write one recipient per line (an inline `#` adds a private note that stays local):

```
Alice
Mike        # met at worlds
Bob Smith
```

Then:

```bash
guest-auth-tokens gen --names secrets/guests.txt --out secrets/tokens.json --links secrets/links.md --base-url https://your-app.example.com
```

That writes a `{token: label}` JSON — feed it to `config.invite_tokens` however
your app prefers — and a markdown file of shareable invite links.

Re-running is **merge-preserving**: a name that already has a token keeps it, so
links you've already sent never break. A name you *remove* is reported rather
than revoked — its token stays valid until you delete it by hand, because
revoking someone's access shouldn't be a side effect of regenerating a file.

Both output files contain working credentials. The tool checks whether they're
covered by a `.gitignore` and warns loudly if not.

Note what this deliberately isn't: there's no live allowlist management, no
storage backend, and no cloud dependency. Where your app keeps its allowlist and
how it refreshes it stays yours.

## Development

```bash
git clone https://github.com/ecoop/guest-auth
cd guest-auth
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check src tests
```

CI runs on Python 3.11 and 3.12 via [GitHub Actions](.github/workflows/ci.yml).

## Versioning

Currently `v0.2.0`. Semver from `v1.0.0` onward; anything before is "shipped but pre-stable API — expect breaking changes."

## Contributing

Issues and pull requests welcome. For substantive changes, open an issue first — this library has a deliberately small surface and staying small is a feature.

## License

MIT. See [LICENSE](LICENSE).

---

_Last updated: 2026-09-05_
