[15/15] spec(core): define the protected resource metadata document - #314
Merged
tnunamak merged 3 commits intoSep 3, 2026
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
tnunamak
force-pushed
the
spec/int0902-14v3-declaration-operational-rules
branch
from
September 3, 2026 09:32
17b7359 to
611de31
Compare
tnunamak
force-pushed
the
spec/int0902-15v3-protected-resource-metadata
branch
from
September 3, 2026 09:32
88c79b4 to
f09572a
Compare
tnunamak
force-pushed
the
spec/int0902-14v3-declaration-operational-rules
branch
from
September 3, 2026 10:11
611de31 to
168f3b0
Compare
tnunamak
force-pushed
the
spec/int0902-15v3-protected-resource-metadata
branch
from
September 3, 2026 10:11
f09572a to
eb2f908
Compare
tnunamak
force-pushed
the
spec/int0902-14v3-declaration-operational-rules
branch
from
September 3, 2026 10:53
168f3b0 to
92494ef
Compare
tnunamak
force-pushed
the
spec/int0902-15v3-protected-resource-metadata
branch
from
September 3, 2026 10:53
eb2f908 to
b267978
Compare
Base automatically changed from
spec/int0902-14v3-declaration-operational-rules
to
spec/integration-0902
September 3, 2026 10:59
tnunamak
changed the base branch from
spec/int0902-14v3-declaration-operational-rules
to
spec/integration-0902
September 3, 2026 10:59
tnunamak
force-pushed
the
spec/int0902-15v3-protected-resource-metadata
branch
from
September 3, 2026 11:00
b267978 to
8c59bb1
Compare
Core never said what `GET /.well-known/oauth-protected-resource` is, although it is already a live wire surface. The reference implementation serves it, the public contract package schemas it, both OpenAPI documents publish it, and both search extension profiles advertise into it. A reader working only from Core could not learn that the document exists, let alone what a resource server must put in it. This adds the smallest subsection that closes the gap without inventing a capability grammar. A resource server MUST publish RFC 9728 protected resource metadata; RFC 9728 Section 3 already fixes where, and Section 5.1 already defines the `resource_metadata` challenge parameter that points a token-less client at it, so Core states the obligation and cites rather than restating the mechanics. `resource` is the resource server's own identifier, and for a `provider_native` source it is the identifier the declaration carries as `source.id` — the binding Section 5 already requires an authorization server to check. Four `pdpp_`-prefixed members become Core concepts with one sentence each: `pdpp_core_query_base`, `pdpp_token_kinds_supported`, `pdpp_self_export_supported`, and `pdpp_provider_connect_version`. RFC 9728 Section 2 permits additional parameters and Section 3.2 requires a reader to ignore what it does not understand, so a generic OAuth client is unaffected. `capabilities` stays with the extension profiles that advertise into it. Core neither requires it nor constrains its contents. `resource_name` is named as RFC 9728's own member rather than a PDPP extension, because a consent surface has no other name to show for the resource. The Informative discovery-and-trust companion described this document first and keeps `pdpp_source_declaration_uri`, which is a provider-native onboarding concern rather than a Core one. It now points at Core Section 8 for the rest, and its RFC 9728 citation is corrected from Section 3.1 to Section 3: 3.1 gives worked request examples, while Section 3 states the well-known-URI derivation rule itself. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…on_servers Two gaps in the metadata surface this row defines, both of which leave a client unable to bootstrap. The 401 challenge was missing entirely. Core defines `authentication_error` mapped to 401 and claims RFC 6750 conformance for both token kinds, but `WWW-Authenticate` appeared nowhere in the document before this row introduced it for RFC 9728. RFC 6750 Section 3 makes that header mandatory on exactly this response: a resource server that receives no credentials, or a token that does not grant access, MUST include it. A client holding no token therefore had no specified way to discover where to authenticate. The resource server now MUST send `WWW-Authenticate: Bearer` on a 401, MUST set `error="invalid_token"` when a token was presented and rejected, and MUST carry the RFC 9728 `resource_metadata` parameter pointing at this document. Putting all three in one place is deliberate: RFC 9728 Section 5.1 extends the very challenge RFC 6750 Section 3 requires, so splitting them would make a reader assemble the bootstrap path from two sections. `authorization_servers` was left to RFC 9728's OPTIONAL. That RFC makes the member optional because some deployments cannot enumerate their issuers, but leaving PDPP silent means a client that fetches the document may still not learn which server issues grants for the resource. A PDPP resource server now MUST publish it when the set is enumerable, and MUST omit it rather than publish a partial list when it is not — a truncated list is worse than none, because a client cannot tell it is incomplete. The alternative path for the non-enumerable case is stated rather than implied: the client uses the issuer from the grant it holds, or the deployment's own onboarding, and Core defines no third route. Resource Server conformance item 16 is extended to cover both. Not addressed here: Core reuses `insufficient_scope` with a meaning narrower than RFC 6750 Section 3.1's. Renaming a defined error code is wire-visible and belongs to the error-vocabulary row, not to this one. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
tnunamak
force-pushed
the
spec/int0902-15v3-protected-resource-metadata
branch
from
September 3, 2026 11:06
8c59bb1 to
1566140
Compare
…unwrap two companion paragraphs The prose gate caps sentences at 40 words and flags prose hard-wrapped mid-sentence. Four in-scope findings, all in text this child added or edited. Two long sentences in the Section 8 subsection. The 401 challenge rule was 44 words carrying three separate MUSTs; each is now its own sentence, which matters here because a reader implementing the bootstrap path needs all three and a 44-word chain invites missing the third. The `authorization_servers` rule was 46 words joining the omit-rather-than- truncate requirement to the alternative discovery path with a semicolon; split at that seam. Two hard-wrapped paragraphs in the Informative companion. The pointer paragraph this child adds was wrapped across four lines, and the metadata-URL paragraph it edits (correcting the RFC 9728 citation from Section 3.1 to Section 3) was wrapped across five. Both are now one paragraph per line, which is the convention the root specs follow and what the gate expects. No words change in either. No requirement changes. Verified after the split: the Bearer challenge, the `error="invalid_token"` parameter, the `resource_metadata` pointer, and the omit-rather-than-truncate rule are all still present and still MUST, and the corrected Section 3 citation survives. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PDPP (the Personal Data Portability Protocol) lets people authorize apps to read selected personal data. It splits that job across two servers: an authorization server, which asks a person what they want to share and issues a grant — the durable record of exactly which streams, fields, and time range that person approved for that app — and a resource server, which holds the data and hands over only what a presented grant covers.
An app that finds a PDPP resource server has no specified way to learn where to send its queries, which kinds of token the server accepts, or which authorization server issues grants for it. The document that already carries those answers exists and is served in production, but it is not defined in any document that binds an implementer. PDPP publishes several specification documents, of which exactly one is normative — binding on anyone claiming conformance — and the rest are Informative, meaning implementer guidance that binds no one. The normative one is
spec-core.md, called Core throughout this description, and Core never mentionsGET /.well-known/oauth-protected-resource. Every implementer today reads that surface off running code, so two independent implementations can disagree about it without either being non-conformant, and an app written against one will silently fail against the other.This pull request is child 15 of 15 in a stacked review series: fifteen separate PRs, each branched on the one before it, each proposing one decision for the owner to accept or reject, indexed as
[N/15]and tracked from umbrella PR #285. "Child" and "row" both mean one of those fifteen proposals — a child is the PR, and a row is that same proposal's line in the umbrella's decision table.The surface is live, not hypothetical, and four separate artifacts already depend on it. The reference implementation — the runnable PDPP server, which since August 2026 lives in the PDP-Connect/data-connect repository rather than in PDP-Connect/pdpp, the specification repository this PR is against — serves the document at that URL.
@pdpp/reference-contractis a different artifact despite the shared word: it is the npm package publishing PDPP's wire-shape schemas for anyone to validate against, and it schemas this document. Both OpenAPI documents — the machine-readable HTTP interface descriptions,reference-public.openapi.jsonandreference-full.openapi.json, a narrower public surface and the complete one — publish the endpoint. And both search extension profiles, the optional add-on specifications for lexical and semantic retrieval, advertise their capabilities into it. The only prose describing it sits in Source Declaration Discovery and Trust, a companion document that child [10/15] reclassifies from normative to Informative. That child has not merged — nothing in this series has — so the sequence to picture is: when child 10 lands, the last written description of a live wire surface stops binding anyone, and this child is what replaces it.Proposed rule: a resource server MUST publish protected resource metadata as defined in RFC 9728, and Core defines the four PDPP-specific members that document already carries. RFC 9728 (OAuth 2.0 Protected Resource Metadata) is the IETF standard for exactly this document, so Core states the obligation and cites the RFC rather than restating its mechanics: Section 3 of that RFC already fixes the URL by inserting
/.well-known/oauth-protected-resourceinto the resource identifier, and Section 5.1 already defines theresource_metadataparameter that aWWW-Authenticatechallenge on a 401 uses to point a token-less client at the document. Section 2 of the RFC permits additional members and Section 3.2 requires a reader to ignore any member it does not understand, which is what makes four PDPP-specific members safe to add: a generic OAuth client that knows nothing about PDPP is unaffected by their presence.The four members are
pdpp_core_query_base(the base path the Core query endpoints extend);pdpp_token_kinds_supported(PDPP defines two kinds of access token — an owner token, which a person uses to reach their own data, and a client token, which an app uses under a grant that person issued — and this member says which of the two this server accepts);pdpp_self_export_supported(whether an owner token may read the owner's own data through the app-facing query endpoints without any app grant); andpdpp_provider_connect_version(which PDPP version the interface implements). Each gets one sentence in the specification text.resourceis named as the resource server's own identifier. A source declaration is the JSON document a data source publishes describing what streams and fields it offers, and itssource.idfield is that source's identifier. For aprovider_nativesource — one whose own operator publishes that declaration, as opposed to a source reached through a connector, the adapter software that collects data from a platform that publishes no declaration of its own —resourceandsource.idare the same identifier. That is not new here: Core's Section 5, which defines the source declaration and the conditions under which an authorization server accepts one, already requires a server to verify that binding before it asks a person for consent. This subsection states the same identity from the resource server's side.Two things are deliberately left out.
capabilitiesstays with the extension profiles that advertise into it, so Core neither requires it nor constrains its contents. The reason is a layering rule the specification states about itself: Core must not depend on the optional extension profiles, because an implementer may conform to Core while implementing none of them — so a Core definition ofcapabilitieswould have to describe contents Core is not allowed to know about.pdpp_source_declaration_uristays in the Informative companion, because it is an onboarding pointer an authorization server follows once, not something a client reads to query records.Protected resource metadatasubsection: publish RFC 9728 metadata (MUST),resourceequalssource.idfor a provider-native source, fourpdpp_-prefixed members defined one sentence each,capabilitiesleft to the extension profiles,WWW-Authenticatebehavior cited to RFC 9728 Section 5.1. Core also carries a numbered checklist per role of what a conformant implementation does; the resource server's list gains item 16, requiring publication of the document.What was verified, and how. The member list was read from the reference implementation on
origin/mainof PDP-Connect/data-connect, inreference-implementation/server/metadata.ts— theProtectedResourceMetadatainterface and thebuildProtectedResourceMetadatafunction that populates it. That file is in the other repository, so this PR's own diff does not contain the evidence: a reviewer who wants to check the member list has to read it there. Every RFC 9728 section number in the new text was checked against the RFC's own text fetched from rfc-editor.org rather than recalled, because a wrong section number in a specification is a defect a reader hits immediately.What is not verified, specifically. The reference implementation publishes more members than this child defines. Beyond the four it defines, the running server emits
pdpp_discovery_hints,pdpp_agent_discovery, andpdpp_owner_agent_onboarding— three members that stay undefined here, so a reader of Core still cannot account for everything a real PDPP server publishes. Whether those three belong in Core, in a future agent-facing profile, or nowhere is a separate decision this child does not make. (A fourth extra member,pdpp_source_declaration_uri, is not in that undefined set: it is deliberately left with the Informative companion, as described above.) Separately, RFC 9728 contains no recommendation to use vendor prefixes for extension members — thepdpp_convention is PDPP's own choice, and the new text says so rather than attributing it to the RFC.Evidence.
pnpm spec:check— one of the repository's own check scripts, run through the pnpm package manager — regenerates the site's specification pages from the rootspec-*.mdfiles and fails on any difference between the two copies; its pass means the published site and the source of truth agree, so a reader of the website sees the same requirement an implementer reads in the repository.pnpm spec:owner-filter-checkguards a specific past mistake: PDPP's owner tokens carry no grant, so prose about filtering an owner's own reads must not borrow the vocabulary used for narrowing an app's grant, or an implementer could apply grant-narrowing logic to a token that has no grant to narrow. Its pass means this change introduced no such crossover.pnpm spec:dateschecks that each specification file's declaredDate:header is not older than the last commit that changed that file's prose (as opposed to a commit that only moved the stamp itself); it reports one failure, onspec-reference-implementation-examples.md. That file documents worked examples drawn from the reference implementation, and it fails the same way onorigin/main— the shared mainline branch, the samemainnamed above — with none of this series applied, so this change neither caused nor fixed it. (origin/mainfails on a second file,spec-collection-profile.md, which describes the optional connector-collection profile; an earlier child in this series moved that file'sDate:stamp forward, which is why only one failure remains here.) The site build prerenders every specification route; the built HTML for Core contains the new#protected-resource-metadataanchor, all four member names, and the new resource-server conformance item requiring the document be published — so the requirement reaches the rendered page, not just the source file. No client, authorization server, or resource server was executed against this text: nothing here is covered by a runtime test.Amended 2026-09-03. Two additions, one from the independent decision review (item J4) and one from the standards review (item I4).
authorization_serverswas left to RFC 9728's OPTIONAL. That RFC makes the member optional because some deployments cannot enumerate their issuers, but PDPP silence means a client that fetches the document may still not learn which server issues grants for the resource — which defeats part of the point of publishing it. A PDPP resource server now MUST publishauthorization_serverswhen that set is enumerable, and MUST omit it rather than publish a partial list when it is not: a truncated list is worse than none, because a client cannot tell it is incomplete. The alternative path for the non-enumerable case is now stated rather than implied — the client uses the issuer from the grant it already holds, or the deployment's own onboarding — and Core defines no third route.Core never required the RFC 6750 challenge.
WWW-Authenticateappeared nowhere in Core before this child introduced it for RFC 9728, yet Core definesauthentication_errormapped to 401 and claims RFC 6750 conformance for both token kinds. RFC 6750 Section 3 makes that header mandatory on exactly this response. A client holding no token therefore had no specified way to discover where to authenticate. The resource server now MUST sendWWW-Authenticate: Beareron a 401, MUST seterror="invalid_token"when a token was presented and rejected, and MUST carry the RFC 9728resource_metadataparameter pointing at this document. All three are stated together on purpose: RFC 9728 Section 5.1 extends the very challenge RFC 6750 Section 3 requires, and splitting them would make a reader assemble the bootstrap path from two sections.Resource Server conformance item 16 is extended to cover both.
Not addressed here. The standards review also notes that Core reuses
insufficient_scopewith a meaning narrower than RFC 6750 Section 3.1's ("Expansion requests a stream not in the grant"). Renaming or realigning a defined error code is wire-visible and belongs to the error-vocabulary row, not to this one.Assisted-by: AI