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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- Inline on the token response as
context(§6.2) — so a single round-trip yields sign-in and context together. GET /api/v0/contextwith the access token — the live value of the grant, any time (§6.3).- Signed, as
context_tokenon the token response, orGET /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=S256 — mandatory 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
- PKCE is mandatory for public clients and the only accepted method is S256. Redirect URIs are matched exactly. Codes live five minutes and are single-use with replay detection. Access tokens are opaque and stored hashed; refresh tokens rotate.
- The reference provider uses password authentication in 0.1. Passkeys (WebAuthn) are the planned primary credential for 0.2 — a person's provider account should be bound to a key, which is also the natural anchor for §7.
- Email is released with
email_verified: falseuntil the provider verifies it. Do not rely on Protext emails for account recovery without your own verification. source.urlis a pointer, not a proof. A verifier who cares must visit it. Providers must not put URLs there that reveal more than the item itself.- Planned for 0.2: selective-disclosure presentation of the bundle as SD-JWT VC, so items can be proven individually to third parties without the provider online; W3C Digital Credentials API presentation from the provider page; an MCP server on the vault so a person's own agents can read it under the same grants.
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.