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:
- No principal → deny
principal_not_found - Suspended principal → deny
principal_suspended - Scope in any role → allow
granted_by_role - Explicit resource grant → allow
granted_by_explicit_grant - 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8a53c377ada61b0bf29f28eae12e46c3500e46baa9030a633f21ce33a997938
|
|
| MD5 |
b31927ea16ddd50fb3e5ba77d2a7a840
|
|
| BLAKE2b-256 |
ab34597e61003debc70ebc2b9f9f311e539873d0451bc1019dc150042d85056e
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f567f5551bb83c935d9d86e0b866df3487ed7269a412f8ff7274a008b4b1171
|
|
| MD5 |
1527ec3725f3b064f562f6239d3b1ac6
|
|
| BLAKE2b-256 |
da2b4c708e61502250c112cb7857a6c432830a03e8b7be4e3587527cfe345bbe
|