Metadata-Version: 2.4
Name: gated-access
Version: 1.0.1
Summary: Pluggable action-gated access for any stack: verify an action, email a one-time code, issue a signed JWT.
Project-URL: Homepage, https://github.com/NI3singh/gated-access
Project-URL: Issues, https://github.com/NI3singh/gated-access/issues
Author: Gated Access contributors
License-Expression: MIT
License-File: LICENSE
Keywords: access,auth,fastapi,gate,jwt,oauth,one-time-code
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Requires-Python: >=3.10
Requires-Dist: fastapi>=0.110
Requires-Dist: httpx>=0.27
Requires-Dist: itsdangerous>=2.1
Requires-Dist: jinja2>=3.1
Requires-Dist: pydantic>=2.6
Requires-Dist: pyjwt>=2.7
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: uvicorn[standard]>=0.29
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

<div align="center">

<img src="https://raw.githubusercontent.com/NI3singh/gated-access/main/assets/logo.svg" width="92" alt="Gated Access logo"/>

# Gated Access

**Pluggable action-gated access for any stack.**

Require a verifiable action — star a repo, follow a user, or your own custom check —
then email a one-time code and issue a signed access token.

<sub>FastAPI · zero-setup local demo · swap an adapter, not your project</sub>

</div>

---

## The idea

Plenty of products want to gate access behind an action: *"do this, then you're in."*
The fragile way is to hard-wire that one action into your app. When the platform or the
rules change, you rewrite everything.

Gated Access treats the **action as a swappable adapter**. The engine only knows the flow:

```
  ┌──────────┐    ┌───────────┐    ┌──────────────┐    ┌─────────────┐
  │ Identify │ -> │  Action   │ -> │  One-time    │ -> │   Access    │
  │ (OAuth)  │    │ (verify)  │    │  code (mail) │    │   (JWT)     │
  └──────────┘    └───────────┘    └──────────────┘    └─────────────┘
```

The user signs in, Gated Access checks the action against their *real* account, emails a
single-use code, and exchanges that code for a JWT your app can verify with a shared
secret. Identity, verification, email, and storage are each an adapter — change one and
nothing else moves.

> **"Any stack"** means the engine is one small headless HTTP API plus a drop-in frontend.
> Your app can be anything; it just talks to four endpoints (or verifies the JWT directly).

---

## Quickstart — the local demo

No GitHub app, no email provider, no credit card. It runs entirely on your machine.

```bash
pip install gated-access
gated-access run        # open http://localhost:8000
```

Click through all four steps. In demo mode Gated Access:

- **simulates** the GitHub sign-in (no real OAuth needed),
- lets you **toggle** "I did the action" to flip what the verifier sees,
- prints the email to the console **and** shows it in an on-screen *demo inbox* so you can
  read the code without sending real mail.

---

## CLI

| Command | What it does |
|---|---|
| `gated-access run [--host H] [--port P] [--config PATH]` | Start the server. Host/port default to the config values; `--config` points at an alternate `gated-access.yaml`. |
| `gated-access secrets` | Print ready-to-paste `GATED_ACCESS_JWT_SECRET=` / `GATED_ACCESS_SESSION_SECRET=` lines (cryptographically random). |
| `gated-access init [--force]` | Interactively write a commented `gated-access.yaml` in the current directory. Pressing Enter at every prompt yields the demo config; non-demo choices print the env vars you still need to set. |
| `gated-access version` | Print the installed version. |

`python -m gated_access` also starts the server, and `uvicorn gated_access.main:app` remains the
plain ASGI entrypoint for deployment.

---

## How it fits together

Four adapter families, each behind a tiny interface in `gated_access/adapters/base.py`:

| Adapter | Demo default | Production options |
|---|---|---|
| **Identity** — who is this user? | `demo` (fabricated user) | `github` (OAuth web flow) |
| **Verification** — did they do it? | `manual` (simulated) | `github_star`, `github_follow` |
| **Email** — deliver the code | `console` (+ demo inbox) | `resend`, `smtp` |
| **Storage** — sessions & codes | `memory` | `sqlite` |

Adding a new check (a webhook, an on-chain event, "is a paying customer") is just a new
`VerificationAdapter` subclass — the routes, codes, tokens, and UI stay exactly the same.

---

## Configuration

Everything is driven by `gated-access.yaml`, with **environment variables overriding the file**
so secrets never live in config. Common keys:

| Key | Meaning |
|---|---|
| `identity.provider` | `demo` or `github` |
| `verify.type` | `manual` · `github_star` (needs `verify.repo`) · `github_follow` (needs `verify.username`) |
| `email.provider` | `console` · `resend` · `smtp` |
| `storage.backend` | `memory` · `sqlite` |
| `code.length` / `code.ttl_seconds` / `code.single_use` | one-time code policy |
| `access_ttl_seconds` | JWT lifetime |
| `demo_mode` | enables the simulated login + `/dev/*` endpoints — **set `false` in production** |
| `branding.*` | product name, copy, and accent colour shown in the UI |

See `.env.example` for the full list of secret/deploy variables.

---

## HTTP API

The frontend is optional — these are all you need to integrate from any language.

| Method & path | Purpose |
|---|---|
| `GET /auth/login` | Begin sign-in (redirects to the identity provider) |
| `GET /auth/callback` | Finish sign-in; sets a signed, http-only session cookie |
| `GET /api/session` | Current status: authenticated? action done? verified? |
| `POST /api/verify` | Run the verification adapter; on success, email a one-time code |
| `POST /api/redeem` | Body `{"code": "..."}` → `{"access": true, "token": "<JWT>"}` |
| `GET /api/me` | Example protected resource — requires `Authorization: Bearer <JWT>` |
| `GET /healthz` | Liveness check |
| `POST /dev/perform-action`, `GET /dev/inbox` | Demo-only helpers (disabled when `demo_mode: false`) |

Your backend doesn't even have to call `/api/me` — verify the JWT yourself with the shared
`jwt_secret` (HS256, issuer `gated-access`).

---

## Going to production

1. **Generate secrets** and set them in the environment:
   ```bash
   gated-access secrets    # prints GATED_ACCESS_JWT_SECRET=... and GATED_ACCESS_SESSION_SECRET=...
   ```
   Gated Access refuses to start with `demo_mode: false` while the dev placeholder secrets
   are still in place.
2. **Create a GitHub OAuth app** at <https://github.com/settings/developers>. Set the
   callback URL to `https://your-domain/auth/callback` and export `GITHUB_CLIENT_ID`,
   `GITHUB_CLIENT_SECRET`, `GITHUB_REDIRECT_URI`.
3. **Switch the adapters** in `gated-access.yaml`:
   ```yaml
   demo_mode: false
   identity: { provider: github }
   verify:   { type: github_star, repo: "you/your-repo" }
   email:    { provider: resend }       # set RESEND_API_KEY in env
   storage:  { backend: sqlite }
   ```
4. **Serve it** behind HTTPS (the session cookie is marked `Secure` automatically when
   `base_url` is `https`):
   ```bash
   uvicorn gated_access.main:app --host 0.0.0.0 --port 8000
   ```

Because it's a plain ASGI app with no required cloud services, it deploys anywhere that
runs Python — a container, Render, Railway, Fly, a VM, etc.

---

## Security notes

- **One-time codes** drawn from an unambiguous alphabet (no `0/O/1/I/L`), single-use and
  time-limited, and **bound to the session that earned them** — a code minted for one user
  is rejected for another.
- **JWT access tokens** (HS256) with issuer and expiry; verify with your shared secret.
- **OAuth `state`** is single-use and short-lived (CSRF protection); the session cookie is
  signed (`itsdangerous`), http-only, and `Secure` over HTTPS.
- The **provider access token is held only until verification succeeds**, then dropped.
- **`/api/redeem` is rate-limited** per session + IP to blunt code-guessing.

---

## A note on GitHub's rules

GitHub's Acceptable Use Policies prohibit **incentivizing stars** ("inauthentic
interactions" and giving stars in exchange for any benefit). So a real "star *my* repo to
unlock" gate aimed at the public would violate those terms.

This project is built as a **portfolio / reference demo of the pattern**, not a growth
hack. Keep it on the right side of the line by pointing the verifier at your *own* repo for
testing, or by using a check that isn't incentivized starring — `github_follow`, a webhook,
"is a customer", or any custom `VerificationAdapter` you write. The whole point of the
adapter design is that the gate survives exactly this kind of rule change.

---

## Project layout

```
gated_access/
├── gated_access/
│   ├── adapters/        identity · verification · email · storage (+ base interfaces)
│   ├── service.py       the engine: begin_auth → verify → redeem
│   ├── factory.py       builds adapters from config and wires the FastAPI routes
│   ├── security.py      signed cookies, OAuth state, rate limiter
│   ├── tokens.py        one-time codes + JWT
│   ├── config.py        typed config (YAML + env)
│   ├── cli.py           the `gated-access` command (run · secrets · init · version)
│   ├── _version.py      single source of truth for the version
│   ├── templates/       demo UI
│   └── static/          styles, client logic, logo
├── tests/               token unit tests + full end-to-end flow
├── pyproject.toml       packaging (hatchling) — the source of truth for dependencies
├── gated-access.yaml         configuration (demo defaults)
├── .env.example         production secrets template
└── requirements*.txt    convenience for clone-based development
```

---

## Development

```bash
git clone https://github.com/NI3singh/gated-access && cd gated-access
pip install -e .[dev]
pytest -q
gated-access run            # or: python -m gated_access
```

---

## Publishing (maintainers)

```bash
python -m build                                # dist/gated-access-X.Y.Z.tar.gz + .whl
twine check dist/*
twine upload --repository testpypi dist/*      # dress rehearsal
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple gated-access
twine upload dist/*                            # real release (needs a PyPI API token)
```

The version lives in `gated_access/_version.py` only — bump it there.

---

## License

MIT — see [LICENSE](https://github.com/NI3singh/gated-access/blob/main/LICENSE).
