SPEC.md on GitHubopenid-configurationprotext.jsonjwks.json

PROTEXT/0.1 — Sign in with context

Status: Draft 0.1 · September 2026 · MIT · github.com/FlicLabs/protext

Protext is an open protocol for signing into an application with context the person owns. It is a profile of OpenID Connect: an ordinary OIDC relying party can sign a person in with Protext today and receive a name and email; a Protext-aware relying party additionally receives a context bundle — typed items with provenance — limited to exactly the items the person ticked.

The reference provider runs at https://protext.org. Anyone may run another; §8 describes how relying parties find them.

1. Goals

  1. The person owns the data. The vault is theirs; export is part of the protocol (§6.4). A provider that cannot be left is not a Protext provider.
  2. Consent is item-level. Applications ask for categories. People decide sentence by sentence. Nothing is shared by default, by inference, or by a setting nobody touched.
  3. Provenance travels; transcripts do not. Every item says where it came from and, where possible, where a verifier can look. The underlying conversation, meeting, page or chat is never part of the bundle.
  4. The data works for the person. A grant is live: an application that was allowed an item may fetch its current value later, until the person revokes the grant. Update once, and every app you allowed follows.
  5. Standards first. OpenID Connect Core, OAuth 2.1 (PKCE mandatory for public clients), RFC 7591 dynamic registration, RFC 7009 revocation, RFC 9207 issuer identification, JWKS. No token, no chain, no new cryptography.
  6. Personhood is a slot, not a gate. The bundle reserves a place for proof-of-personhood credentials issued by third parties (§7). A provider must not require one to sign in.

2. Terms

Term Meaning
Person The human whose context it is. OIDC's end-user.
Provider A server that holds a person's vault and speaks this protocol. OIDC's OpenID Provider.
Relying party (RP) An application that signs people in with Protext. OIDC's client.
Vault Everything the provider holds for one person: items, grants, access log.
Item One typed fact with a source (§3).
Category The first segment of a kind; the unit of scope (§4).
Bundle The set of items released to one RP under one grant, plus envelope (§5).
Grant The person's standing decision about what one RP may see (§6).

3. Items

3.1 Shape

{
  "id": "it_8f1c…",
  "kind": "work.looking-for",
  "value": "a CEO to take SyncedIn and run it",
  "label": "optional human label",
  "source": {
    "provider": "teammeet.io",
    "url": "https://teammeet.io/jack",
    "imported_at": "2026-09-02T09:41:00Z",
    "evidence": "published profile item, attributed to the speaker's own microphone"
  },
  "updated_at": "2026-09-02T09:41:00Z"
}
Field Required Meaning
id yes Opaque, stable per item, unique within the provider.
kind yes <category>.<name>, lower-case, [a-z]+\.[a-z][a-z0-9-]*.
value yes A string, or an object for structured kinds (§3.3).
label no A human label the person or importer attached ("Twitter", "Website").
source.provider yes A hostname (teammeet.io, america.co) or one of the reserved names manual, chatgpt, claude, gemini, ai-export.
source.url no Where a verifier can look. Must be public; must not reveal more than the item itself.
source.imported_at yes When the provider took the item in.
source.evidence no A one-line description of why the source vouches for the item. Never the private material itself.
updated_at yes Last change to value, label or source.

3.2 Categories and well-known kinds

Categories are fixed by this version. Kinds inside a category are open: any kind matching the grammar is valid, and RPs must ignore kinds they do not know. The following kinds have a fixed meaning and, where noted, map onto a standard OIDC claim so that plain OIDC clients get them for free.

Category (scope) Kind OIDC claim Multi
identity (context:identity) identity.name name
identity.handle preferred_username
identity.avatar picture
identity.headline
identity.bio
identity.location
identity.birthdate birthdate
contact (context:contact) contact.email email
contact.phone phone_number
link (context:link) link.website website
link.social yes
link.booking
work (context:work) work.building yes
work.problem yes
work.looking-for yes
work.can-help-with yes
work.role
work.company
interest (context:interest) interest.topic yes
memory (context:memory) memory.note yes
pref (context:pref) pref.language
pref.timezone zoneinfo
pref.tone

The four work.* kinds are borrowed from TeamMeet's profile model, where each is a claim the person made on their own microphone; they are the kinds that make one stranger reach out to another.

3.3 Structured values

link.social and any link.* kind may carry { "url": "https://…", "label": "Twitter" }. Other kinds carry strings in 0.1. Providers must not put nested private material (transcripts, chat logs, files) into value.

3.4 Visibility

Each item is private (default) or public. Public items appear on the person's provider page (https://protext.org/<handle>). Visibility is independent of grants: an RP sees an item only through a grant, whether or not it is public.

4. Scopes

Scope Unlocks
openid Required. sub, ID token.
profile Alias for context:identity + context:link (OIDC compatibility).
email Alias for context:contact.
offline_access A refresh token, so the grant can be re-read later without the person present.
context:<category> Items in that category.
context:* Every category. RPs should not ask for this without a reason they can state on their consent screen.

Scopes limit what may be shared. The grant (§6) records what is shared. An RP receives the intersection.

5. The bundle

{
  "protext": "0.1",
  "sub": "u_…",
  "handle": "jack",
  "issued_at": "2026-09-02T09:45:12Z",
  "items": [ … ],
  "personhood": [ ]
}
Field Meaning
protext Spec version.
sub The OIDC subject — stable per person per provider.
handle The person's handle at this provider. https://<provider>/<handle> is their public page.
issued_at When this bundle was assembled.
items Items released under the grant, in the shape of §3.1.
personhood Credentials, §7. Empty in 0.1.

The bundle is delivered three ways, all carrying identical content:

  1. Inline on the token response as context (§6.2) — so a single round-trip yields sign-in and context together.
  2. GET /api/v0/context with the access token — the live value of the grant, any time (§6.3).
  3. Signed, as context_token on the token response, or GET /api/v0/context?format=jwt — an RS256 JWS whose payload is { "sub", "protext": <bundle> }, issuer = provider, audience = client_id, verifiable against the provider's JWKS. Use it when the bundle has to be handed to a third party who needs to know where it came from.

6. Flow

Protext is the OIDC authorization code flow. Nothing else is supported (no implicit, no hybrid, no password grant).

6.1 Authorization request

GET {authorization_endpoint} with client_id, redirect_uri (must match a registered URI exactly; loopback ports are free per RFC 8252), response_type=code, scope (must include openid), state, and PKCE code_challenge + code_challenge_method=S256mandatory for public clients, recommended for all. Optional nonce, prompt=consent (forces the consent screen even when a grant exists).

The provider shows the consent screen: for every category the scopes unlock, every item the person has, each with a checkbox. The provider must show the item's value and source.provider. Defaults are the provider's choice, except that memory.* items must default to unchecked when no prior grant exists.

If a grant for this RP already covers the requested scopes and prompt is not consent, the provider may skip the screen and re-issue the existing grant (single sign-on). It must not widen the grant silently.

The redirect back carries code, state, and iss (RFC 9207).

6.2 Token request

POST {token_endpoint}, form-encoded: grant_type=authorization_code, code, redirect_uri, client_id, code_verifier (when PKCE was used), and client authentication for confidential clients (client_secret_basic or client_secret_post).

Response (application/json, Cache-Control: no-store):

{
  "token_type": "Bearer",
  "access_token": "…",
  "expires_in": 3600,
  "refresh_token": "…",            // only with offline_access
  "scope": "openid profile context:work",
  "id_token": "eyJ…",              // RS256; standard claims from §3.2 + protext_handle, protext_url
  "context": { …bundle… },         // Protext extension
  "context_token": "eyJ…"          // Protext extension, §5(3)
}

A code is single-use. Presenting it twice revokes every token minted from its grant.

6.3 Reading later

GET {userinfo_endpoint} returns the standard claims plus protext (the bundle). GET /api/v0/context returns the bundle alone. Both read the current items under the grant; both are logged in the person's access log with the RP's name and the item count. grant_type=refresh_token rotates the refresh token and also returns a fresh bundle.

6.4 Revocation and export

The person may revoke any grant at the provider; every token minted from it stops working at once. RPs may revoke their own tokens at {revocation_endpoint} (RFC 7009). The person may download the whole vault as a bundle at any time (/api/v0/export on the reference provider); providers must offer an equivalent.

6.5 Registration

RPs register at {registration_endpoint} (RFC 7591) with client_name, redirect_uris (https, or loopback), optional client_uri, logo_uri, and token_endpoint_auth_method (none for public clients — the default — or client_secret_post/client_secret_basic). The consent screen tells the person when an RP is self-registered. Providers may also offer registration through a signed-in dashboard.

7. Personhood (reserved)

{ "issuer": "world.org", "type": "proof-of-personhood", "verified_at": "2026-…", "proof": "…" }

personhood is an array of credentials issued by third parties, attached to the person's vault by the person. 0.1 defines the shape and issues nothing. The intended issuers are the ones that already exist — World ID, Human Passport, EU Digital Identity wallets, mobile driver's licences via the W3C Digital Credentials API — presented to the provider with OpenID for Verifiable Presentations and re-presented to RPs here. A provider must never make sign-in conditional on a credential. An RP that requires one says so on its own screen, after sign-in.

8. Discovery and multiple providers

A provider publishes /.well-known/openid-configuration (OIDC Discovery, with the additional members protext_context_endpoint and protext_metadata) and /.well-known/protext.json:

{
  "protext": "0.1",
  "issuer": "https://protext.org",
  "context_endpoint": "https://protext.org/api/v0/context",
  "profile_url_template": "https://protext.org/{handle}",
  "categories": { "work": { "scope": "context:work", "label": "Work", "description": "…" } },
  "kinds": { "work.building": { "label": "Building", "multi": true } },
  "personhood_issuers": [],
  "sdk": "https://protext.org/sdk.js"
}

A person's Protext address is <handle>@<provider> or the URL https://<provider>/<handle>. An RP that supports more than one provider asks for the address, fetches that provider's protext.json, registers with it if it has not already (§6.5), and proceeds. The reference SDK takes an issuer option for this.

9. Security considerations and roadmap

10. Conformance

A Protext provider implements §3–§6 and §8 and passes the reference test suite in the repository. A Protext RP uses the authorization code flow with PKCE, ignores unknown kinds, and respects revocation. A Protext importer produces items whose source is honest: provider names the real origin, evidence describes the real basis, and nothing private rides along.