Skip to main content

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.

Installation

Install name: miniapppolis-identity (PyPI)  ·  Import namespace: identity

identity was unavailable on PyPI — an unrelated Microsoft auth library holds it — so the distribution is prefixed while the import namespace stays as every enforcement point already writes it.

[project]
dependencies = [
  "miniapppolis-identity>=2.0,<3",
]

A calling principal needs only the contract and the verifier. An enforcement point that resolves principals against a store adds the [store] extra, which pulls in SQLAlchemy:

dependencies = [
  "miniapppolis-identity[store]>=2.0,<3",
]
from identity import ChainVerifier, Principal, VerifiedSubject

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.

Errors

Only verify raises, and only subclasses of IdentityError. An adapter maps each to a response:

Error Means Response
CredentialInvalid The credential failed verification 401
IssuerNotTrusted Correctly shaped, from an issuer this enforcement point was never told about 401
IssuerUnavailable A trusted issuer's key set could not be fetched; says nothing about the credential 503

Each carries a reason for the enforcement point's logs. The reason is not returned to the caller.

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.

Its tables are prefixed identity_ in the default schema (not a Postgres schema of their own — see ADR-001) so it can be added to an existing database without colliding with application tables.

An issuer row with enabled false withdraws trust from the whole tenant: resolve returns None for its principals, so they are denied principal_not_found whatever roles they hold.

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 (verify via identity.apikey, identity.clerk and identity.chain; resolve and emit_audit via the [store] extra; authorize via identity.policy) and the authorize fixture suite are real; the TypeScript binding and the verify/resolve/audit fixture directories are not filled in yet.

Metadata

Release files for miniapppolis-identity 2.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for miniapppolis-identity 2.1.8
File Size Uploaded Provenance
miniapppolis_identity-2.1.8.tar.gz 9.8 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for miniapppolis-identity 2.1.8
File Interpreter ABI Platform
miniapppolis_identity-2.1.8-py3-none-any.whl Python 3 none any Details

Total release size: 9.9 MB

Release files / miniapppolis_identity-2.1.8.tar.gz

Download URL miniapppolis_identity-2.1.8.tar.gz
Size 9.8 MB
Tags Source
SHA-256 checksum
How to use checksums
2d3909dd8969b6931cbae7a60b9f50ea84bf69757106c86fd4176c4ca8a769cc
BLAKE2b-256 checksum
How to use checksums
19ddc8d8ce89d90d0dd3b446b61ffde576120b310d1b6a20f675695db1ae25a0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / miniapppolis_identity-2.1.8-py3-none-any.whl

Download URL miniapppolis_identity-2.1.8-py3-none-any.whl
Size 28.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fc9577c21570e5755498e4c0bc62d58c972bf75c5e63fea531f2a352b62490a5
BLAKE2b-256 checksum
How to use checksums
6ebe0121950688e1d910fb440775195693e2c139540b081d899db7f4e6a6df97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for miniapppolis-identity 2.1.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page