Metadata-Version: 2.5
Name: miniapppolis-identity
Version: 2.0.1
Summary: Cross-ecosystem identity contract: principals, roles, scopes, and the four-function binding every enforcement point implements.
Project-URL: Homepage, https://github.com/mini-app-polis/identity
Project-URL: Repository, https://github.com/mini-app-polis/identity
Project-URL: Changelog, https://github.com/mini-app-polis/identity/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/mini-app-polis/identity/issues
Author: Kaiano Levine
License: MIT
Requires-Python: >=3.11
Requires-Dist: httpx
Requires-Dist: pyjwt[crypto]>=2.13.0
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.20.0; extra == 'dev'
Requires-Dist: jsonschema>=4.23; extra == 'dev'
Requires-Dist: mypy>=1.11.2; extra == 'dev'
Requires-Dist: pre-commit>=3.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest>=8.3.2; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
Provides-Extra: store
Requires-Dist: sqlalchemy>=2.0; extra == 'store'
Description-Content-Type: text/markdown

# identity

The cross-ecosystem identity contract for MiniAppPolis: **principals and grants**,
not just authentication.

One specification with N conformant enforcement points — explicitly **not** a
central auth service on the request path. A central service would put a network
hop and a single point of failure in front of every request in the fleet. This
repo instead defines what a correct enforcement point does, ships a binding that
does it, and provides the fixture suite that proves any other binding agrees.

The design ships here, with the implementation. `ecosystem-standards` holds
rules, not designs, and no service depends on it.

## Scope

| In | Out |
|---|---|
| Principals (human and machine), roles, scopes, explicit grants | Session management, login UI, token minting |
| The four-function binding contract | A runtime service other services call |
| The principal store schema | A shared runtime store |
| The cross-language fixture suite | Per-service business rules |

## Two ways to authenticate, one way to authorize

Machines and humans prove identity differently, and pretending otherwise
produces a worse version of both.

**Machines hold a named API key.** The key *is* the name. Verification is a
constant-time comparison against keys held in configuration — no token to
mint, no issuer to call on the request path, and nothing for the caller to
assert. Impersonating a machine requires possessing its key rather than
claiming its name.

**Humans hold issuer sessions.** People need expiry, revocation and a login
flow, which a long-lived string does not have.

Key material never reaches the principal store. Keys live in deployment
configuration; the store holds names, status and roles. A database dump
exposes no credentials, and rotating a key is a configuration change rather
than a migration.

Both paths converge immediately after verification: one principal table, one
role model, one authorization decision, one audit trail. `authorize` does not
branch on how the caller was authenticated, and could not — by then the
distinction is gone.

## The four functions

Every enforcement point implements the same four, in the same order, once per request:

```
verify(credential)          -> VerifiedSubject     raises on failure
resolve(subject)            -> Principal | None    None is a valid answer
authorize(principal, scope) -> Decision            never raises
emit_audit(event)           -> None                never raises into the request
```

Four rather than one, because each step fails differently and each failure needs
a different response. A collapsed `is_allowed(token, scope) -> bool` cannot tell
"your token is forged" from "you are suspended" from "you are exactly who you say
and simply lack this scope" — and cannot record any of them.

`verify` proves who the *issuer* says you are. `resolve` answers what *this
ecosystem* knows about you. Keeping them separate is what stops `sub` being used
as a primary key.

## Principals

Humans and machines are the same shape and resolve through the same decision.
`kind` exists for audit and policy, never as a proxy for privilege — privilege
comes from roles, for both.

A principal's identity is `(issuer, subject)`, and its identifier is a UUID this
store owns. Two Clerk tenants can legitimately mint the same `sub`; multi-issuer
is a design property, not an edge case. The two tenants stay separate: different
products, different audiences.

## Roles and scopes

Roles are named bundles of scopes. Roles are the only thing granted to a
principal; scopes are the only thing checked at an enforcement point.

Scopes are exactly three dot-separated segments — `<domain>.<resource>.<action>`,
e.g. `wcs.notes.read`. Three segments is a deliberate constraint: it keeps scopes
greppable and makes wildcard expansion unnecessary.

Decision precedence is fixed and short:

1. No principal → deny `principal_not_found`
2. Suspended principal → deny `principal_suspended`
3. Scope in any role → allow `granted_by_role`
4. Explicit resource grant → allow `granted_by_explicit_grant`
5. Otherwise → deny `no_matching_scope`

Suspension is checked before roles on purpose. Reversing 2 and 3 would make
suspension advisory.

## The principal store

One schema (`sql/principal-store.sql`), one instance per ecosystem database —
not shared at runtime. The cogs + `api-kaianolevine-com` database gets one;
`deejaytools-com` gets its own. They share the schema, never the rows.

It installs into its own `identity` Postgres schema so it can be added to an
existing database without colliding with application tables.

## Conformance

`schema/` is the neutral source of truth. `fixtures/` is what keeps every
binding honest — the same inputs, the same expected decisions, in every
language. `authorize` is the part that can be pinned exactly, because it is the
only one of the four that is pure: no network, no database, no sink.

A binding is conformant when it passes the fixture suite. Nothing else counts.

## Status

Early. The Python binding and the `authorize` fixture suite are real; `verify`
and `resolve` reference implementations, the TypeScript binding, and the
`verify`/`resolve`/`audit` fixture directories are not filled in yet.
