Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/publish-rc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,26 @@ jobs:

- uses: astral-sh/setup-uv@v7

- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: ui/pnpm-lock.yaml

- name: Enable pnpm
run: corepack enable

- name: Compute RC version
run: |
CURRENT=$(grep '^version = ' pyproject.toml | sed 's/version = "//;s/"//')
BASE=$(echo "$CURRENT" | sed 's/rc[0-9]*$//')
RC_VERSION="${BASE}rc${{ github.run_number }}"
sed -i "s/^version = \".*\"/version = \"${RC_VERSION}\"/" pyproject.toml

- name: Build static UI
run: ./scripts/build-ui.sh

- name: Build
run: uv build

Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ jobs:

- uses: astral-sh/setup-uv@v7

- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: ui/pnpm-lock.yaml

- name: Enable pnpm
run: corepack enable

- name: Build static UI
run: ./scripts/build-ui.sh

- name: Build distributions
run: uv build

Expand Down
40 changes: 28 additions & 12 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,45 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6


- uses: astral-sh/setup-uv@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: "pip"


- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: ui/pnpm-lock.yaml

- name: Enable pnpm
run: corepack enable

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
run: uv pip install --system -e ".[dev]"

- name: Build static UI
run: ./scripts/build-ui.sh

- name: Lint static UI
run: uv run pnpm --dir ui lint

- name: Lint with ruff
run: |
ruff check src/ tests/
ruff format --check src/ tests/
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/

- name: Type check with ty
run: ty check src/
run: uv run ty check src/

- name: Run tests with coverage
run: pytest --cov=authsome --cov-report=xml --cov-report=term -p no:xdist
run: uv run pytest --cov=authsome --cov-report=xml --cov-report=term -p no:xdist

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v6
with:
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ downloads/
eggs/
.eggs/
lib/
!ui/src/lib/
!ui/src/lib/**
lib64/
parts/
sdist/
Expand Down Expand Up @@ -204,6 +206,10 @@ cython_debug/
# Ruff stuff:
.ruff_cache/

# Generated static dashboard bundle
src/authsome/ui/web/*
!src/authsome/ui/web/.gitkeep

# PyPI configuration file
.pypirc

Expand Down Expand Up @@ -252,4 +258,4 @@ __marimo__/
docs/superpowers/

# Authsome skill (generated at eval time)
.claude/skills/authsome/
.claude/skills/authsome/
19 changes: 9 additions & 10 deletions docs/site/reference/daemon-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,19 +141,18 @@ These routes back the local HTTP proxy started by `authsome run`. The proxy neve

## Dashboard UI

These routes serve HTML. The dashboard is served at the root prefix.
The Next.js dashboard is served as a static export at the daemon root. Browser-only form routes remain for account sessions and provider login starts.

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/` | Dashboard overview. |
| `GET` | `/applications` | Applications / Providers view. |
| `GET` | `/manage/connections` | All connections, by provider. |
| `GET` | `/identity` | Identity details. |
| `GET` | `/audit` | Admin audit dashboard. |
| `GET` | `/apps/{provider_name}` | Detail pane for one provider. |
| `GET` | `/apps/{provider_name}/connections/{connection_name}` | Connection detail pane. |
| `POST` | `/apps/{provider_name}/{connection_name}/disconnect` | Log out from the dashboard UI. |
| `POST` | `/apps/{provider_name}/connect` | Start a flow from the dashboard UI. |
| `GET` | `/` | Static dashboard shell. |
| `POST` | `/session` | Return the dashboard URL for a PoP-authenticated local client. |
| `POST` | `/auth/login` | Create a browser dashboard session. |
| `POST` | `/auth/register` | Register an account and create a browser dashboard session. |
| `POST` | `/logout` | Clear the browser dashboard session. |
| `GET` | `/claim/{token}` | Render the account claim confirmation page. |
| `POST` | `/claim/{token}/confirm` | Attach a local identity to the signed-in account. |
| `POST` | `/auth/providers/{provider_name}/connect` | Start a provider login flow from the dashboard. |


## Auth
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ dependencies = [
"fastapi>=0.115",
"uvicorn>=0.30",
"python-multipart>=0.0.27",
"jinja2>=3.1",
"py-key-value-aio[disk]",
"aiosqlite>=0.20",
"asyncpg>=0.30",
Expand Down Expand Up @@ -65,6 +64,9 @@ authsome = "authsome.cli.main:cli"
[tool.hatch.build.targets.wheel]
packages = ["src/authsome"]

[tool.hatch.build.targets.sdist.force-include]
"src/authsome/ui/web" = "src/authsome/ui/web"

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
Expand Down
15 changes: 15 additions & 0 deletions scripts/build-ui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UI_DIR="${ROOT_DIR}/ui"
TARGET_DIR="${ROOT_DIR}/src/authsome/ui/web"
UV_BIN="${UV:-uv}"

"${UV_BIN}" run pnpm --dir "${UI_DIR}" install --frozen-lockfile
"${UV_BIN}" run pnpm --dir "${UI_DIR}" build

rm -rf "${TARGET_DIR}"
mkdir -p "${TARGET_DIR}"
cp -R "${UI_DIR}/out/." "${TARGET_DIR}/"
printf '%s\n' "Generated static UI assets are copied here by scripts/build-ui.sh." > "${TARGET_DIR}/.gitkeep"
5 changes: 2 additions & 3 deletions src/authsome/proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,15 +520,14 @@ def _deny_body(reason: str, match: RouteMatch | None) -> str:
and a dashboard URL so the agent (or human) can recover; other
reasons fall back to a generic message.

The dashboard URL uses ``DEFAULT_SERVER_BASE_URL``. It still requires an active dashboard session
to land on the connect screen directly.
The dashboard URL uses ``DEFAULT_SERVER_BASE_URL`` and still requires an active browser session.
"""
if reason == "no_credentials" and match is not None:
provider = match.provider
return (
f"Forbidden: provider '{provider}' is configured but has no "
f"active connection. Run `authsome login {provider}` to connect, "
f"or visit {DEFAULT_SERVER_BASE_URL}/apps/{provider}."
f"or open the dashboard at {DEFAULT_SERVER_BASE_URL}/."
)
return "Forbidden by Authsome proxy policy"

Expand Down
4 changes: 2 additions & 2 deletions src/authsome/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def ui_auth_required_handler(request: Request, exc: UiAuthRequiredError):
app.include_router(proxy_router)
app.include_router(ui_router)

static_dir = files("authsome.ui").joinpath("static")
app.mount("/static", StaticFiles(directory=str(static_dir)), name="ui-static")
ui_dir = files("authsome.ui").joinpath("web")
app.mount("/", StaticFiles(directory=str(ui_dir), html=True, check_dir=False), name="ui")

return app
23 changes: 23 additions & 0 deletions src/authsome/server/routes/_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ async def get_admin_auth_service(
return auth


async def get_daemon_or_browser_auth_service(request: Request) -> CredentialService:
"""Resolve auth from PoP headers or an existing browser dashboard session."""
if request.headers.get("Authorization"):
ownership = await verify_pop_caller(request)
return _build_service(request, ownership)

await resolve_ui_request_identity(request)
auth = await get_auth_service(
request,
principal_id=getattr(request.state, "ui_principal_id", None),
)
if auth is None:
raise HTTPException(status_code=401, detail="Missing or invalid browser session")
return auth


async def get_admin_daemon_or_browser_auth_service(request: Request) -> CredentialService:
auth = await get_daemon_or_browser_auth_service(request)
if auth.principal_role != PrincipalRole.ADMIN:
raise HTTPException(status_code=403, detail="Admin role required")
return auth


def get_vault_registry(request: Request) -> VaultRegistry:
return request.app.state.store.vaults

Expand Down
7 changes: 5 additions & 2 deletions src/authsome/server/routes/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

from authsome import audit
from authsome.server.credential_service import CredentialService
from authsome.server.routes._deps import get_admin_auth_service, get_protected_auth_service
from authsome.server.routes._deps import (
get_admin_daemon_or_browser_auth_service,
get_protected_auth_service,
)

router = APIRouter(prefix="/audit", tags=["audit"])

Expand All @@ -17,7 +20,7 @@
async def list_audit_events(
request: Request,
limit: int = 50,
auth: CredentialService = Depends(get_admin_auth_service),
auth: CredentialService = Depends(get_admin_daemon_or_browser_auth_service),
) -> dict[str, Any]:
_ = auth
return {"entries": await request.app.state.audit_log.list_events(limit=limit)}
Expand Down
9 changes: 7 additions & 2 deletions src/authsome/server/routes/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
from authsome.auth.models.enums import ExportFormat
from authsome.server.analytics import capture_event
from authsome.server.credential_service import CredentialService
from authsome.server.routes._deps import get_admin_auth_service, get_protected_auth_service, get_vault_registry
from authsome.server.routes._deps import (
get_admin_auth_service,
get_daemon_or_browser_auth_service,
get_protected_auth_service,
get_vault_registry,
)
from authsome.server.store.repositories import VaultRegistry

router = APIRouter(tags=["connections"])


@router.get("/connections")
async def list_connections(auth: CredentialService = Depends(get_protected_auth_service)):
async def list_connections(auth: CredentialService = Depends(get_daemon_or_browser_auth_service)):
by_source = await auth.list_providers_by_source()
return {
"connections": await auth.list_connections(),
Expand Down
16 changes: 11 additions & 5 deletions src/authsome/server/routes/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

from authsome import __version__
from authsome.server.credential_service import CredentialService
from authsome.server.routes._deps import get_protected_auth_service, get_server_base_url
from authsome.server.routes._deps import (
get_daemon_or_browser_auth_service,
get_protected_auth_service,
get_server_base_url,
)
from authsome.server.schemas import HealthResponse, ReadyResponse
from authsome.utils import connection_is_active

Expand Down Expand Up @@ -115,18 +119,20 @@ async def ready(
@router.get("/whoami")
async def whoami(
request: Request,
auth: CredentialService = Depends(get_protected_auth_service),
auth: CredentialService = Depends(get_daemon_or_browser_auth_service),
server_base_url: str = Depends(get_server_base_url),
) -> dict[str, str]:
effective_source, backend_description = _describe_vault_encryption(auth.vault)
identity = auth.require_identity()
identity = auth.identity or getattr(request.state, "ui_identity", "") or ""
return {
"version": __version__,
"home": str(request.app.state.store.home),
"identity": identity,
"active_identity": identity,
"principal_id": getattr(request.state, "principal_id", ""),
"vault_id": getattr(request.state, "vault_id", ""),
"principal_id": getattr(request.state, "principal_id", None) or auth.principal_id or "",
"vault_id": getattr(request.state, "vault_id", None) or auth.vault_id or "",
"principal_role": auth.principal_role.value,
"account_email": getattr(request.state, "ui_email", ""),
"did": getattr(request.state, "did", ""),
"registration_status": getattr(request.state, "registration_status", "registered"),
"daemon_url": server_base_url,
Expand Down
Loading
Loading