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.

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

miniapppolis_identity-2.0.1.tar.gz (8.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

miniapppolis_identity-2.0.1-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

File details

Details for the file miniapppolis_identity-2.0.1.tar.gz.

File metadata

  • Download URL: miniapppolis_identity-2.0.1.tar.gz
  • Upload date:
  • Size: 8.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for miniapppolis_identity-2.0.1.tar.gz
Algorithm Hash digest
SHA256 b8a53c377ada61b0bf29f28eae12e46c3500e46baa9030a633f21ce33a997938
MD5 b31927ea16ddd50fb3e5ba77d2a7a840
BLAKE2b-256 ab34597e61003debc70ebc2b9f9f311e539873d0451bc1019dc150042d85056e

See more details on using hashes here.

File details

Details for the file miniapppolis_identity-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: miniapppolis_identity-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 25.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for miniapppolis_identity-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3f567f5551bb83c935d9d86e0b866df3487ed7269a412f8ff7274a008b4b1171
MD5 1527ec3725f3b064f562f6239d3b1ac6
BLAKE2b-256 da2b4c708e61502250c112cb7857a6c432830a03e8b7be4e3587527cfe345bbe

See more details on using hashes here.

Release history Release notifications | RSS feed

2.0.2

2 files

This release

2.0.1 This release

2 files

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