diff --git a/REUSE.toml b/REUSE.toml index 84d5ca550..337273c4c 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -34,7 +34,7 @@ SPDX-License-Identifier = "CC0-1.0" # This is last and override-level so the protocol-specification license cannot # be shadowed by the documentation annotation. [[annotations]] -path = ["spec-*.md", "apps/site/content/docs/spec-*.md", "packages/conformance-suite/spec/spec-*.md", "LICENSE-specs"] +path = ["spec-*.md", "apps/site/content/docs/spec-*.md", "LICENSE-specs"] precedence = "override" SPDX-FileCopyrightText = "The PDP-Connect Contributors" SPDX-License-Identifier = "LicenseRef-CSL-1.0" diff --git a/docs/reference/conformance-normative-matrix.md b/docs/reference/conformance-normative-matrix.md index 75bfbb05c..8c6100978 100644 --- a/docs/reference/conformance-normative-matrix.md +++ b/docs/reference/conformance-normative-matrix.md @@ -11,7 +11,7 @@ This file is the **v0.1** inventory: the adopted normative draft at Every normative clause in Core sections 2, 3, 4, 5, 6, 7, 8, 10 and 12 and Appendix A, mapped to the Section 9 conformance items that summarize it and to the suite cases that -exercise it. Section 9 is the index here, not the authority: its 45 numbered +exercise it. Section 9 is the index here, not the authority: its 123 numbered items are one-line precis of the clauses below. `Observable` records how a clause could be checked at all, independent of @@ -48,16 +48,16 @@ one that exists today. ## Totals -Clauses enumerated: **144**. +Clauses enumerated: **708**. | MUST clauses | Count | | --- | --- | -| Total | **119** | -| With at least one case | **85** | -| Untestable (review-only, no case) | **26** | -| Not yet tested (observable, no case) | **8** | +| Total | **667** | +| With at least one case | **84** | +| Untestable (review-only, no case) | **188** | +| Not yet tested (observable, no case) | **395** | -The last three rows partition the total. 2 of the clauses with a case are +The last three rows partition the total. 0 of the clauses with a case are review-only: a case exercises part of them, and a run still reports them `untestable`. A clause counts as covered when a registered case exercises it. Coverage is @@ -66,26 +66,26 @@ whether it passes is a separate question a run answers. | Level | Clauses | | --- | --- | -| MUST | 119 | -| MAY | 12 | -| SHOULD | 13 | +| MUST | 667 | +| MAY | 20 | +| SHOULD | 21 | | Role | Clauses | | --- | --- | -| resource-server | 59 | -| authorization-server | 72 | -| client | 27 | -| source-declaration | 10 | +| resource-server | 190 | +| authorization-server | 418 | +| client | 102 | +| source-declaration | 23 | A clause binding two roles is counted under each, so the role column sums above the clause total. | Observable | Clauses | | --- | --- | -| black-box-http | 83 | -| review-only | 39 | -| client-capture | 15 | -| declaration-static | 7 | +| black-box-http | 412 | +| review-only | 202 | +| client-capture | 65 | +| declaration-static | 29 | ## Clauses by Section 9 item @@ -95,34 +95,68 @@ to know what a result about that item does and does not establish. ### AS-1 (authorization-server, MUST) -Accepts selection requests using the RFC 9396 `authorization_details` envelope with type "https://pdpp.dev/data-access". +Accepts selection requests using the RFC 9396 `authorization_details` envelope with `type: "https://pdpp.dev/data-access"`. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `10.3-2` | 1573 | SHOULD | black-box-http | — | — | +| `10.3-2` | 2751 | SHOULD | black-box-http | — | — | ### AS-2 (authorization-server, MUST) -Validates selection requests against one retained SourceDeclaration snapshot: rejects unknown streams, unsupported selection parameters, and unrecognized selection presets. +Validates public request structure and owner-independent capabilities, rejecting public invalidity with `invalid_authorization_details`. Resolves selections against one retained SourceDeclaration snapshot. Treats an otherwise valid selection that is infeasible under privately accepted capabilities as owner-dependent eligibility, only within authenticated review. There, omits an optional unapproved stream or refuses under the required-stream/minimum rules with indistinguishable `access_denied`, without client-visible private diagnostics, as Authorization privacy boundary requires. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `5.2-4` | 519 | MUST | black-box-http | `AS-2/time-range-without-consent-time-field-refused` | — | -| `6.8-1` | 787 | MUST | black-box-http | `AS-2/view-and-fields-mutually-exclusive` | — | -| `6.8-2` | 793 | MUST | black-box-http | `AS-2/time-range-without-consent-time-field-refused` | — | -| `6.8-3` | 797 | MUST | black-box-http | `AS-2/wildcard-beside-named-stream-refused`
`AS-2/duplicate-stream-name-refused` | — | -| `6.9-1` | 818 | MUST | black-box-http | `AS-16/duplicate-stream-in-a-selection-preset-refused` | — | +| `5.3-7` | 792 | MUST | black-box-http | `AS-2/time-range-without-consent-time-field-refused` | — | +| `6.11-3` | 1187 | MUST | black-box-http | `AS-2/view-and-fields-mutually-exclusive` | The case refuses-then-classifies: it requires 400 `invalid_request`, but the locked text maps a request with both to RFC 9396 `invalid_authorization_details`. The refusal half agrees with the clause; the classification half contradicts it, so a conforming AS fails the case. | +| `6.11-14` | 1200 | MUST | black-box-http | `AS-2/time-range-without-consent-time-field-refused` | — | +| `6.11-19` | 1206 | MUST | black-box-http | `AS-2/wildcard-beside-named-stream-refused`
`AS-2/duplicate-stream-name-refused` | — | +| `6.12-1` | 1216 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-2` | 1216 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-3` | 1218 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-4` | 1218 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-5` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-6` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-7` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-8` | 1222 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-9` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-10` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-11` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-12` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-13` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-14` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-15` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-16` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-17` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-18` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-19` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-20` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-21` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-22` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-23` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-24` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-25` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-26` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-27` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-28` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-29` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-30` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-31` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-32` | 1238 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.12-33` | 1238 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-34` | 1238 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.17-1` | 1318 | MUST | black-box-http | `AS-16/duplicate-stream-in-a-selection-preset-refused` | — | ### AS-3 (authorization-server, MUST) -Issues grants conforming to the Section 7 grant schema, with all fields derived from the selection request, client registration, or AS policy. +For Core `data-access` details, issues grants that conform to the grant schema defined in Section 7 (normative field tables). The AS generates `grant_id` and `issued_at`; it resolves other grant fields from authenticated identities, the selection request, the accepted source declaration, eligible instance handles, user approval, AS policy, or AS-resolved client metadata. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.3-3` | 734 | SHOULD | review-only | — | — | -| `7.1-1` | 866 | MUST | black-box-http | `AS-3/issued-grant-artifact-matches-section-7-schema`
`AS-3/resolved-grant-matches-observable-schema-fields` | — | -| `7.2-4` | 897 | MUST | black-box-http | `AS-3/client-claims-stay-outside-the-resolved-grant` | — | -| `10.3-1` | 1571 | MUST | review-only | — | "Tamper-sensitive" names a design posture, not a wire behaviour; the spec defers grant signing and a formal token format to a future version, so there is no observable artifact to check. | +| `6.3-3` | 1057 | SHOULD | review-only | — | — | +| `7.1-1` | 1371 | MUST | black-box-http | `AS-3/issued-grant-artifact-matches-section-7-schema`
`AS-3/resolved-grant-matches-observable-schema-fields` | The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's. | +| `7.2-16` | 1411 | MUST | black-box-http | `AS-3/client-claims-stay-outside-the-resolved-grant` | — | +| `10.3-1` | 2749 | MUST | review-only | — | "Tamper-sensitive" names a design posture, not a wire behaviour; the spec defers grant signing and a formal token format to a future version, so there is no observable artifact to check. | ### AS-4 (authorization-server, MUST) @@ -130,91 +164,89 @@ Expands wildcards and selection presets into explicit stream names, fields, per- | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.8-3` | 797 | MUST | black-box-http | `AS-2/wildcard-beside-named-stream-refused`
`AS-2/duplicate-stream-name-refused` | — | -| `7.2-1` | 897 | MUST | black-box-http | `AS-4/omitted-fields-expanded-before-issuance` | — | +| `6.11-19` | 1206 | MUST | black-box-http | `AS-2/wildcard-beside-named-stream-refused`
`AS-2/duplicate-stream-name-refused` | — | +| `7.2-6` | 1411 | MUST | black-box-http | `AS-4/omitted-fields-expanded-before-issuance` | — | ### AS-5 (authorization-server, MUST) -Produces a Source validation failure when a request contains both or neither of `streams` and `selection_preset`. +Produces a binding-neutral Source validation failure when a Core `data-access` detail contains both or neither of `streams` and `selection_preset`. The OAuth/RAR binding maps it to RFC 9396 `invalid_authorization_details`. Rejects a request in which any authorization-details object has an unsupported type or is invalid under its type's definition (Section 6). | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.8-1` | 787 | MUST | black-box-http | `AS-2/view-and-fields-mutually-exclusive` | — | +| `6.11-3` | 1187 | MUST | black-box-http | `AS-2/view-and-fields-mutually-exclusive` | The case refuses-then-classifies: it requires 400 `invalid_request`, but the locked text maps a request with both to RFC 9396 `invalid_authorization_details`. The refusal half agrees with the clause; the classification half contradicts it, so a conforming AS fails the case. | ### AS-6 (authorization-server, MUST) -MUST NOT reject a `purpose_code` solely because it is absent from the PDPP registry; displays `purpose_description` or the raw URI for unrecognized codes. +MUST NOT reject a complete detail solely because its `purpose_code` is not in Appendix A. Rejects a detail with a code outside Appendix A that lacks a non-empty `purpose_description`, and displays that description. MAY reject a `purpose_code` based on local policy. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.5-1` | 753 | MUST | black-box-http | `AS-6/unregistered-purpose-not-rejected` | — | -| `A.0-1` | 1895 | MUST | black-box-http | `AS-6/unregistered-purpose-not-rejected` | — | +| `6.6-2` | 1090 | MUST | black-box-http | — | AS-6/unregistered-purpose-not-rejected submits a detail with no `purpose_description`. The locked text requires the AS to reject such a detail for a code outside Appendix A, so the case does not probe the MUST NOT, which concerns a complete detail. No case submits a complete detail with an unrecognized purpose code. | +| `A.0-1` | 3223 | MUST | black-box-http | — | AS-6/unregistered-purpose-not-rejected submits a detail with no `purpose_description`. The locked text requires the AS to reject such a detail for a code outside Appendix A, so the case does not probe the MUST NOT, which concerns a complete detail. No case submits a complete detail with an unrecognized purpose code. | ### AS-7 (authorization-server, MUST) -Renders requester identity, declaration-authored descriptions, policy declarations, and client claims as distinct categories; MUST attribute `client_claims` to the client and MUST NOT present them as protocol-enforced terms. - -| Clause | Spec line | Level | Observable | Cases | Gap | -| --- | --- | --- | --- | --- | --- | -| `5.3-2` | 538 | SHOULD | review-only | — | Consent-surface rendering is not observable over HTTP; a case could only assert on a deployment's HTML, which tests the deployment rather than the protocol. | -| `5.3-3` | 539 | MAY | review-only | — | — | -| `5.3-4` | 541 | MUST | client-capture | `AS-7/client-does-not-override-display-descriptions` | — | -| `5.4-2` | 567 | MUST | review-only | — | A rendering obligation on the consent surface, which black-box HTTP cannot observe; the suite can see what the AS bound, not what it showed. | -| `5.8-7` | 628 | MUST | review-only | — | The obligation is on rendered output in an unspecified output context; asserting on it means asserting on a deployment's HTML, which is outside what this suite judges. | -| `6.1-1` | 694 | MAY | review-only | — | — | -| `6.1-2` | 700 | MUST | black-box-http | `AS-7/valid-url-hosted-client-identity-accepted`
`AS-7/malformed-url-hosted-client-identity-refused` | — | -| `6.1-4` | 702 | MUST | black-box-http | `AS-7/valid-url-hosted-client-identity-accepted`
`AS-7/malformed-url-hosted-client-identity-refused` | — | -| `6.1-5` | 706 | MUST | review-only | — | Which source the AS resolved from is visible only on the consent surface it renders, which is not black-box observable. | -| `6.1-6` | 707 | MUST | review-only | — | A display obligation on the consent surface; a case can see what the AS bound into the grant, not what it rendered. | -| `6.1-7` | 708 | MAY | review-only | — | — | -| `6.1-8` | 709 | MUST | review-only | — | Trust-signal rendering is a consent-surface obligation; not observable over the protocol surface this suite drives. | -| `6.1-9` | 710 | MUST | review-only | — | Consent-surface wording obligation; not observable without asserting on a deployment's rendered text. | -| `6.1-10` | 711 | MUST | black-box-http | — | Missing fixture: an inbound HTTP endpoint named as an unverified client's `logo_uri` that logs requests, plus a way to open the consent surface so a fetch triggered by rendering it is seen. The harness hosts a request-logging fixture for client identity documents (src/tests/client-identity.ts serveDocument) but has no consent-render hook. A fetch made by the owner's browser, rather than the AS, would be seen only if the fixture logs it; the verified-client and proxied-asset exceptions need local policy knowledge the suite lacks. | -| `6.1-11` | 712 | SHOULD | review-only | — | — | -| `6.2-2` | 718 | MUST | black-box-http | `AS-7/pre-registered-public-clients-carry-no-private-state` | — | -| `6.3-1` | 728 | MUST | review-only | — | A visual-register obligation on the consent surface. This is the clause AS-7 chiefly summarizes, and it is the clearest example of a MUST that is not black-box observable in principle. | -| `6.4-1` | 746 | MUST | review-only | — | The normative core of AS-7, and not observable over HTTP: a case can see what the server bound, not what it rendered. | -| `6.5-2` | 754 | MUST | review-only | — | The AS half is a display obligation on the consent surface; the client half is a SHOULD needing client capture. Neither is reachable from this suite's seam. | -| `7.2-6` | 897 | MUST | black-box-http | `AS-15/rendered-client-claims-bound-with-attribution` | — | +Renders requester identity metadata, declaration-authored data descriptions, structured policy declarations, and client-authored claims as semantically distinct categories during consent. MUST attribute `client_claims` to the client and MUST NOT present them as protocol-enforced terms. If `client_claims` are rendered during final review, binds the normalized exact claims into the immutable final approval artifact and review revision, and preserves that binding in retained consent evidence, without adding them to the resolved grant or RS enforcement. Records in the issued grant's `client.client_display` the exact `policy_uri` and `tos_uri` it presented during consent, and omits any it did not present. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `5.4-2` | 815 | SHOULD | review-only | — | Consent-surface rendering is not observable over HTTP; a case could only assert on a deployment's HTML, which tests the deployment rather than the protocol. | +| `5.4-3` | 816 | MAY | review-only | — | — | +| `5.4-5` | 819 | MUST | client-capture | `AS-7/client-does-not-override-display-descriptions` | — | +| `5.6-7` | 879 | MUST | review-only | — | A rendering obligation on the consent surface, which black-box HTTP cannot observe; the suite can see what the AS bound, not what it showed. | +| `5.10-7` | 946 | MUST | review-only | — | The obligation is on rendered output in an unspecified output context; asserting on it means asserting on a deployment's HTML, which is outside what this suite judges. | +| `6.1-1` | 1017 | MAY | review-only | — | — | +| `6.1-2` | 1023 | MUST | black-box-http | `AS-7/valid-url-hosted-client-identity-accepted`
`AS-7/malformed-url-hosted-client-identity-refused` | — | +| `6.1-4` | 1025 | MUST | black-box-http | `AS-7/valid-url-hosted-client-identity-accepted`
`AS-7/malformed-url-hosted-client-identity-refused` | — | +| `6.1-5` | 1029 | MUST | review-only | — | Which source the AS resolved from is visible only on the consent surface it renders, which is not black-box observable. | +| `6.1-6` | 1030 | MUST | review-only | — | A display obligation on the consent surface; a case can see what the AS bound into the grant, not what it rendered. | +| `6.1-7` | 1031 | SHOULD | review-only | — | — | +| `6.1-9` | 1032 | MUST | review-only | — | Trust-signal rendering is a consent-surface obligation; not observable over the protocol surface this suite drives. | +| `6.1-10` | 1033 | MUST | review-only | — | Consent-surface wording obligation; not observable without asserting on a deployment's rendered text. | +| `6.1-11` | 1034 | MUST | black-box-http | — | Missing fixture: an inbound HTTP endpoint named as an unverified client's `logo_uri` that logs requests, plus a way to open the consent surface so a fetch triggered by rendering it is seen. The harness hosts a request-logging fixture for client identity documents (src/tests/client-identity.ts serveDocument) but has no consent-render hook. A fetch made by the owner's browser, rather than the AS, would be seen only if the fixture logs it; the verified-client and proxied-asset exceptions need local policy knowledge the suite lacks. | +| `6.1-12` | 1035 | SHOULD | review-only | — | — | +| `6.2-2` | 1041 | MUST | black-box-http | `AS-7/pre-registered-public-clients-carry-no-private-state` | — | +| `6.3-1` | 1051 | MUST | review-only | — | A visual-register obligation on the consent surface. This is the clause AS-7 chiefly summarizes, and it is the clearest example of a MUST that is not black-box observable in principle. | +| `6.4-1` | 1069 | MUST | review-only | — | The normative core of AS-7, and not observable over HTTP: a case can see what the server bound, not what it rendered. | +| `6.6-3` | 1091 | MUST | review-only | — | The AS half is a display obligation on the consent surface; the client half is a SHOULD needing client capture. Neither is reachable from this suite's seam. | +| `7.2-10` | 1411 | MUST | black-box-http | `AS-15/rendered-client-claims-bound-with-attribution` | — | ### AS-8 (authorization-server, MUST) -Tracks grant lifecycle (active, expired, revoked) and reflects revocation immediately in introspection responses (`active: false`). +Tracks grant lifecycle (active, expired, revoked). Reflects revocation immediately in introspection: the revoked grant is inactive at once, and a token is `active: false` when no grant it covers is active. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `2.2-3` | 104 | MUST | black-box-http | `AS-8/revoked-grant-refused` | A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path. | -| `10.2-2` | 1556 | MUST | black-box-http | — | Restates 8.1-1 as a revocation-propagation bound; needs a separated AS/RS target and a timed revocation observation, neither of which the current topologies support. | -| `10.2-7` | 1565 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Introspection of the family-linked token is checked only where the target offers an introspection route; otherwise the case judges the resource read alone, which is not the introspection interface this clause names. The prohibition on issuing refresh tokens for a `single_use` grant is not covered, and needs a target supporting both `single_use` and refresh tokens at once. | -| `10.2-10` | 1554 | MUST | black-box-http | `AS-8/revoked-grant-refused` | A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path. | -| `10.7-1` | 1593 | MUST | black-box-http | `AS-8/revoked-token-introspects-inactive` | — | +| `2.2-3` | 107 | MUST | black-box-http | `AS-8/revoked-grant-refused` | A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path. | +| `10.2-2` | 2730 | MUST | black-box-http | `AS-8/revoked-grant-refused` | A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path. | +| `10.2-8` | 2743 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Introspection of the family-linked token is checked only where the target offers an introspection route; otherwise the case judges the resource read alone, which is not the introspection interface this clause names. The prohibition on issuing refresh tokens for a `single_use` grant is not covered, and needs a target supporting both `single_use` and refresh tokens at once. | +| `10.8-12` | 2785 | MUST | black-box-http | `AS-8/revoked-token-introspects-inactive` | — | ### AS-9 (authorization-server, MUST) -Issues access tokens bound to specific grants, carrying the PDPP introspection extension fields. +For Core `data-access` details, issues access tokens bound to specific grants. Access tokens include the PDPP introspection extension fields. Binds each Core client token to exactly one grant. Without a companion credential binding, rejects more than one Core data-access detail with `invalid_authorization_details`. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `10.2-3` | 1558 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Family linkage is tested through reuse revocation; the `expires_in`/`exp` derivation and omission rules are not, and would need a target that can issue a non-expiring access token. | -| `10.2-4` | 1560 | MUST | black-box-http | `AS-9/token-response-forbids-caching` | — | +| `10.2-4` | 2736 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Family linkage is tested through reuse revocation; the `expires_in`/`exp` derivation and omission rules are not, and would need a target that can issue a non-expiring access token. | +| `10.2-5` | 2738 | MUST | black-box-http | `AS-9/token-response-forbids-caching` | — | ### AS-10 (authorization-server, MUST) -For `single_use` grants, consumes the grant atomically with first client-token issuance and rejects later attempts to issue new client access tokens against it. +For `single_use` grants, consumes the grant atomically with first client-token issuance. Gives that access token a finite expiration set by local policy and, when the grant has `expires_at`, no later than `expires_at`; that expiry ends the grant's one read window. Lets a later token in the same refresh-token family carry the grant only inside the window, reports the grant inactive for reads after it, and otherwise rejects access-token issuance against the consumed grant. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `7.5-1` | 927 | MUST | black-box-http | `AS-10/single-use-grant-consumed` | — | -| `7.5-2` | 927 | MAY | black-box-http | — | — | +| `7.8-4` | 1483 | MUST | black-box-http | `AS-10/single-use-grant-consumed` | — | ### AS-11 (authorization-server, MUST) -Validates stream/field/view/resource-id shape at grant issuance. +Validates stream/field/view/resource-id shape at grant issuance. Rejects `resources` on a stream with more than one instance. Rejects `resources` entries that are not canonical under Section 4 and does not percent-decode JSON `resources` values. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `7.1-1` | 866 | MUST | black-box-http | `AS-3/issued-grant-artifact-matches-section-7-schema`
`AS-3/resolved-grant-matches-observable-schema-fields` | — | -| `7.2-0` | 891 | MUST | black-box-http | `AS-2/dotted-nested-field-refused` | The `fields` row of the normative StreamGrant table; carries no RFC 2119 keyword, so its MUST level is Section 9 AS item 11's. The top-level-only constraint is tested; uniqueness and non-emptiness of the issued allowlist are not. | +| `7.1-1` | 1371 | MUST | black-box-http | `AS-3/issued-grant-artifact-matches-section-7-schema`
`AS-3/resolved-grant-matches-observable-schema-fields` | The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's. | +| `7.2-1` | 1399 | MUST | black-box-http | `AS-2/dotted-nested-field-refused` | The `fields` row of the normative StreamGrant table; carries no RFC 2119 keyword, so its MUST level is Section 9 AS item 11's. The top-level-only constraint is tested; uniqueness, non-emptiness, and inclusion of every `required_fields` entry in the issued allowlist are not. | ### AS-12 (authorization-server, MUST) @@ -222,197 +254,809 @@ MUST NOT define a view including fields absent from the retained SourceDeclarati | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `5.6-2` | 598 | MUST | black-box-http | `AS-12/view-within-declared-schema` | — | +| `5.8-2` | 916 | MUST | black-box-http | `AS-12/view-within-declared-schema` | — | ### AS-13 (authorization-server, MUST) -Resolves view names to field lists at issuance time and stores resolved `fields` in the StreamGrant. +Resolves view names to field lists at issuance time; stores resolved `fields` and `required_fields` in the `StreamGrant`. Freezes `cursor_field` as `sort_field` when its format is `date` or `date-time` and the grant includes it; otherwise omits `sort_field`. Client-token record reads reject query-time `view` in v0.1. Owner-token current-capability reads MAY resolve current views. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `5.6-2a` | 600 | MUST | black-box-http | `AS-13/view-evolution-does-not-widen-an-issued-grant` | Recorded as MUST-equivalent because Section 9 AS item 13 states it as one; the spec's own sentence here carries no RFC 2119 keyword, so the binding level is the conformance list's rather than this subsection's. The covering case exercises the field-widening half against a target whose view evolves after issuance; the re-consent half binds a consent flow the suite cannot drive. | +| `5.8-3` | 918 | MUST | black-box-http | `AS-13/view-evolution-does-not-widen-an-issued-grant` | Recorded as MUST-equivalent because Section 9 AS item 13 states it as one; the spec's own sentence here carries no RFC 2119 keyword, so the binding level is the conformance list's rather than this subsection's. The covering case exercises the field-widening half against a target whose view evolves after issuance; the re-consent half binds a consent flow the suite cannot drive. | ### AS-14 (authorization-server, MUST) -Obtains explicit affirmative user consent before issuing grants with `purpose_code: https://pdpp.dev/purpose/ai_training`. +Rejects a detail whose `processing_permissions` is malformed or contains a duplicate, unknown, or unsupported value, with `invalid_authorization_details`, and never ignores, substitutes, or narrows such a value or issues an ordinary grant in its place. Advertises in `pdpp_processing_permissions_supported`, and grants, only permissions whose complete defining contract it implements. Treats `https://pdpp.dev/processing/ai_training` as unsupported until the PDPP AI Training Profile is published and its registration is approved, and afterwards supports it only by conforming to that profile. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.7-1` | 774 | MUST | review-only | `AS-14/explicit-consent-required-for-ai-training` | — | -| `A.0-2` | 1905 | MUST | review-only | `AS-14/explicit-consent-required-for-ai-training` | — | +| `6.8-1` | 1126 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-3` | 1144 | MUST | black-box-http | — | No case in this suite version exercises this clause. | ### AS-15 (authorization-server, MUST) -Resolves omitted instance IDs before final approval, binds resolved instances and decision fields to an immutable review revision, and rejects stale approval when eligibility or the reviewed revision changed. +For Core `data-access` details, resolves omitted instance IDs before the final approval surface. Binds exact resolved instances and all final decision fields to an immutable review revision or digest. Rejects stale approval if eligibility or the reviewed revision changes before approval. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.3-2` | 730 | MUST | black-box-http | `AS-15/rendered-client-claims-bound-with-attribution` | — | -| `7.2-1` | 897 | MUST | black-box-http | `AS-4/omitted-fields-expanded-before-issuance` | — | -| `7.2-2` | 897 | MUST | black-box-http | `AS-15/final-approval-artifact-carries-resolved-terms` | — | -| `7.2-3` | 897 | MUST | black-box-http | `AS-15/approval-requires-the-issued-revision` | — | -| `7.2-4` | 897 | MUST | black-box-http | `AS-3/client-claims-stay-outside-the-resolved-grant` | — | -| `7.2-5` | 897 | MUST | black-box-http | `AS-15/approval-requires-the-issued-revision` | The case rejects approval of a revision the AS never issued. It never makes a previously valid revision stale, nor changes instance eligibility after review, so the stale-after-change obligation is not exercised. Missing case: approve a revision after eligibility changes. | -| `7.2-6` | 897 | MUST | black-box-http | `AS-15/rendered-client-claims-bound-with-attribution` | — | +| `6.3-2` | 1053 | MUST | black-box-http | `AS-15/rendered-client-claims-bound-with-attribution` | — | +| `7.2-6` | 1411 | MUST | black-box-http | `AS-4/omitted-fields-expanded-before-issuance` | — | +| `7.2-7` | 1411 | MUST | black-box-http | `AS-15/final-approval-artifact-carries-resolved-terms` | The case checks the instance ids, streams, fields, resources, temporal field, since/until, purpose, retention, client identity and grant expiry. The locked text also requires `required_fields`, the inclusive/exclusive statement of the bounds, and processing permissions, which the case does not check. | +| `7.2-10` | 1411 | MUST | black-box-http | `AS-15/rendered-client-claims-bound-with-attribution` | — | +| `7.2-15` | 1411 | MUST | black-box-http | `AS-15/approval-requires-the-issued-revision` | — | +| `7.2-16` | 1411 | MUST | black-box-http | `AS-3/client-claims-stay-outside-the-resolved-grant` | — | +| `7.2-17` | 1411 | MUST | black-box-http | `AS-15/approval-requires-the-issued-revision` | The case rejects approval of a revision the AS never issued. It never makes a previously valid revision stale, nor changes instance eligibility after review, so the stale-after-change obligation is not exercised. Missing case: approve a revision after eligibility changes. | ### AS-16 (authorization-server, MUST) -Retains one exact SourceDeclaration snapshot through validation, consent, narrowing, issuance, and consent evidence; a later declaration never substitutes for it. +Retains one exact SourceDeclaration snapshot through request validation, consent display, narrowing, issuance, and consent evidence. A later current declaration never substitutes for it. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `5.2-1` | 508 | MUST | review-only | — | What the AS treated as authenticated is an internal trust decision; from outside, an accepted and a rejected declaration differ only by whether consent proceeded, which does not discriminate this clause. | -| `5.2-5` | 525 | MUST | declaration-static | `AS-16/embedded-schema-remote-reference-refused` | Partially covered. The case exercises the local-reference obligation (`$ref`/`$dynamicRef` must be local fragments), which is the half that makes consent interpretation depend on a mutable remote schema. The `$schema` dialect equality and full metaschema meta-validation are implemented by the reference target but not separately asserted: a case for each would need its own defect to discriminate, and the remote-reference oracle already fails a target that skips embedded-schema validation altogether. | -| `5.8-1` | 616 | MUST | black-box-http | — | The former case submitted a non-normative top-level `authority` member to an operator declaration-onboarding hook. Core defines the accepted authority as AS-side acceptance metadata and gives `SourceDeclaration` no such document member; this surface cannot observe a client introducing authority during authorization. A client-authorization fixture with an overridable source identifier is required. | -| `5.8-2` | 618 | MUST | black-box-http | `AS-16/provider-native-source-id-mismatch-refused` | — | -| `5.8-3` | 620 | MUST | review-only | — | Restates 5.2-1 as an acceptance obligation; what the AS relied on internally is not observable from a request outcome. | -| `5.8-4` | 622 | MUST | black-box-http | `AS-16/declaration-equivocation-refused-and-prior-content-retained` | — | -| `5.8-5` | 624 | MUST | black-box-http | — | Missing fixture: an inbound HTTP endpoint serving a candidate declaration that logs each request the AS makes (credentials sent, redirect targets followed, remote schema retrieval). The harness already hosts one for client identity documents (src/tests/client-identity.ts serveDocument) but has no hook to offer an AS a declaration pointer to retrieve. The HTTPS requirement needs a TLS fixture, and the connection-bound address check is internal to the AS, so those parts stay unobservable even with the fixture. | -| `5.8-6` | 626 | MUST | review-only | — | A DNS-rebinding defence stated as a connection-time ordering obligation; distinguishing it from a URL-time check needs control of the AS's resolver, not an HTTP client. | -| `5.8-7` | 628 | MUST | review-only | — | The obligation is on rendered output in an unspecified output context; asserting on it means asserting on a deployment's HTML, which is outside what this suite judges. | -| `5.8-8` | 628 | MUST | black-box-http | `RS-15/client-metadata-projection-closed` | — | -| `10.1-1` | 1546 | SHOULD | review-only | — | The spec itself states these limits are set by local judgment and that a peer cannot observe which values a server chose, so no interoperability test can exist for this clause. | - -### AS-17 (authorization-server, MUST) - -Returns 400 `unsupported_version` when the `PDPP-Version` header specifies an unsupported version. +| `5.3-1` | 779 | MUST | review-only | — | What the AS treated as authenticated is an internal trust decision; from outside, an accepted and a rejected declaration differ only by whether consent proceeded, which does not discriminate this clause. | +| `5.3-8` | 798 | MUST | declaration-static | `AS-16/embedded-schema-remote-reference-refused` | Partially covered. The case exercises the local-reference obligation (`$ref`/`$dynamicRef` must be local fragments), which is the half that makes consent interpretation depend on a mutable remote schema. The `$schema` dialect equality and full metaschema meta-validation are implemented by the reference target but not separately asserted: a case for each would need its own defect to discriminate, and the remote-reference oracle already fails a target that skips embedded-schema validation altogether. | +| `5.10-1` | 934 | MUST | black-box-http | — | The former case submitted a non-normative top-level `authority` member to an operator declaration-onboarding hook. Core defines the accepted authority as AS-side acceptance metadata and gives `SourceDeclaration` no such document member; this surface cannot observe a client introducing authority during authorization. A client-authorization fixture with an overridable source identifier is required. | +| `5.10-2` | 936 | MUST | black-box-http | `AS-16/provider-native-source-id-mismatch-refused` | — | +| `5.10-3` | 938 | MUST | review-only | — | Restates 5.3-1 as an acceptance obligation; what the AS relied on internally is not observable from a request outcome. | +| `5.10-4` | 940 | MUST | black-box-http | `AS-16/declaration-equivocation-refused-and-prior-content-retained` | — | +| `5.10-5` | 942 | MUST | black-box-http | — | Missing fixture: an inbound HTTP endpoint serving a candidate declaration that logs each request the AS makes (credentials sent, redirect targets followed, remote schema retrieval). The harness already hosts one for client identity documents (src/tests/client-identity.ts serveDocument) but has no hook to offer an AS a declaration pointer to retrieve. The HTTPS requirement needs a TLS fixture, and the connection-bound address check is internal to the AS, so those parts stay unobservable even with the fixture. | +| `5.10-6` | 944 | MUST | review-only | — | A DNS-rebinding defence stated as a connection-time ordering obligation; distinguishing it from a URL-time check needs control of the AS's resolver, not an HTTP client. | +| `5.10-7` | 946 | MUST | review-only | — | The obligation is on rendered output in an unspecified output context; asserting on it means asserting on a deployment's HTML, which is outside what this suite judges. | +| `5.10-9` | 946 | MUST | black-box-http | `RS-15/client-metadata-projection-closed` | — | +| `10.1-1` | 2722 | SHOULD | review-only | — | The spec itself states these limits are set by local judgment and that a peer cannot observe which values a server chose, so no interoperability test can exist for this clause. | + +### AS-17 (authorization-server, MAY) + +Not used. `PDPP-Version` negotiation is a Resource Server requirement and does not version Authorization Server endpoints. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.14-1` | 1458 | MUST | black-box-http | `RS-11/unsupported-version-rejected`
`AS-17/unsupported-version-rejected` | Carries no RFC 2119 keyword; its MUST level is Section 9 AS item 17 and RS item 11. The unsupported-version half is tested on both roles; the absent-header half — that the response echoes the selected version in a `PDPP-Version` response header — is not asserted by any case, and needs only an unwritten case, not a new hook. | +| `8.17-7` | 2482 | MUST | black-box-http | `RS-11/unsupported-version-rejected` | The unsupported-version half is tested on the resource server. No case asserts that an absent header selects `0.1`, or that the response carries exactly one `PDPP-Version` field naming the selected identifier. AS-17/unsupported-version-rejected is no longer mapped here: the locked text makes `PDPP-Version` negotiation a Resource Server requirement and retires AS item 17. | ### AS-18 (authorization-server, MUST) -Authenticates the RS at the RFC 7662 introspection endpoint and returns the complete grant enforcement context in one response. +For a separated AS and RS, authenticates the RS at the RFC 7662 introspection endpoint and returns the complete grant enforcement context in one response, including `pdpp_instance_bindings`. Never returns `pdpp_instance_bindings` to a client. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `2.2-1` | 104 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; that the RS resolves the COMPLETE enforcement context from the response has no dedicated case, and the same obligation at 8.2-2 is review-only. | -| `8.2-1` | 1083 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | — | -| `8.2-2` | 1095 | MUST | review-only | — | The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer. | -| `10.2-1` | 1554 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; the no-second-lookup half is an internal call-count obligation, as at 8.2-2. | +| `2.2-1` | 107 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; that the RS resolves the COMPLETE enforcement context from the response has no dedicated case, and the same obligation at 8.3-8 is review-only. | +| `8.3-1` | 1991 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | — | +| `8.3-8` | 2006 | MUST | review-only | — | The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer. | +| `10.2-1` | 2730 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; the no-second-lookup half is an internal call-count obligation, as at 8.3-8. | ### AS-19 (authorization-server, MUST) -Consumes each OAuth authorization code atomically on first successful redemption and rejects every later redemption with `invalid_grant`. +Consumes each OAuth authorization code atomically on its first successful redemption. Rejects every later redemption with `invalid_grant` and does not issue another token. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `10.2-5` | 1562 | MUST | black-box-http | `AS-19/authorization-code-redemption-is-not-replayable` | — | +| `10.2-6` | 2740 | MUST | black-box-http | `AS-19/authorization-code-redemption-is-not-replayable` | — | ### AS-20 (authorization-server, MUST) -Issues refresh tokens only for `continuous` grants, rotates by family, and on reuse of a superseded token revokes the family and every family-linked access token, returning `invalid_grant`. +For a token response that covers only Core `data-access` grants, issues a refresh token only when at least one covered grant is `continuous`. Never extends a `single_use` grant's read window through refresh. Rotates refresh tokens by family. Reuse of a superseded token revokes the family and every family-linked access token, returns `invalid_grant`, and requires fresh authorization. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `10.2-3` | 1558 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Family linkage is tested through reuse revocation; the `expires_in`/`exp` derivation and omission rules are not, and would need a target that can issue a non-expiring access token. | -| `10.2-6` | 1564 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | — | -| `10.2-7` | 1565 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Introspection of the family-linked token is checked only where the target offers an introspection route; otherwise the case judges the resource read alone, which is not the introspection interface this clause names. The prohibition on issuing refresh tokens for a `single_use` grant is not covered, and needs a target supporting both `single_use` and refresh tokens at once. | +| `10.2-4` | 2736 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Family linkage is tested through reuse revocation; the `expires_in`/`exp` derivation and omission rules are not, and would need a target that can issue a non-expiring access token. | +| `10.2-7` | 2742 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Rotation and replay revocation are tested. That refresh issuance and family revocation are serialized is an internal ordering property that no black-box case can observe. | +| `10.2-8` | 2743 | MUST | black-box-http | `AS-20/refresh-reuse-revokes-the-family` | Introspection of the family-linked token is checked only where the target offers an introspection route; otherwise the case judges the resource read alone, which is not the introspection interface this clause names. The prohibition on issuing refresh tokens for a `single_use` grant is not covered, and needs a target supporting both `single_use` and refresh tokens at once. | ### AS-21 (authorization-server, MUST) -Rejects unsupported persisted authorization state before introspection or request handling, without reconstructing missing facts from current configuration. +Rejects unsupported persisted authorization state before introspection or request handling. Does not reconstruct missing facts from current configuration and requires fresh consent when no migration applies. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.7-3` | 1477 | MUST | review-only | — | An ordering obligation over internal persisted state: the suite would need to write a legacy-shaped record into the target's store, which the adapter deliberately cannot do, and even then could not observe that the refusal happened *before* route handling. | +| `10.2-9` | 2743 | MUST | review-only | — | An upgrade-path obligation over pre-existing persisted state: the suite would have to plant legacy tokens in the target's store before boot, which the adapter deliberately cannot do. | + +### AS-22 (authorization-server, MUST) + +Rejects a source declaration containing an unrecognized `source.kind` or `streams[].semantics` value. Rejects a language map or `display.default_language` that breaks a structural or language-tag rule in Display text languages. MAY reject a declaration whose language versions it detects do not state the same meaning. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `5.5-1` | 856 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.5-2` | 856 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.5-3` | 856 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.5-4` | 856 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `5.5-5` | 858 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.5-6` | 858 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.5-7` | 858 | MUST | declaration-static | — | No case in this suite version exercises this clause. | + +### AS-23 (authorization-server, MUST) + +Issues a grant with `grantors` only after each listed representative approves the same immutable final review revision. Authenticates each representative and, before showing any subject-specific information or accepting approval, verifies their authority under Representative approval. Binds each identity and capacity to that revision and retained consent evidence, and keeps the data subject in `subject`. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.5-1` | 1443 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | + +### AS-24 (authorization-server, MUST) + +For Core `data-access` details, lets the owner approve or decline each detail of a request on its own. Issues one grant for each approved, resolved detail, covering one subject, one source, and one purpose, with its own lifecycle. Identifies grants by `grant_id` and does not treat subject, source, and purpose as a grant key. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `7.4-3` | 921 | MUST | review-only | — | An ordering obligation over internal persisted state: the suite would need to write a legacy-shaped record into the target's store, which the adapter deliberately cannot do, and even then could not observe that the refusal happened *before* route handling. | -| `10.2-8` | 1565 | MUST | review-only | — | An upgrade-path obligation over pre-existing persisted state: the suite would have to plant legacy tokens in the target's store before boot, which the adapter deliberately cannot do. | +| `7.0-1` | 1333 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-25 (authorization-server, MUST) + +For Core `data-access` details, if it offers client-initiated revocation, ends grants as Section 10 defines. Revoking a refresh token, current or superseded, ends every grant its family covers; revoking an access token ends its grants only when no refresh-token family covers them. In the OAuth binding, implements it as RFC 7009 token revocation and advertises `revocation_endpoint` in RFC 8414 metadata. Accepts `pdpp_disposition` on that request and records it only when the request carries current client authentication. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.15-3` | 1569 | SHOULD | client-capture | — | No case in this suite version exercises this clause. | +| `7.21-2` | 1692 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-1` | 2777 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-26 (authorization-server, MUST) + +Ends one grant among several that share a credential without revoking that credential or ending the other grants. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `10.8-4` | 2777 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-27 (authorization-server, MUST) + +Rejects a declaration that names an ineligible `consent_time_field` or a non-boolean `consent_time_content_may_exceed`. Rejects a `time_range` with no bound, a bound whose type does not match the field's format, an invalid bound, or a `since` not before `until`; the OAuth/RAR binding returns `invalid_authorization_details`. This error applies only to public invalidity. A failure that depends on a declaration accepted privately for the owner is an eligibility failure under Authorization privacy boundary and MUST NOT produce a client-visible declaration-dependent validation error. Never converts or rounds a bound. Sets `time_constraint.type` from the field's format in the retained declaration. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.12-1` | 1216 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-2` | 1216 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-3` | 1218 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-4` | 1218 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-5` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-6` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-7` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-8` | 1222 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-9` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-10` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-11` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-12` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-13` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-14` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-15` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-16` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-17` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-18` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-19` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-20` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-21` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-22` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-23` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-24` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-25` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-26` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-27` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-28` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-29` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-30` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-31` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-32` | 1238 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.12-33` | 1238 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-34` | 1238 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-28 (authorization-server, MUST) + +For each time-bounded stream, renders the exact bounds with their inclusive and exclusive meaning and the declared meaning of the field. Shows a relative label only when it has verified it for those bounds. Discloses that matched records can contain data from outside the bounds when the stream declares `consent_time_content_may_exceed: true`. For a `continuous` grant, discloses that the client can see records leave the window. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `5.6-5` | 877 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-8` | 881 | MAY | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-12` | 884 | MUST | declaration-static | — | No case in this suite version exercises this clause. | + +### AS-29 (authorization-server, MUST) + +Gives each client pairwise identifiers: `subject.id`, `grantors[].id`, instance handles, and any ID Token `sub` differ between clients for the same subject, grantor, or instance. They stay the same for one client, are never reassigned, and cannot be reversed by a client. Sets ID Token `sub` to the authenticated person's identifier, which is a grantor's identifier when a representative authenticates. Uses a globally resolvable `subject.id` only when the owner explicitly chooses to disclose it to that client, and binds that identifier and the client into the final approval artifact and retained consent evidence. Returns an internal subject identifier and `pdpp_instance_bindings` only to the RS. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.4-1` | 1429 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-30 (authorization-server, MUST) + +Resolves each request as Request resolution defines before final approval. Treats requested instances, resources, time bounds, and the field ceiling as upper limits, never narrows below schema-required fields, and adds nothing to compensate for a refused selection. Checks required streams and each retained stream's `minimum` before issuing a grant or client token. Reports an unmet minimum on a required stream as `access_denied` and a malformed `minimum` as `invalid_authorization_details`. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.13-1` | 1244 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.13-2` | 1244 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.13-3` | 1244 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-1` | 1248 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-2` | 1248 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-3` | 1250 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-4` | 1250 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-5` | 1250 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-6` | 1252 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-7` | 1252 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.14-8` | 1254 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.14-9` | 1254 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-1` | 1258 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-2` | 1258 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-3` | 1258 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-4` | 1260 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-5` | 1260 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-6` | 1262 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-7` | 1262 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-8` | 1262 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-9` | 1262 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-10` | 1262 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-11` | 1266 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-12` | 1266 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-13` | 1266 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-14` | 1268 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-15` | 1268 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `6.15-16` | 1270 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-17` | 1270 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.15-18` | 1270 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-1` | 1274 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-2` | 1274 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-3` | 1274 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-4` | 1276 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-5` | 1276 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-6` | 1278 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-7` | 1278 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `6.16-8` | 1280 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-9` | 1282 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-10` | 1282 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-11` | 1282 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-12` | 1282 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-13` | 1282 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-14` | 1282 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-15` | 1284 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-16` | 1284 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.16-17` | 1284 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-18` | 1286 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-19` | 1301 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-20` | 1301 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-31 (authorization-server, MUST) + +Resolves purpose and retention only from the current request or recipient-authorized standing terms, and retains the evidence identifying those terms with the consent evidence. Rejects a `retention.max_duration` that is not in the form `PnD`. Discloses during owner review that envelope identifiers are part of every disclosed record. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.16-2` | 1274 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.24-2` | 1847 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-32 (authorization-server, MUST) + +Returns one `{ type, grant }` element per covered grant in every token response that issues a client access token, including refresh responses, as Client-visible authorization result defines. Returns the same element shape in introspection. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.3-1` | 1415 | MUST | black-box-http | `AS-9/token-response-carries-granted-authorization-details` | The case checks that a token response carries a non-empty `authorization_details` array and, where introspection is available, that it agrees with introspection on the granted streams and fields. It does not count one element per covered grant, does not check the `{ type, grant }` element shape, and reads `streams` at the top level of each element where the locked text nests them under `grant`, so against a conforming response its comparison reduces to an empty-against-empty match. | +| `7.3-2` | 1415 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-3` | 1415 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-4` | 1417 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-5` | 1417 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-6` | 1419 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.3-7` | 1419 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-8` | 1419 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.3-9` | 1421 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.3-10` | 1421 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-11` | 1421 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-12` | 1423 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-13` | 1423 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.3-14` | 1425 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-33 (authorization-server, MUST) + +Advertises `https://pdpp.dev/data-access` and `https://pdpp.dev/grant-lifecycle` in `authorization_details_types_supported` and rejects unknown detail types, and a detail that carries a member it does not define. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.4-1` | 2022 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-34 (authorization-server, MUST) + +Chooses one version of each language-map display value it renders by the procedure in Display language. Does not reject a request solely because a requested language is unsupported. Binds the identity, exact string, and any chosen language tag of each declaration display value shown on final review into the final approval artifact, the review revision or digest, and retained consent evidence. Records display text from an earlier consent step as Section 7 requires. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.5-1` | 1073 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.5-2` | 1075 | SHOULD | black-box-http | — | No case in this suite version exercises this clause. | +| `6.5-3` | 1077 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.5-4` | 1083 | SHOULD | black-box-http | — | No case in this suite version exercises this clause. | +| `6.5-5` | 1083 | SHOULD | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-35 (authorization-server, SHOULD) + +SHOULD give every client access token a finite expiration and SHOULD issue it from a refresh-token family for a `continuous` grant, which allows renewal without re-consent while the family remains valid. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.8-5` | 1484 | SHOULD | client-capture | — | No case in this suite version exercises this clause. | + +### AS-36 (authorization-server, MUST) + +For a detail that carries a processing permission, requires a distinct affirmative owner selection, never preselects it, and never makes another detail depend on it. Renders each permission as a labeled term distinct from the purpose. Requires a non-empty `purpose_description` on an AI training detail. Issues the grant with its exact permission set or not at all, and never adds a permission to a detail that did not request it. Binds that set into final owner review, the review revision, retained consent evidence, the grant, token results, and introspection, and requires a new review when the set changes after review. Includes a reviewed `expires_at` in every training grant. On the approval surface for an AI training detail, makes the statements and avoids the claims that AI training consent lists. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.10-1` | 1172 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.10-2` | 1172 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.10-3` | 1172 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.10-4` | 1172 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.10-5` | 1172 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.10-6` | 1172 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.10-7` | 1174 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.10-8` | 1174 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.10-9` | 1174 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.10-10` | 1174 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-37 (authorization-server, MUST) + +When it supports a processing permission, keeps read access and processing authority distinct as Read access and processing authority defines. Issues no further processing authority after grant revocation or grant expiry. When the grant has `expires_at`, issues no processing authority that outlasts it. Never lets a processing credential confer read access. Does not treat read-window closure, access-token expiry, or refresh-token replay detection alone as ending processing authority or prohibiting its renewal. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.9-1` | 1492 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.9-2` | 1493 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.9-3` | 1495 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.9-4` | 1495 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-38 (authorization-server, MUST) + +Its operator does not use owner data held in the AS role for AI training, except under a training grant issued to it as a client. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.9-6` | 1164 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-39 (authorization-server, MUST) + +Provides the grant lifecycle operation and advertises `pdpp_grant_lifecycle_endpoint`, as Grant lifecycle operation defines. For queries, accepts any token it issued to the client that covers the grant, whether active, expired, superseded, or revoked. Requires current client authentication for writes. Returns indistinguishable `invalid_grant` entries for unknown, foreign, and uncovered grants. Stores token digests, not tokens, and can disable any token it issued for lifecycle use. Admits at least one query per grant per retry interval from each client. Records a client-conveyed `delete` as that client's `received` report only when the request reports the client's first receipt time. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.21-1` | 1688 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-2` | 1692 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-3` | 1698 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.21-4` | 1709 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-5` | 1709 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-6` | 1713 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-7` | 1713 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.21-8` | 1713 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.21-9` | 1715 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.21-10` | 1715 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-11` | 1715 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-12` | 1715 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.21-13` | 1715 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.21-14` | 1715 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.21-15` | 1716 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-16` | 1716 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-17` | 1717 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.21-18` | 1717 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-19` | 1718 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.21-20` | 1735 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-21` | 1739 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-22` | 1746 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-23` | 1746 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-24` | 1746 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.21-25` | 1748 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-26` | 1748 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.21-27` | 1748 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-1` | 1804 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-2` | 1804 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.22-3` | 1804 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-4` | 1806 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-5` | 1806 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-6` | 1806 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-7` | 1806 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.22-8` | 1806 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.22-9` | 1808 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-10` | 1808 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-40 (authorization-server, MUST) + +Returns a positive assessment only when no erasure instruction applies to the grant, whatever its read state, and only from state ordered after every terminal event it has acknowledged. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.17-2` | 1593 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-41 (authorization-server, MUST) + +On every interface through which the owner ends a grant, requires an explicit disposition for each grant in the action's scope, rejects a request without one, and applies no default. Presents `keep` and `delete` with neither preselected, and lists the complete affected set as Ending paths defines, including already-ended and consumed grants whose held data may remain and grants that carry a processing permission. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.15-1` | 1566 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.15-2` | 1566 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.15-3` | 1569 | SHOULD | client-capture | — | No case in this suite version exercises this clause. | +| `7.15-4` | 1572 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.15-5` | 1574 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.15-6` | 1574 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.15-7` | 1574 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.15-8` | 1576 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.15-9` | 1576 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.15-10` | 1576 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.15-11` | 1576 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.15-12` | 1576 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.15-13` | 1578 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.15-14` | 1578 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.15-15` | 1578 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.15-16` | 1578 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.15-17` | 1578 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.15-18` | 1578 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-42 (authorization-server, MUST) + +Records each erasure instruction durably before acknowledging it and preserves its terminal effect. An immediate instruction revokes an active grant. An advance instruction takes effect at grant end and does not itself end the grant earlier. Records acceptance, first return, reported receipt, and reported completion or exception separately. Accepts `retained_until` only in its three forms, and shows a `retained` report's end in that form, never as completed deletion. From the effective time, issues no further processing authority that permits use of the covered data. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.9-3` | 1495 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.15-15` | 1578 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-43 (authorization-server, MUST) + +Shows the owner the stop-use and deletion statements that Erasure requires, and the time by which covered processing stops. Never shows a receipt-based deletion deadline calculated from acceptance or first return. Until a receipt is reported, shows the latest recorded stage and no deletion date. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.18-1` | 1612 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-2` | 1614 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-3` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-4` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-5` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-6` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-7` | 1633 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-8` | 1635 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-9` | 1640 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-10` | 1640 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-11` | 1642 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-12` | 1644 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-13` | 1644 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-14` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-15` | 1646 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-16` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-17` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-18` | 1648 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-19` | 1650 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-20` | 1650 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.18-21` | 1650 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-22` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-23` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-24` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-25` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-26` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-27` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-28` | 1652 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-29` | 1652 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-30` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-31` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-32` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.19-1` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-2` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-3` | 1658 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-4` | 1660 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-5` | 1662 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-6` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-7` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-1` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-2` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-3` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-4` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-5` | 1674 | MAY | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-6` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-7` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-8` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-9` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### AS-44 (authorization-server, MUST) + +Meets Lifecycle durability: records terminal events before acknowledging them, in a history that no restore or failover through a path its mechanism detects can make an enforcement point treat as current while it omits an acknowledged terminal event. Records the other events that section lists before a response discloses or relies on them, and never reuses a `grant_id` or `instruction_id`. Never returns a positive assessment, an active introspection result, or a token that contradicts an acknowledged terminal event, on any issuance or assessment path, including code, device-code, and refresh exchanges, token re-delivery, lifecycle write authentication, and package fan-out. This holds except after a restore or rollback path that it discloses as undetected. After restore or failover, treats a history as current only when its declared mechanism establishes that the history includes every acknowledged terminal event, and otherwise fails closed. Describes the durability it actually provides, including the disk and host losses it does not survive, the restore and rollback paths its mechanism does not detect, and what such a path can revive. While its history is unavailable, accepts, records, and later applies owner revocations, dispositions, and erasure requests. For an action affecting several grants, records a result per grant and reports the action complete only when every requested ending and disposition is recorded. Keeps lifecycle and authentication bindings for as long as that section requires. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.23-1` | 1814 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-2` | 1814 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-3` | 1816 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-4` | 1817 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-5` | 1817 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-6` | 1818 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-7` | 1819 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-8` | 1819 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-9` | 1819 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-10` | 1820 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-11` | 1820 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-12` | 1820 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-13` | 1821 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-14` | 1823 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-15` | 1823 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-16` | 1823 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-17` | 1824 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-18` | 1824 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-19` | 1824 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-20` | 1825 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-21` | 1825 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-22` | 1826 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-23` | 1826 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-24` | 1826 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.23-25` | 1826 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-26` | 1827 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-27` | 1827 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-28` | 1827 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-29` | 1828 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.23-30` | 1828 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-31` | 1828 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-32` | 1828 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.23-33` | 1828 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.23-34` | 1829 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-35` | 1829 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-36` | 1829 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.23-37` | 1829 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-45 (authorization-server, MUST) + +Shows in final owner review what happens to held data after the grant ends, as Recipient commitments and approval defines. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.16-1` | 1274 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-2` | 1274 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-3` | 1274 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-4` | 1276 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-5` | 1276 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-6` | 1278 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-7` | 1278 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `6.16-8` | 1280 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-9` | 1282 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-10` | 1282 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-11` | 1282 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-12` | 1282 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-13` | 1282 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-14` | 1282 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-15` | 1284 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-16` | 1284 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.16-17` | 1284 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-18` | 1286 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-19` | 1301 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.16-20` | 1301 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-46 (authorization-server, MUST) + +On security revocation, shows the owner the ending and its reason and offers the disposition. Before decommission, offers a disposition for every grant whose held data may remain, shows the stage of each disposition, and warns about each `delete` that no client has received. In a narrowing flow, states the effect of each disposition as Grant narrowing defines. Lets the owner request erasure for an ended grant while its held data may remain. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.12-2` | 1530 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.12-3` | 1530 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-47 (authorization-server, MUST) + +Supports lifecycle recovery, and gives a recovery token no read access and no refresh token. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.22-1` | 1804 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-2` | 1804 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.22-3` | 1804 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-4` | 1806 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-5` | 1806 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-6` | 1806 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-7` | 1806 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.22-8` | 1806 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.22-9` | 1808 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.22-10` | 1808 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-48 (authorization-server, MUST) + +Retrieves declarations over HTTPS without ambient credentials. Validates every redirect and final URL against its accepted pointer and network policy. Validates the destination immediately before each connection and connects only to that validated address. Rejects automatic remote-schema retrieval and fails closed on any failed check, as Section 5 requires. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `5.10-5` | 942 | MUST | black-box-http | — | Missing fixture: an inbound HTTP endpoint serving a candidate declaration that logs each request the AS makes (credentials sent, redirect targets followed, remote schema retrieval). The harness already hosts one for client identity documents (src/tests/client-identity.ts serveDocument) but has no hook to offer an AS a declaration pointer to retrieve. The HTTPS requirement needs a TLS fixture, and the connection-bound address check is internal to the AS, so those parts stay unobservable even with the fixture. | +| `5.10-6` | 944 | MUST | review-only | — | A DNS-rebinding defence stated as a connection-time ordering obligation; distinguishing it from a URL-time check needs control of the AS's resolver, not an HTTP client. | + +### AS-49 (authorization-server, MUST) + +Renders all declaration display values and client-authored values, including `client_display`, resolved client metadata, `purpose_description`, and `client_claims`, safely for their output context under Section 5. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `5.10-7` | 946 | MUST | review-only | — | The obligation is on rendered output in an unspecified output context; asserting on it means asserting on a deployment's HTML, which is outside what this suite judges. | + +### AS-50 (authorization-server, MUST) + +Accepts a valid client ID metadata document as an identity without requiring preregistration. Keeps that identity-form acceptance distinct from authorization or admission denial under local policy, as Section 6 requires. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.1-2` | 1023 | MUST | black-box-http | `AS-7/valid-url-hosted-client-identity-accepted`
`AS-7/malformed-url-hosted-client-identity-refused` | — | + +### AS-51 (authorization-server, MUST) + +Freezes each stream's schema-required consent floor from the retained declaration into `required_fields` before issuance. Includes the set in reviewed decision evidence, the client-visible grant, and the grant returned through introspection. Includes an empty array for an empty floor. Keeps the set unchanged for the life of that grant. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.14-3` | 1250 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.2-2` | 1405 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-52 (authorization-server, MUST) + +Applies Authorization privacy boundary to every supported authorization entry point. Performs owner-independent syntax and binding checks without querying private owner availability. Defers owner-dependent resolution to authenticated review. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.12-1` | 1216 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-2` | 1216 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-3` | 1218 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-4` | 1218 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-5` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-6` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-7` | 1220 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-8` | 1222 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-9` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-10` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-11` | 1224 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-12` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-13` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-14` | 1226 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-15` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-16` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-17` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-18` | 1228 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-19` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-20` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-21` | 1230 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-22` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-23` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-24` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-25` | 1232 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-26` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-27` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-28` | 1234 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-29` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-30` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-31` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-32` | 1238 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.12-33` | 1238 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-34` | 1238 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-53 (authorization-server, MUST) + +Keeps optional ineligible streams unapproved. Refuses required ineligible streams and unmet required authorization minima with `access_denied`. Preserves `invalid_authorization_details` for malformed or unsupported details and any error defined for a specific authorization-details type. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.15-11` | 1266 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-54 (authorization-server, MUST) + +Demonstrates equivalent pre-authentication observables when only protected owner state varies. Demonstrates equivalent client-visible eligibility denials after review. Includes status, response shape and size, redirects, headers, transaction behavior, and timing in its evidence. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.12-29` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.12-31` | 1236 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### AS-55 (authorization-server, MUST) + +Records the entry points, protected-state variants, timing method, and observation limits exercised by its authorization-privacy tests. Reports an unobserved supported path as UNVERIFIED rather than passed. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `9.4-1` | 2658 | MUST | review-only | — | The obligation binds the conformance report an implementer publishes about its own authorization-privacy tests, not a request the suite sends. No case can observe it. | + +### AS-56 (authorization-server, MUST) + +States during final owner review that ordinary access confers no independent-recipient disclosure authority. Distinguishes parties acting for the client. Does not claim that this duty technically prevents onward disclosure. Applies the closed processing-permission rules without substitution or fallback. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.16-15` | 1284 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.16-17` | 1284 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.20-6` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### AS-57 (authorization-server, MUST) + +Passes a differential authorization-privacy case that varies private declaration acceptance, temporal capability, and view availability against a matched owner refusal. The client-visible result, the callback, and any transaction-status channel are equivalent across the variants, and no unapproved diagnostic reaches the client. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `9.5-1` | 2694 | MUST | review-only | — | The obligation binds the conformance report an implementer publishes, and the differential authorization-privacy case it names needs a matched owner refusal the black-box adapter cannot arrange. No case in this suite version exercises it. | ### RS-1 (resource-server, MUST) -Implements the Section 8 query endpoints: list streams, get stream metadata, list records, get a single record, get a blob, delete a record (owner-authenticated). +Implements the query endpoints defined in Section 8: list streams, get stream metadata, list records, get a single record, get a blob, delete a record (owner-authenticated). | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.12-1` | 1383 | MUST | black-box-http | `RS-1/blob-outside-grant-refused`
`RS-1/blob-unauthenticated-refused` | — | -| `8.12-2` | 1385 | MUST | black-box-http | `RS-1/get-blob-bytes` | Content-Length is checked against the body only when present: the case cannot tell whether the length was known, so an omitted Content-Length is not a finding. `Accept-Ranges: bytes` is not asserted: the case never issues a Range request, so it cannot tell whether the target supports ranges. | -| `8.12-3` | 1391 | MUST | black-box-http | `RS-1/blob-redirect-response-valid` | The header half is checked; the "valid for at least 60 seconds" half is not, because the suite deliberately does not follow the signed URL and so cannot observe its expiry. | +| `8.15-4` | 2356 | MUST | black-box-http | `RS-1/blob-outside-grant-refused`
`RS-1/blob-unauthenticated-refused` | — | +| `8.15-5` | 2366 | MUST | black-box-http | `RS-1/get-blob-bytes` | Content-Length is checked against the body only when present: the case cannot tell whether the length was known, so an omitted Content-Length is not a finding. `Accept-Ranges: bytes` is not asserted: the case never issues a Range request, so it cannot tell whether the target supports ranges. | +| `8.15-6` | 2372 | MUST | black-box-http | `RS-1/blob-redirect-response-valid` | The header half is checked. The suite deliberately does not follow the signed URL, so the expiry bounds the following paragraph sets on it are not observed. | ### RS-2 (resource-server, MUST) -Enforces grant constraints on every client request: stream membership, explicit instance handles, frozen `time_constraint`, `fields` allowlist, and `resources` filter. +Enforces grant constraints on every client request: stream membership, explicit instance handles, frozen `time_constraint`, `fields` allowlist, `required_fields`, frozen `sort_field`, and `resources` filter. Authorizes each disclosure under one grant and never combines the constraints of different grants. Uses Section 4's canonical key representation, never percent-decodes JSON key values, and decodes an HTTP path key exactly once before comparison. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.1-2` | 1077 | MUST | black-box-http | `RS-15/client-metadata-projection-closed`
`RS-2/field-projection-not-exceeded` | — | -| `8.12-1` | 1383 | MUST | black-box-http | `RS-1/blob-outside-grant-refused`
`RS-1/blob-unauthenticated-refused` | — | +| `8.1-3` | 1969 | MUST | black-box-http | `RS-15/client-metadata-projection-closed`
`RS-2/field-projection-not-exceeded` | — | +| `8.15-4` | 2356 | MUST | black-box-http | `RS-1/blob-outside-grant-refused`
`RS-1/blob-unauthenticated-refused` | — | ### RS-3 (resource-server, MUST) -Resolves access tokens through authenticated RFC 7662 introspection, enforces only from that response, makes no second AS lookup while handling the request, and caches positive results no longer than min(token_exp, 60 seconds). +In a separated deployment, resolves access tokens through authenticated RFC 7662 introspection, enforces only from that response, and makes no second AS lookup while handling the request. A co-located deployment may use a local equivalent. Caches positive results for no more than 60 seconds and never past `exp` when present. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `2.2-1` | 104 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; that the RS resolves the COMPLETE enforcement context from the response has no dedicated case, and the same obligation at 8.2-2 is review-only. | -| `2.2-2` | 104 | MUST | review-only | — | The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer, as at 8.2-2. | -| `8.1-1` | 1066 | MUST | black-box-http | — | Needs a separated AS/RS target plus a clock the case controls: the suite would have to revoke a grant and prove the RS stops serving within 60 seconds, which no current target topology supports. | -| `8.2-1` | 1083 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | — | -| `8.2-2` | 1095 | MUST | review-only | — | The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer. | -| `8.2-4` | 1099 | MUST | black-box-http | `AS-8/revoked-grant-refused` | A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path. | -| `10.2-1` | 1554 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; the no-second-lookup half is an internal call-count obligation, as at 8.2-2. | -| `10.2-2` | 1556 | MUST | black-box-http | — | Restates 8.1-1 as a revocation-propagation bound; needs a separated AS/RS target and a timed revocation observation, neither of which the current topologies support. | +| `2.2-1` | 107 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; that the RS resolves the COMPLETE enforcement context from the response has no dedicated case, and the same obligation at 8.3-8 is review-only. | +| `2.2-2` | 107 | MUST | review-only | — | The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer, as at 8.3-8. | +| `8.1-1` | 1957 | MUST | black-box-http | — | Needs a separated AS/RS target plus a clock the case controls: the suite would have to revoke a grant and prove the RS stops serving within 60 seconds, which no current target topology supports. | +| `8.3-1` | 1991 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | — | +| `8.3-8` | 2006 | MUST | review-only | — | The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer. | +| `8.3-21` | 2018 | MUST | black-box-http | `AS-8/revoked-grant-refused` | A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path. | +| `10.2-1` | 2730 | MUST | black-box-http | `AS-18/introspection-requires-authentication` | The authentication half is tested; the no-second-lookup half is an internal call-count obligation, as at 8.3-8. | ### RS-4 (resource-server, MUST) -Distinguishes owner tokens from client tokens via `pdpp_token_kind`, determined solely from the introspection response and never from token syntax. +Distinguishes owner tokens from client tokens via `pdpp_token_kind`, and authorizes Core operations only for those two kinds, each under its own rules. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.1-3` | 1079 | MUST | black-box-http | `RS-4/token-kind-not-inferred-from-syntax` | — | -| `8.2-3` | 1097 | MUST | black-box-http | `RS-4/unrecognized-token-kind-is-unauthorized` | — | +| `8.1-4` | 1971 | MUST | black-box-http | `RS-4/token-kind-not-inferred-from-syntax` | — | +| `8.3-19` | 2016 | MUST | black-box-http | `RS-4/unrecognized-token-kind-is-unauthorized` | — | ### RS-5 (resource-server, MUST) -For owner tokens computes the effective filter as the permitted owner request filter alone; for client tokens in v0.1 rejects request-time predicate filters and enforces the frozen grant constraints. +For owner tokens, computes the effective filter as the permitted owner request filter alone (there is no grant filter). For client tokens in v0.1, rejects request-time predicate filters and enforces the frozen grant constraints. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.1-1a` | 1073 | MUST | black-box-http | `RS-9/client-token-exact-filter-rejected`
`RS-10/owner-filter-unknown-field-rejected` | Carries no RFC 2119 keyword here; its MUST level is Section 9 RS item 5's, and the enforceable half is restated with keywords at 8.9-2 and 8.9-9. The owner-side "cannot widen" property is not directly tested — that needs an owner filter naming a record outside the owner's scope. | +| `8.1-2` | 1965 | MUST | black-box-http | `RS-9/client-token-exact-filter-rejected`
`RS-10/owner-filter-unknown-field-rejected` | Carries no RFC 2119 keyword here; its MUST level is Section 9 RS item 5's, and the enforceable half is restated with keywords at 8.12-2 and 8.12-10. The owner-side "cannot widen" property is not directly tested — that needs an owner filter naming a record outside the owner's scope. | ### RS-6 (resource-server, MUST) -Returns structured errors as defined in the Section 8 unified error table. +Returns structured errors as defined in Section 8 (unified error table). | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.9-11` | 1309 | MUST | black-box-http | `RS-6/order-mismatched-cursor-rejected` | The resource-server half is covered. The client half — that a client follows a `next_cursor` with the same `order` and restarts pagination to change direction — binds the client's outgoing requests and stays `client-capture`: the adapter has no reverse channel onto a client under test. | +| `8.12-12` | 2247 | MUST | black-box-http | `RS-6/order-mismatched-cursor-rejected` | The resource-server half is covered. The client half — that a client follows a `next_cursor` with the same `order` and restarts pagination to change direction — binds the client's outgoing requests and stays `client-capture`: the adapter has no reverse channel onto a client under test. | ### RS-7 (resource-server, MUST) -Supports incremental sync via `changes_since` for `mutable_state` streams, including tombstones, omission of records whose grant-authorized projection did not change, and HTTP 410 `cursor_expired` on expiry. +Supports incremental sync via `changes_since` on every stream, starting from `beginning`, with tombstone entries and HTTP 410 with error code `cursor_expired` on cursor expiry. Returns every record not visible at h0 and visible at h1, and omits a record visible at both horizons whose projection did not change, unless the previous session withheld it, or redacted a reference in it, after a deletion. The projection is the grant-authorized projection for a client token, and the session's effective `fields` or resolved `view` projection for an owner token. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `4.3-1` | 247 | MAY | black-box-http | — | — | -| `4.3-2` | 247 | MUST | black-box-http | `RS-7/expired-sync-cursor-reported-as-gone` | — | -| `4.3-6` | 251 | MUST | black-box-http | `RS-7/deletion-surfaces-as-a-tombstone`
`RS-7/tombstone-envelope-survives-sparse-fields` | — | -| `4.3-7` | 268 | SHOULD | review-only | — | Not observable: whether the source deletion time was known is internal state, so a `deleted_at` value cannot be judged against this clause from outside. | -| `8.9-13` | 1313 | MUST | black-box-http | `RS-7/sync-eligibility-computed-on-the-authorized-projection` | — | -| `8.9-14` | 1315 | MUST | black-box-http | `RS-7/paginated-sync-session-anchored-to-one-horizon` | — | +| `4.4-4` | 313 | MUST | black-box-http | `RS-7/deletion-surfaces-as-a-tombstone` | Only the emission of a tombstone for a deleted record is observed. That no tombstone is emitted for any other record, and that a record moved out of the query context also yields one, is not tested. RS-7/tombstone-envelope-survives-sparse-fields is no longer mapped here: it requires `deleted_at` and `emitted_at` on every tombstone, and the locked text gives a client-token tombstone neither. | +| `4.4-10` | 321 | MAY | black-box-http | — | — | +| `4.4-11` | 321 | MUST | black-box-http | `RS-7/expired-sync-cursor-reported-as-gone` | — | +| `8.12-14` | 2251 | MUST | black-box-http | `RS-7/sync-eligibility-computed-on-the-authorized-projection` | — | +| `8.12-16` | 2255 | MUST | black-box-http | `RS-7/paginated-sync-session-anchored-to-one-horizon` | — | ### RS-8 (resource-server, MUST) -Returns `next_changes_since` on the terminal page of every `changes_since` response. +Returns `next_changes_since` on the terminal page of every `changes_since` response and on no other page. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `4.3-5` | 249 | MUST | black-box-http | `RS-8/terminal-page-carries-next-changes-since`
`RS-7/changes-since-accepted-on-multi-instance-grant` | — | -| `8.9-14` | 1315 | MUST | black-box-http | `RS-7/paginated-sync-session-anchored-to-one-horizon` | — | -| `8.9-15` | 1350 | MUST | black-box-http | `RS-8/terminal-page-carries-next-changes-since`
`RS-7/changes-since-accepted-on-multi-instance-grant` | — | +| `4.4-14` | 323 | MUST | black-box-http | `RS-8/terminal-page-carries-next-changes-since`
`RS-7/changes-since-accepted-on-multi-instance-grant` | — | +| `8.12-16` | 2255 | MUST | black-box-http | `RS-7/paginated-sync-session-anchored-to-one-horizon` | — | +| `8.12-19` | 2292 | MUST | black-box-http | `RS-8/terminal-page-carries-next-changes-since`
`RS-7/changes-since-accepted-on-multi-instance-grant` | — | ### RS-9 (resource-server, MUST) -Rejects client-token exact and range `filter[...]` parameters with 400 `invalid_request` before consulting current declaration metadata. +Rejects client-token exact and range `filter[...]` parameters with 400 `invalid_request` before consulting current declaration metadata; owner-token current-capability reads MAY retain declared filter behavior. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.9-2` | 1283 | MUST | black-box-http | `RS-9/client-token-exact-filter-rejected` | — | -| `8.9-3` | 1284 | MUST | black-box-http | `RS-9/client-token-view-rejected` | The client-token rejection half is covered. The sentence's second half — `view` and `fields` being mutually exclusive — is an owner-token concern (a client token must reject `view` outright, so the pair can never both be honoured there) and is tracked at 6.8-1, which needs a view-declaring target. | -| `8.9-4` | 1286 | MUST | black-box-http | `RS-9/client-token-expand-rejected` | — | -| `8.9-5` | 1286 | MUST | black-box-http | `RS-9/client-token-expand-limit-rejected` | — | -| `8.9-9` | 1296 | MUST | black-box-http | `RS-9/client-token-exact-filter-rejected` | The status and code are tested; the ordering half ("before the RS consults ... metadata") is not, and black-box observation cannot establish it. | -| `8.9-10` | 1301 | MUST | black-box-http | `RS-9/client-token-expand-rejected` | — | +| `8.12-2` | 2221 | MUST | black-box-http | `RS-9/client-token-exact-filter-rejected` | — | +| `8.12-3` | 2222 | MUST | black-box-http | `RS-9/client-token-view-rejected` | The client-token rejection half is covered. The sentence's second half — `view` and `fields` being mutually exclusive — is an owner-token concern (a client token must reject `view` outright, so the pair can never both be honoured there) and is tracked at 6.11-3, which needs a view-declaring target. | +| `8.12-5` | 2224 | MUST | black-box-http | `RS-9/client-token-expand-rejected` | — | +| `8.12-6` | 2224 | MUST | black-box-http | `RS-9/client-token-expand-limit-rejected` | — | +| `8.12-10` | 2234 | MUST | black-box-http | `RS-9/client-token-exact-filter-rejected` | The status and code are tested; the ordering half ("before the RS consults ... metadata") is not, and black-box observation cannot establish it. | +| `8.12-11` | 2239 | MUST | black-box-http | `RS-9/client-token-expand-rejected` | — | ### RS-10 (resource-server, MUST) @@ -420,63 +1064,242 @@ Rejects unknown query parameters and unsupported query shapes with 400 instead o | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.9-5` | 1286 | MUST | black-box-http | `RS-9/client-token-expand-limit-rejected` | — | -| `8.9-7` | 1292 | MUST | black-box-http | `RS-10/unknown-parameter-rejected`
`RS-10/unsupported-bracketed-shape-rejected`
`RS-10/owner-filter-unknown-field-rejected`
`RS-10/owner-expand-undeclared-relation-rejected`
`RS-10/malformed-limit-rejected` | — | -| `8.9-8` | 1294 | MUST | black-box-http | `RS-10/oversized-limit-clamped-with-warning` | — | +| `8.12-6` | 2224 | MUST | black-box-http | `RS-9/client-token-expand-limit-rejected` | — | +| `8.12-8` | 2230 | MUST | black-box-http | `RS-10/unknown-parameter-rejected`
`RS-10/unsupported-bracketed-shape-rejected`
`RS-10/owner-filter-unknown-field-rejected`
`RS-10/owner-expand-undeclared-relation-rejected`
`RS-10/malformed-limit-rejected` | — | +| `8.12-9` | 2232 | MUST | black-box-http | `RS-10/oversized-limit-clamped-with-warning` | — | ### RS-11 (resource-server, MUST) -Implements `PDPP-Version` header negotiation. +Implements API versioning: supports the exact `0.1` contract, selects it for an absent header, rejects malformed or unsupported values without fallback, reports the selected contract in responses, and publishes the matching default in protected resource metadata. Binds cursors and synchronization tokens to the selected contract and preserves all grant and profile enforcement independently of it. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `7.4-2` | 917 | MUST | black-box-http | `RS-11/unsupported-grant-schema-version-rejected` | — | -| `8.14-1` | 1458 | MUST | black-box-http | `RS-11/unsupported-version-rejected`
`AS-17/unsupported-version-rejected` | Carries no RFC 2119 keyword; its MUST level is Section 9 AS item 17 and RS item 11. The unsupported-version half is tested on both roles; the absent-header half — that the response echoes the selected version in a `PDPP-Version` response header — is not asserted by any case, and needs only an unwritten case, not a new hook. | +| `7.7-2` | 1473 | MUST | black-box-http | `RS-11/unsupported-grant-schema-version-rejected` | — | +| `8.17-1` | 2438 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-2` | 2440 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `8.17-3` | 2442 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-4` | 2442 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-5` | 2442 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-6` | 2444 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.17-7` | 2482 | MUST | black-box-http | `RS-11/unsupported-version-rejected` | The unsupported-version half is tested on the resource server. No case asserts that an absent header selects `0.1`, or that the response carries exactly one `PDPP-Version` field naming the selected identifier. AS-17/unsupported-version-rejected is no longer mapped here: the locked text makes `PDPP-Version` negotiation a Resource Server requirement and retires AS item 17. | +| `8.17-8` | 2482 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-9` | 2482 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-10` | 2484 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-11` | 2484 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-12` | 2486 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-13` | 2486 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-14` | 2486 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-15` | 2488 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.17-16` | 2488 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-17` | 2490 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-18` | 2492 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-19` | 2492 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.17-20` | 2494 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-21` | 2494 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.17-22` | 2498 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.17-23` | 2498 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | ### RS-12 (resource-server, MUST) -Scopes owner token access to a single subject's data store, deriving `subject_id` from the introspection response. +Scopes owner token access to a single subject's data store; derives `subject_id` from introspection response. Never discloses the introspection `subject_id` to a client. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.3-1` | 1105 | MUST | black-box-http | `RS-12/foreign-subject-cannot-read` | — | +| `8.4-4` | 2026 | MUST | black-box-http | `RS-12/foreign-subject-cannot-read` | — | ### RS-13 (resource-server, SHOULD) -SHOULD support owner-authenticated access to the record query endpoints without a client grant (self-export). +SHOULD support owner-authenticated access to the `/v1/streams/{stream}/records` query endpoints without a client grant, allowing the data subject to export their own data directly (self-export). | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.3-2` | 1109 | SHOULD | black-box-http | `RS-13/self-export-supported` | — | +| `8.4-5` | 2030 | SHOULD | black-box-http | `RS-13/self-export-supported` | — | ### RS-14 (resource-server, MUST) -For owner-token stream-metadata reads, returns the full current stream metadata within the owner's scope, including current query, view, and relationship capabilities. +For owner-token stream-metadata reads, returns the full current stream metadata within the owner's subject/source/connection scope, including current query, view, and relationship capabilities. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.9-6` | 1290 | MUST | black-box-http | `RS-14/owner-metadata-full-current-document` | — | +| `8.12-7` | 2228 | MUST | black-box-http | `RS-14/owner-metadata-full-current-document` | — | ### RS-15 (resource-server, MUST) -For client-token stream-metadata reads, returns only a projection derived from the resolved authorization context, excluding current view/relationship/filter/expansion/aggregation capability and any post-issuance declaration change. +For client-token stream-metadata reads, returns only a projection derived from the resolved authorization context: the granted stream and its explicitly granted fields, and immutable/frozen grant facts. MUST NOT include current view, relationship, filter, expansion, or aggregation capability unless that capability is explicitly part of a future frozen grant vocabulary, and MUST NOT surface a source-declaration change made after grant issuance. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `5.8-8` | 628 | MUST | black-box-http | `RS-15/client-metadata-projection-closed` | — | -| `8.1-2` | 1077 | MUST | black-box-http | `RS-15/client-metadata-projection-closed`
`RS-2/field-projection-not-exceeded` | — | -| `8.7-1` | 1186 | MUST | black-box-http | `RS-15/client-metadata-projection-closed` | — | +| `5.10-9` | 946 | MUST | black-box-http | `RS-15/client-metadata-projection-closed` | — | +| `8.1-3` | 1969 | MUST | black-box-http | `RS-15/client-metadata-projection-closed`
`RS-2/field-projection-not-exceeded` | — | +| `8.9-1` | 2110 | MUST | black-box-http | `RS-15/client-metadata-projection-closed` | — | ### RS-16 (resource-server, MUST) -Publishes RFC 9728 protected resource metadata at the RFC 9728 Section 3 location carrying `resource` and the four `pdpp_` members, and returns a `WWW-Authenticate: Bearer` challenge with `resource_metadata` on 401. +Publishes RFC 9728 protected resource metadata at the location RFC 9728 Section 3 derives from its resource identifier, carrying `resource`, the four required `pdpp_`-prefixed members defined in Section 8, and `authorization_servers` when its issuer set is enumerable. Also carries `pdpp_freshness_granularity_seconds` when it attaches `freshness` to client-token responses. Returns a `WWW-Authenticate: Bearer` challenge on 401 per RFC 6750 Section 3, carrying the RFC 9728 `resource_metadata` parameter. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.5-1` | 2034 | MUST | black-box-http | `RS-16/protected-resource-metadata-published` | — | +| `8.5-2` | 2036 | MUST | black-box-http | `RS-16/401-carries-resource-metadata-challenge`
`RS-16/unauthenticated-request-refused` | — | +| `8.5-8` | 2052 | SHOULD | black-box-http | — | — | +| `8.5-9` | 2054 | MUST | review-only | — | Whether the target's authorization-server set is enumerable is not declared anywhere the suite can read, so the presence or absence of the member cannot be judged; closing this needs a target-config field stating enumerability. | + +### RS-17 (resource-server, MUST) + +Does not interpret an unrecognized stream semantic as `append_only` or `mutable_state`, or an unrecognized grant `source.kind` as a known provenance class. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.4-1` | 1113 | MUST | black-box-http | `RS-16/protected-resource-metadata-published` | — | -| `8.4-2` | 1115 | MUST | black-box-http | `RS-16/401-carries-resource-metadata-challenge`
`RS-16/unauthenticated-request-refused` | — | -| `8.4-3` | 1128 | SHOULD | black-box-http | — | — | -| `8.4-4` | 1130 | MUST | review-only | — | Whether the target's authorization-server set is enumerable is not declared anywhere the suite can read, so the presence or absence of the member cannot be judged; closing this needs a target-config field stating enumerability. | +| `6.7-3` | 1112 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-18 (resource-server, MUST) + +Serves a blob only when a record the requesting token may currently read, including its field projection, references it. Otherwise returns 404 `blob_not_found`. Gives a redirect's signed URL an expiry no later than the positive-status cache expiry, or, when no result was cached, 60 seconds after the RS sent the introspection request that validated the token. The expiry is also no later than the token or grant expiration when present. A delayed response never restarts this deadline. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.15-4` | 2356 | MUST | black-box-http | `RS-1/blob-outside-grant-refused`
`RS-1/blob-unauthenticated-refused` | — | +| `8.15-8` | 2376 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.15-9` | 2376 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-19 (resource-server, MUST) + +Returns 401 with `WWW-Authenticate: Bearer error="invalid_token"` for every inactive access token. Uses the structured code `grant_expired` or `grant_revoked` only when authenticated context establishes that cause, otherwise `authentication_error`. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.16-6` | 2434 | MAY | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-13` | 2787 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### RS-20 (resource-server, MUST) + +Emits a tombstone for each record visible at the previous horizon and not visible at the new one, and for no other record. Retains the state needed to determine visibility at every unexpired horizon, and otherwise fails the session with 410 `cursor_expired`. Uses the exact client tombstone shape for client tokens. For owner tokens, adds `removal`, and `deleted_at` and `emitted_at` only for a deletion. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.4-4` | 313 | MUST | black-box-http | `RS-7/deletion-surfaces-as-a-tombstone` | Only the emission of a tombstone for a deleted record is observed. That no tombstone is emitted for any other record, and that a record moved out of the query context also yields one, is not tested. RS-7/tombstone-envelope-survives-sparse-fields is no longer mapped here: it requires `deleted_at` and `emitted_at` on every tombstone, and the locked text gives a client-token tombstone neither. | +| `4.4-6` | 315 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-21 (resource-server, MUST) + +Binds each page cursor and `changes_since` token to its grant, or owner subject and scope, and to its query context, and rejects a mismatch as `invalid_cursor`. Orders a `changes_since` session by `instance_id` and then canonical key string, does not serve a record deleted after the session's first page, and rejects `expand[]` and `expand_limit[...]` on `changes_since`. Issues tokens and page cursors that reveal no readable position, time, or field value, and whose equality or ordering signals no change outside the grant or owner scope. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.4-3` | 294 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.12-15` | 2253 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-22 (resource-server, MUST) + +Compares time values and bounds by the grant's frozen `time_constraint.type`, never a current declaration, as calendar dates or exact instants, without a host, viewer, or owner time zone. Validates values itself under RFC 3339, including its leap-second rule. Excludes a record whose value is absent, null, or invalid from every read surface. Applies the same rules to owner-token range filters on `date` and `date-time` fields, using the declared format. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.10-1` | 1508 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-23 (resource-server, MUST) + +Puts `instance_id` on every record and tombstone it serves: the grant's handle for a client token, and the owner's handle for an owner token. Serves each `resource_ref` in the resolved or redacted form according to the token-kind, source, and sync-horizon rules in Section 4. In the next session, returns each referring record that the previous session served with a reference redacted after a target deletion, as Section 4 requires. Identifies records within a stream by instance and canonical key in sync, removals, cursors, and single-record reads. Requires `instance_id` on a single-record read or record delete when the query context covers more than one instance of the stream, and on every blob fetch. Rejects a missing one with 400 `invalid_request`. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.11-7` | 601 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.13-2` | 2336 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.13-3` | 2336 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-24 (resource-server, MUST) + +In a separated deployment, resolves every handle through `pdpp_instance_bindings`. Fails a client request with 403 `grant_invalid` when a handle the request needs has no binding or names no stored instance. Treats an owner instance without a binding as outside the owner scope. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.3-17` | 2014 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | + +### RS-25 (resource-server, MUST) + +When it attaches `freshness`, lists exactly one entry for each instance that the query context authorizes for the stream, whatever the page contains. Computes `last_success_at` over the full authorized scope of the query, counts only successful operations as Section 8 defines them, and reports null when it cannot establish relevance safely. In a client-token response, carries no failure time, truncates each value to its declared granularity of at least 3600 seconds, and reports no value before its interval ends. Takes a snapshot of the array on the first page of a `changes_since` session and repeats that exact snapshot on every later page. Reports no staleness verdict. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.10-1` | 2175 | MAY | black-box-http | — | — | +| `8.10-4` | 2195 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-26 (resource-server, MUST) + +Builds disclosed `data` only from the top-level members the grant and the request-time field selection permit, with no nulls or fabricated values for withheld members. Refuses a read it cannot serve that way with 403 `disclosure_unavailable`. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.3-2` | 284 | MUST | black-box-http | `RS-2/field-projection-not-exceeded` | The case reads a field-narrowed grant and checks that no returned record carries a member outside the grant's allowlist. It does not exercise a request-time field selection narrower than the grant. | +| `4.3-7` | 288 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-27 (resource-server, MUST) + +Enforces the resolved grant independently of provider action permission, as Existing permissions and action results defines. Never treats a data grant as action permission or lets a broad read scope widen a grant, and discloses existing data in an action response only under the applicable data grant. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.2-1` | 1975 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-2` | 1977 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-3` | 1977 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-4` | 1979 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-5` | 1979 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-6` | 1981 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-7` | 1983 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-8` | 1983 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-9` | 1985 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-10` | 1985 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.2-11` | 1985 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-28 (resource-server, MUST) + +Delivers an `evidence` item only when it implements the item's type, the proof is valid over the served values and authenticates the attester, the item's record binding verifies for the caller, and the item does not widen the disclosure. Decides per record whether to deliver an item only when that decision is independent of hidden data. Adds no member to an item, delivers no item with an unresolved local reference, never passes an unchecked item through, and marks no item as verified or unverified. Sends no evidence on a tombstone, and does not return a record in a `changes_since` session only because its evidence changed. Delivers evidence on owner-token reads only for types whose profile supports them. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.7-27` | 457 | MAY | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-28` | 459 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | + +### RS-29 (resource-server, MUST) + +Never treats `processing_permissions` or a processing credential as read authority. Its operator does not use owner data it holds in the RS role for AI training, except under a training grant issued to it as a client. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.9-6` | 1164 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### RS-30 (resource-server, MUST) + +Validates each StreamGrant's explicit `required_fields` and its containment in `fields` before disclosure. Rejects missing or malformed context with 403 `grant_invalid`. Applies the frozen floor to every client record-data projection and its evidence without declaration lookup. Preserves tombstone shape and existing sync eligibility rules. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.7-13` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-31 (resource-server, MUST) + +For a client request whose explicit `fields` contains both granted and ungranted names, returns 403 `field_not_granted` and no records. The error names only the ungranted names. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.7-15` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-32 (resource-server, MUST) + +Given the same request and grant, returns the same status, headers, and body, apart from `request_id`, `Request-Id`, and `Date`, when the same well-formed outside-grant name exists in the current schema and when it is invented. It performs no source-schema or record-data lookup before rejection. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.7-15` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-18` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### RS-33 (resource-server, MUST) + +For a client list read whose grant omits the declared date `cursor_field`, uses descending `(instance_id, canonical key)` order by default, even when date order differs. When the grant includes the field, it uses the frozen `sort_field`. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `5.6-1` | 868 | MUST | declaration-static | — | No Section 9 item covers it, so `requirementIds` stays empty. Core explicitly permits streams that cannot define a stable `consent_time_field` to omit it, so the former negative fixture (`time_range_capable` with no consent field) did not violate this clause. A valid black-box fixture would need to observe the AS deriving a consent boundary from `cursor_field`; the declaration submission surface exposes neither that derived state nor a normative capability member. | ### CL-1 (client, MUST) @@ -484,7 +1307,7 @@ Submits selection requests using the RFC 9396 `authorization_details` envelope. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.8-4` | 801 | SHOULD | client-capture | — | — | +| `6.11-21` | 1210 | SHOULD | client-capture | — | — | ### CL-2 (client, MUST) @@ -492,62 +1315,430 @@ Uses access tokens (not raw grants) to authenticate with the resource server. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `10.7-2` | 1595 | MUST | client-capture | `CL-2/grant-revocation-stops-further-requests` | — | +| `10.8-14` | 2787 | MUST | client-capture | `CL-2/grant-revocation-stops-further-requests` | — | ### CL-3 (client, MUST) -Treats `cursor` and `changes_since` tokens as opaque and from distinct token spaces; MUST NOT use a `next_cursor` value as a `changes_since` parameter. +Treats `cursor` and `changes_since` tokens as opaque and from distinct token spaces. MUST NOT use a `next_cursor` value as a `changes_since` parameter. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `4.3-4` | 249 | MUST | client-capture | `CL-3/changes-since-does-not-reuse-next-cursor` | — | -| `8.9-1` | 1281 | MUST | client-capture | `CL-3/forwards-cursors-as-opaque-values` | — | -| `8.9-11` | 1309 | MUST | black-box-http | `RS-6/order-mismatched-cursor-rejected` | The resource-server half is covered. The client half — that a client follows a `next_cursor` with the same `order` and restarts pagination to change direction — binds the client's outgoing requests and stays `client-capture`: the adapter has no reverse channel onto a client under test. | +| `4.4-13` | 323 | MUST | client-capture | `CL-3/changes-since-does-not-reuse-next-cursor` | — | +| `8.12-1` | 2219 | MUST | client-capture | `CL-3/forwards-cursors-as-opaque-values` | — | +| `8.12-12` | 2247 | MUST | black-box-http | `RS-6/order-mismatched-cursor-rejected` | The resource-server half is covered. The client half — that a client follows a `next_cursor` with the same `order` and restarts pagination to change direction — binds the client's outgoing requests and stays `client-capture`: the adapter has no reverse channel onto a client under test. | ### CL-4 (client, MUST) -Stores `next_changes_since` from the terminal page of a `changes_since` response for the next sync session. +Stores `next_changes_since` from the terminal page of a `changes_since` response for use in the next sync session. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.9-16` | 249 | MUST | client-capture | `CL-4/stores-terminal-next-changes-since` | — | +| `4.4-16` | 323 | MUST | client-capture | `CL-4/stores-terminal-next-changes-since` | The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's. | ### CL-5 (client, MUST) -Respects HTTP 410 `cursor_expired` by performing a full re-sync rather than retrying with the expired cursor. +Respects HTTP 410 `cursor_expired` responses by starting again from `beginning`, rather than retrying with the expired token. After every completed `beginning` session, replaces its copy of the stream synchronized under that grant and query context with that session's result. Applies each returned record as a replacement of its copy within the same projection, never as a partial update: omission of an optional field inside that projection removes it, and a present `null` is a stored null. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `4.3-3` | 247 | MUST | client-capture | `CL-5/full-resync-after-cursor-expired` | — | -| `8.9-12` | 1311 | MUST | client-capture | `CL-5/endpoint-cursor-expiry-also-full-resyncs` | — | +| `4.4-12` | 321 | MUST | client-capture | `CL-5/full-resync-after-cursor-expired` | — | +| `8.12-13` | 2249 | MUST | client-capture | `CL-5/endpoint-cursor-expiry-also-full-resyncs` | — | ### CL-6 (client, MUST) -Honors retention commitments declared in the grant. +Honors retention commitments declared in the grant. Counts each record's retention ceiling from its first acquisition, never restarts it, and applies `on_expiry` at the ceiling to the record and its derivatives with no grace period. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `7.10-1` | 957 | MUST | review-only | — | The spec states outright that PDPP does not technically enforce retention, so no protocol test can exist; Section 9 CL item 6 states it as a client MUST, and the only available evidence is contractual rather than executable. | +| `7.24-1` | 1833 | MUST | review-only | — | The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's. The spec states outright that PDPP does not technically enforce retention, so no protocol test can exist; Section 9 CL item 6 states it as a client MUST, and the only available evidence is contractual rather than executable. | ### CL-7 (client, MUST) -Treats unrecognized error codes as opaque, falling back to the actual HTTP status code and headers, and never fails to parse on an unknown `code` or `type`. +Treats unrecognized error codes as opaque, falling back to the exact HTTP status code and applicable response headers rather than failing on an unknown code. Takes the actual status code and headers as the authoritative outcome. Uses a recognized `error.type` only to refine category or presentation, and only when compatible with that outcome. Ignores an absent, unrecognized, or status-incompatible `type` for control flow, and never fails to parse on an unknown `code` or `type`. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `8.13-1` | 1415 | MUST | client-capture | `CL-7/unknown-error-code-keeps-http-status-authoritative` | — | -| `8.13-2` | 1420 | MAY | client-capture | — | — | -| `8.13-3` | 1421 | MUST | client-capture | `CL-7/status-incompatible-code-does-not-override-status` | — | -| `8.13-4` | 1422 | MUST | client-capture | `CL-7/unknown-error-identifiers-do-not-break-parser` | — | -| `8.13-5` | 1424 | MAY | client-capture | — | — | +| `8.16-1` | 2398 | MUST | client-capture | `CL-7/unknown-error-code-keeps-http-status-authoritative` | — | +| `8.16-2` | 2403 | MAY | client-capture | — | — | +| `8.16-3` | 2404 | MUST | client-capture | `CL-7/status-incompatible-code-does-not-override-status` | — | +| `8.16-4` | 2405 | MUST | client-capture | `CL-7/unknown-error-identifiers-do-not-break-parser` | — | +| `8.16-5` | 2407 | MAY | client-capture | — | — | ### CL-8 (client, MUST) -Reads `source.kind` from the issued grant and applies provenance policy before first use of the records; MUST NOT assume a provenance class it did not read from the grant. +Where local policy depends on source provenance, MUST read `source.kind` from the issued grant and apply that policy before first use of the records. A client MUST NOT assume a provenance class it did not read from the grant, and MUST NOT treat an unrecognized `source.kind` as either known value. A client with no provenance-dependent policy has nothing to check. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.7-1` | 1110 | MUST | client-capture | `CL-8/reads-source-kind-from-grant` | The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's. | + +### CL-9 (client, MUST) + +Stops using an access token on a 401 with Bearer error `invalid_token`, and stops requests against a grant on `grant_revoked`. Does not treat an inactive token as proof of revocation. Starts a grant lifecycle query before further use of held data of that grant. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `10.8-13` | 2787 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `10.8-15` | 2787 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `10.8-16` | 2787 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### CL-10 (client, MUST) + +Identifies each record within a stream by its `instance_id` and `id` together. Treats a tombstone for a record it does not hold as a no-op. Reconciles records it obtained outside sync sessions itself, because the sync guarantee covers only a copy built from sync sessions. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.4-8` | 317 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `4.6-11` | 408 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-11 (client, MUST) + +Does not infer from an empty result, the end of pagination, a tombstone, or a 404 that a record does not exist at the source or that collection is complete. Treats a null `last_success_at` only as the absence of a reportable timestamp. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.11-1` | 2203 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-12 (client, MUST) + +Establishes that the AS advertises `https://pdpp.dev/data-access` before requesting it. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.4-1` | 2022 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### CL-13 (client, MUST) + +Inspects the authorization result in each token response, including refresh responses, before requesting record disclosure, and requests no disclosure when it cannot obtain or interpret the result. Does not infer authorization from its original request, a tool listing, or token syntax. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.3-6` | 1419 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.3-7` | 1419 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### CL-14 (client, MUST) + +Does not validate a projected `data` object as a full stream record. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.3-4` | 284 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-15 (client, MUST) + +Ignores any member that it does not recognize in a Core-defined JSON object that a Section 8 endpoint returns. Does not fail to parse because of that member, as Response forward compatibility requires. This rule does not reach inside profile-defined objects such as an evidence item's `proof`; their profiles define parsing and verification failure. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.6-1` | 2060 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.6-2` | 2060 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### CL-16 (client, MUST) + +Treats an evidence item as absent unless it verifies the item under the profile for its type. Ignores an item with an unknown `type` or `attester.role`. Treats a local reference to an item that the response does not carry as absent, and retrieves a URI reference only as the type's profile defines. Infers record provenance from an item only when it has verified the item's binding to its own record context. For a composed binding, it also verifies the whole chain and trusts the mapping attester. Does not treat delivery as verification, and decides itself whether to trust the attester. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `4.7-31` | 481 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | + +### CL-17 (client, MUST) + +Trains on PDPP inputs only when every contributing PDPP input has covering training permission and valid processing authority, as AI training defines, regardless of purpose code or acquisition grant. Applies this rule to derivatives and imposes it on every recipient or subprocessor receiving PDPP inputs. Treats the legacy purpose code `https://pdpp.dev/purpose/ai_training` as conferring no training authority. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.9-1` | 1164 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.9-2` | 1164 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.9-3` | 1164 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `6.9-4` | 1164 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.9-5` | 1164 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `6.9-6` | 1164 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.9-7` | 1166 | MUST | black-box-http | — | No case in this suite version exercises this clause. | + +### CL-18 (client, MUST) + +Requests a processing permission only when the AS advertises it in `pdpp_processing_permissions_supported`, in a detail separate from any ordinary access, and with a `purpose_description` where Section 6 requires one. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.8-19` | 1154 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### CL-19 (client, MUST) + +After a grant ends, uses its held data only within the grant's surviving-use terms, including only the limited continuation of already-issued processing authority permitted by the defining profile. Otherwise keeps, but does not use, data held only under a grant that carries a processing permission. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.13-2` | 1543 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-28` | 1652 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### CL-20 (client, MUST) + +Holds a positive assessment before first use of a grant's held data. Never treats record delivery, introspection, a read error, or a package read status as an assessment. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.17-4` | 1597 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### CL-21 (client, MUST) + +While it runs and it or a party acting for it holds held data of a grant, attempts a lifecycle query at least once per reconciliation interval, starting one interval after first acquisition. Retries failed or unanswered attempts as Positive assessment and reconciliation requires, including its `Retry-After` and transport-failure widening rules, until an attempt succeeds or it holds no held data of the grant. Also starts a query before further use after a failure that shows the grant may have ended, and after resuming from suspension or power-off if an attempt fell due meanwhile. Does not treat a failed or unanswered query as a reason to stop use the grant's terms permit. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.17-1` | 1593 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.17-2` | 1593 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.17-3` | 1595 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.17-4` | 1597 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.17-5` | 1599 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.17-6` | 1599 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.17-7` | 1601 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.17-8` | 1608 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.17-9` | 1608 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### CL-22 (client, MUST) + +On learning of an erasure instruction, stops use as soon as practicable and no later than the stop-use bound after its effective time, or at once if it learns of the instruction after that deadline. Deletes the covered data, with the scope and by the deadline Erasure defines. Records its first receipt, never restarts a deadline, and treats the instruction as terminal. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.18-1` | 1612 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-2` | 1614 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-3` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-4` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-5` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-6` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-7` | 1633 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-8` | 1635 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-9` | 1640 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-10` | 1640 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-11` | 1642 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-12` | 1644 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-13` | 1644 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-14` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-15` | 1646 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-16` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-17` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-18` | 1648 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-19` | 1650 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-20` | 1650 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.18-21` | 1650 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-22` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-23` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-24` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-25` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-26` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-27` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-28` | 1652 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-29` | 1652 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-30` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-31` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-32` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.19-1` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-2` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-3` | 1658 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-4` | 1660 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-5` | 1662 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-6` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-7` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-1` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-2` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-3` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-4` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-5` | 1674 | MAY | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-6` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-7` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-8` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-9` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-23 (client, MUST) + +Applies the overdue-disposal rules in Erasure before further use. Treats backups and legally retained data as Erasure defines. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.18-1` | 1612 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-2` | 1614 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-3` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-4` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-5` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-6` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-7` | 1633 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-8` | 1635 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-9` | 1640 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-10` | 1640 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-11` | 1642 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-12` | 1644 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-13` | 1644 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-14` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-15` | 1646 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-16` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-17` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-18` | 1648 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-19` | 1650 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-20` | 1650 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.18-21` | 1650 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-22` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-23` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-24` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-25` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-26` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-27` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-28` | 1652 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-29` | 1652 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-30` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-31` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-32` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.19-1` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-2` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-3` | 1658 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-4` | 1660 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-5` | 1662 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-6` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-7` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-1` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-2` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-3` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-4` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-5` | 1674 | MAY | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-6` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-7` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-8` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-9` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-24 (client, MUST) + +Imposes the held-data duties on every party acting for it, and relays erasure instructions with absolute deadlines to it, as Downstream holders defines. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.19-1` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-2` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-3` | 1658 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-4` | 1660 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-5` | 1662 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-6` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-7` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-25 (client, MUST) + +If it offers its own disconnect, states what happens to held data and offers `keep` and `delete` with neither preselected. Treats `delete` as an erasure instruction effective at once. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.16-1` | 1582 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | + +### CL-26 (client, MUST) + +Keeps the records that Erasure requires. Exercises already-issued processing authority after effective erasure only within that section's limited continuation and the defining profile's stop requirements. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.18-1` | 1612 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-2` | 1614 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-3` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-4` | 1623 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-5` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-6` | 1633 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-7` | 1633 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-8` | 1635 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-9` | 1640 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-10` | 1640 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-11` | 1642 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.18-12` | 1644 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-13` | 1644 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-14` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-15` | 1646 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.18-16` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-17` | 1646 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-18` | 1648 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-19` | 1650 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-20` | 1650 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.18-21` | 1650 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-22` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-23` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-24` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-25` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-26` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-27` | 1652 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-28` | 1652 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.18-29` | 1652 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.18-30` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-31` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.18-32` | 1654 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.19-1` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-2` | 1658 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-3` | 1658 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-4` | 1660 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-5` | 1662 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.19-6` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.19-7` | 1662 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-1` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-2` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-3` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-4` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-5` | 1674 | MAY | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-6` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-7` | 1674 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.20-8` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.20-9` | 1676 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-27 (client, MUST) + +Sends a non-empty `purpose_description` that the owner can understand with any purpose code outside Appendix A. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `6.6-3` | 1091 | MUST | review-only | — | The AS half is a display obligation on the consent surface; the client half is a SHOULD needing client capture. Neither is reachable from this suite's seam. | + +### CL-28 (client, MUST) + +Retains the authenticated AS issuer with every grant and does not infer it from `grant_id`, `source.id`, or an untrusted credential, as Section 8 requires. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.5-10` | 2054 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-29 (client, MUST) + +Discloses PDPP data to an independent recipient only under an expressly covering registered processing permission and valid authority under its defining profile, except for the required-by-law case in Section 7. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.20-2` | 1668 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### CL-30 (client, MUST) + +Classifies a receiving party by the operation it performs. Does not treat independent use as processing for the client. Applies the rule to derivatives and service outputs as well as source records. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.19-4` | 1660 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-31 (client, MUST) + +Does not infer independent-recipient disclosure authority from purpose, an owner action instruction, a claim, a positive assessment, an erasure exception, or read-token activity. Records any required-by-law disclosure as Section 7 requires. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `7.20-3` | 1672 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | + +### CL-32 (client, MUST) + +Pins a supported HTTP contract with `PDPP-Version` on each Core RS operation, including retries and pagination, and verifies the selected version before using a successful response. Does not infer a contract from a URL path, grant schema, document release, or profile version, and does not silently fall back after a version failure. + +| Clause | Spec line | Level | Observable | Cases | Gap | +| --- | --- | --- | --- | --- | --- | +| `8.5-7` | 2050 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.17-15` | 2488 | MUST | client-capture | — | No case in this suite version exercises this clause. | + +### CL-33 (client, MUST) + +Reads each StreamGrant's `required_fields` from the authorization result before requesting disclosure. Expects those fields in every returned record's `data` when the stored record version has a value for them, and does not infer the set from a declaration. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `6.6-1` | 770 | MUST | client-capture | `CL-8/reads-source-kind-from-grant` | — | +| `4.3-1` | 282 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | ## Clauses no Section 9 item covers @@ -561,27 +1752,227 @@ reports. | Clause | Spec line | Level | Observable | Cases | Gap | | --- | --- | --- | --- | --- | --- | -| `3.0-1` | 194 | MUST | review-only | — | Binds the personal server's read of an upstream platform, a hop outside the Core HTTP surface a client can reach; no Section 9 item names it and no black-box hook exposes the upstream request. | -| `3.0-2` | 196 | MUST | review-only | — | Onward transfer of a token happens outside the protocol exchange; a client-capture fixture sees only the client's own requests, and no Section 9 client item names this obligation. | -| `4.5-1` | 310 | MUST | black-box-http | `RS-1/compound-primary-key-canonically-encoded` | — | -| `4.5-2` | 312 | MUST | review-only | — | The rejection half binds a write interface the Collection Profile defines, which Core's read-only query surface cannot reach; the read half could be checked but no Section 9 item claims it. | -| `4.6-1` | 319 | SHOULD | declaration-static | — | — | -| `4.8-1` | 372 | MUST | black-box-http | `RS-1/blob-ref-mime-type-valid` | The value lives in a record's `data.blob_ref`, so the case reads served records for the blob fixture's grant. It judges media-type STRUCTURE only: a registered top-level type or a well-formed `x-` token, a `/`, and a subtype made of RFC 6838 restricted-name characters (parameters are not validated). It does not consult the live IANA registry, so a well-formed but unregistered subtype passes, and it observes only the records that fixture exposes. A `mime_type` that is present but not a string fails; a `blob_ref` with no `mime_type` skips. No Section 9 item covers it, so `requirementIds` stays empty; the case is filed under RS-1 because the blob fixture is its surface. | -| `5.2-2` | 515 | MUST | declaration-static | `AS-16/key-field-not-declared-in-schema-refused` | No Section 9 item covers declaration internal consistency, so `requirementIds` stays empty; the case is filed under AS-16 because declaration acceptance is the surface it observes. | -| `5.2-3` | 518 | MUST | declaration-static | `AS-16/consent-time-field-not-declared-in-schema-refused` | No Section 9 item covers `consent_time_field` declaration validity, so `requirementIds` stays empty; the case is filed under AS-16 because declaration acceptance is the surface it observes. | -| `5.3-1` | 534 | MAY | declaration-static | — | — | -| `5.4-1` | 560 | MUST | declaration-static | — | No Section 9 item covers it, so `requirementIds` stays empty. Core explicitly permits streams that cannot define a stable `consent_time_field` to omit it, so the former negative fixture (`time_range_capable` with no consent field) did not violate this clause. A valid black-box fixture would need to observe the AS deriving a consent boundary from `cursor_field`; the declaration submission surface exposes neither that derived state nor a normative capability member. | -| `5.6-1` | 598 | MAY | declaration-static | — | — | -| `5.6-3` | 602 | MUST | black-box-http | `AS-13/unrecognized-view-uri-treated-as-opaque` | — | -| `6.1-3` | 700 | MAY | black-box-http | — | — | -| `6.2-1` | 716 | MAY | black-box-http | — | — | -| `7.4-1` | 913 | MUST | review-only | — | A design constraint on implementations rather than a checkable wire behaviour; its observable consequences are the three axis rows, recorded separately as 7.4-2, AS-17, and RS-11. | -| `7.8-1` | 949 | SHOULD | review-only | — | — | -| `8.8-1` | 1257 | MAY | black-box-http | — | — | -| `10.2-9` | 1567 | SHOULD | review-only | — | The spec marks this subsection non-normative guidance; recorded so the scan's keyword hit is accounted for rather than appearing as an untracked clause. | -| `10.4-1` | 1577 | MUST | review-only | — | Binds the Collection Profile's INTERACTION channel, which Core's HTTP surface does not expose; logs and persisted state are outside what a black-box client can read. | -| `10.5-1` | 1581 | SHOULD | review-only | — | — | -| `12.3-1` | 1671 | MUST | review-only | — | Whether a stream name "encodes predicate logic" is a semantic judgement no static check settles. The paragraph is headed "(non-normative)" yet carries a MUST NOT; recorded at the keyword's level and flagged for the spec lock. | -| `12.4-1` | 1677 | MUST | review-only | — | Needs a target that offers an extension capability and a hook that enables it; the suite has no such fixture. Missing case: a grant's reachable data is identical with the extension enabled and disabled. | -| `12.4-2` | 1677 | MUST | black-box-http | — | No case yet checks that each capability a target exercises is advertised in its declared metadata. Missing case: cross-check stream metadata and protected resource metadata against the capabilities the target answers. | -| `12.4-3` | 1677 | MUST | client-capture | — | Needs a client fixture response that advertises an unknown capability; no Section 9 client item names it. Missing case: the client under test completes its flow against metadata carrying an undeclared capability. | +| `2.5-1` | 143 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `3.0-1` | 231 | MUST | review-only | — | Binds the personal server's read of an upstream platform, a hop outside the Core HTTP surface a client can reach; no Section 9 item names it and no black-box hook exposes the upstream request. | +| `3.0-2` | 233 | MUST | review-only | — | Onward transfer of a token happens outside the protocol exchange; a client-capture fixture sees only the client's own requests, and no Section 9 client item names this obligation. | +| `4.2-1` | 274 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `4.2-2` | 274 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.2-3` | 278 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.2-4` | 278 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.3-3` | 284 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.3-5` | 286 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `4.3-6` | 286 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.3-8` | 288 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.4-1` | 294 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.4-2` | 294 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.4-5` | 313 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.4-7` | 315 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.4-9` | 319 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `4.4-15` | 323 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-1` | 398 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-2` | 402 | MUST | black-box-http | `RS-1/compound-primary-key-canonically-encoded` | — | +| `4.6-3` | 402 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-4` | 402 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-5` | 402 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-6` | 402 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.6-7` | 402 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-8` | 402 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-9` | 404 | MUST | review-only | — | The rejection half binds a write interface the Collection Profile defines, which Core's read-only query surface cannot reach; the read half could be checked but no Section 9 item claims it. | +| `4.6-10` | 408 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.6-12` | 412 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `4.6-13` | 412 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.7-1` | 416 | MAY | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-2` | 423 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-3` | 424 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.7-4` | 427 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-5` | 437 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.7-6` | 441 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-7` | 441 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-8` | 443 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.7-9` | 443 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `4.7-10` | 445 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `4.7-11` | 447 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-12` | 447 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.7-14` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-16` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-17` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-19` | 449 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-20` | 451 | MUST | black-box-http | `RS-2/field-projection-not-exceeded` | The case reads a field-narrowed grant and checks that no returned record carries a field outside the grant's allowlist. It does not exercise a request-time `fields` selection or an `expand`ed relation. | +| `4.7-21` | 451 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-22` | 453 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-23` | 453 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-24` | 455 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-25` | 455 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-26` | 455 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-29` | 466 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.7-30` | 470 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `4.8-1` | 513 | SHOULD | declaration-static | — | — | +| `4.10-1` | 566 | MUST | black-box-http | `RS-1/blob-ref-mime-type-valid` | The value lives in a record's `data.blob_ref`, so the case reads served records for the blob fixture's grant. It judges media-type STRUCTURE only: a registered top-level type or a well-formed `x-` token, a `/`, and a subtype made of RFC 6838 restricted-name characters (parameters are not validated). It does not consult the live IANA registry, so a well-formed but unregistered subtype passes, and it observes only the records that fixture exposes. A `mime_type` that is present but not a string fails; a `blob_ref` with no `mime_type` skips. No Section 9 item covers it, so `requirementIds` stays empty; the case is filed under RS-1 because the blob fixture is its surface. | +| `4.11-1` | 589 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.11-2` | 589 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.11-3` | 599 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `4.11-4` | 601 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.11-5` | 601 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `4.11-6` | 601 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.11-8` | 601 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `4.11-9` | 601 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `5.3-2` | 781 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.3-3` | 787 | MUST | declaration-static | `AS-16/key-field-not-declared-in-schema-refused` | No Section 9 item covers declaration internal consistency, so `requirementIds` stays empty; the case is filed under AS-16 because declaration acceptance is the surface it observes. | +| `5.3-4` | 789 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.3-5` | 790 | MUST | declaration-static | `AS-16/consent-time-field-not-declared-in-schema-refused` | The locked text adds that the field's schema format is `date` or `date-time`; the case checks only that the field is declared in the schema. No Section 9 item covers `consent_time_field` declaration validity beyond AS-27, so the case stays filed under AS-16, the surface it observes. | +| `5.3-6` | 791 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.3-9` | 800 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.3-10` | 802 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `5.4-1` | 811 | MAY | declaration-static | — | — | +| `5.4-4` | 817 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-2` | 875 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-3` | 875 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `5.6-4` | 877 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-6` | 877 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-9` | 882 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-10` | 883 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-11` | 883 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `5.6-13` | 885 | SHOULD | declaration-static | — | No case in this suite version exercises this clause. | +| `5.8-1` | 916 | MAY | declaration-static | — | — | +| `5.8-4` | 920 | MUST | black-box-http | `AS-13/unrecognized-view-uri-treated-as-opaque` | — | +| `5.10-8` | 946 | MUST | declaration-static | — | No case in this suite version exercises this clause. | +| `6.0-1` | 956 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.1-3` | 1023 | MAY | black-box-http | — | — | +| `6.1-8` | 1031 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `6.2-1` | 1039 | MAY | black-box-http | — | — | +| `6.6-1` | 1090 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.6-4` | 1091 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.6-5` | 1099 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `6.6-6` | 1099 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.7-2` | 1112 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.7-4` | 1112 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `6.7-5` | 1112 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `6.8-2` | 1126 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-4` | 1144 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `6.8-5` | 1146 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `6.8-6` | 1146 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `6.8-7` | 1146 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.8-8` | 1148 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.8-9` | 1148 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `6.8-10` | 1148 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-11` | 1148 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.8-12` | 1148 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.8-13` | 1150 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.8-14` | 1150 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-15` | 1150 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-16` | 1150 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `6.8-17` | 1150 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-18` | 1150 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.8-20` | 1154 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-21` | 1154 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-22` | 1154 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-23` | 1154 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-24` | 1156 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.8-25` | 1156 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.11-1` | 1183 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-2` | 1183 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-4` | 1188 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-5` | 1190 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-6` | 1192 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.11-7` | 1192 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-8` | 1192 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-9` | 1192 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-10` | 1194 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-11` | 1194 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-12` | 1194 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `6.11-13` | 1198 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-15` | 1202 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-16` | 1202 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-17` | 1202 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-18` | 1202 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `6.11-20` | 1210 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.0-2` | 1333 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.0-3` | 1333 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `7.2-3` | 1405 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.2-4` | 1405 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.2-5` | 1407 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.2-8` | 1411 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.2-9` | 1411 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.2-11` | 1411 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.2-12` | 1411 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.2-13` | 1411 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.2-14` | 1411 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.4-2` | 1429 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.4-3` | 1429 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.4-4` | 1433 | MUST | review-only | — | The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe. | +| `7.4-5` | 1433 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.4-6` | 1435 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.7-1` | 1469 | MUST | review-only | — | A design constraint on implementations rather than a checkable wire behaviour; its observable consequences are the three axis rows, recorded separately as 7.7-2, AS-17, and RS-11. | +| `7.8-1` | 1483 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.8-2` | 1483 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.8-3` | 1483 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.10-2` | 1510 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.10-3` | 1512 | SHOULD | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.12-1` | 1528 | SHOULD | review-only | — | — | +| `7.13-1` | 1540 | MAY | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.14-1` | 1558 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.14-2` | 1558 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.14-3` | 1558 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.14-4` | 1558 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `7.14-5` | 1558 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.14-6` | 1558 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.16-2` | 1582 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.24-3` | 1847 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `7.24-4` | 1847 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `7.24-5` | 1849 | MUST | review-only | — | The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes. | +| `7.24-6` | 1849 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.0-1` | 1951 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-2` | 1997 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `8.3-3` | 2002 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-4` | 2004 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-5` | 2004 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-6` | 2004 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-7` | 2004 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-9` | 2008 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-10` | 2008 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-11` | 2008 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-12` | 2010 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-13` | 2010 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-14` | 2010 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-15` | 2012 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-16` | 2012 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `8.3-18` | 2014 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.3-20` | 2016 | MUST | review-only | — | The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe. | +| `8.4-2` | 2022 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.4-3` | 2022 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.5-3` | 2040 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.5-4` | 2040 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.5-5` | 2047 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.5-6` | 2050 | MUST | client-capture | — | No case in this suite version exercises this clause. | +| `8.10-2` | 2187 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.10-3` | 2193 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.10-5` | 2195 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.10-6` | 2195 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.12-4` | 2223 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.12-17` | 2255 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.12-18` | 2263 | MAY | black-box-http | — | No case in this suite version exercises this clause. | +| `8.13-1` | 2334 | MAY | black-box-http | — | No case in this suite version exercises this clause. | +| `8.15-1` | 2354 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.15-2` | 2354 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.15-3` | 2354 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.15-7` | 2376 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `8.15-10` | 2376 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.2-3` | 2732 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.2-10` | 2745 | SHOULD | review-only | — | The spec marks this subsection non-normative guidance; recorded so the scan's keyword hit is accounted for rather than appearing as an untracked clause. | +| `10.2-11` | 2745 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.4-1` | 2755 | MUST | review-only | — | Binds the Collection Profile's INTERACTION channel, which Core's HTTP surface does not expose; logs and persisted state are outside what a black-box client can read. | +| `10.5-1` | 2759 | SHOULD | review-only | — | — | +| `10.6-1` | 2765 | SHOULD | client-capture | — | No case in this suite version exercises this clause. | +| `10.8-2` | 2777 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-3` | 2777 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-5` | 2779 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-6` | 2781 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-7` | 2781 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-8` | 2781 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `10.8-9` | 2783 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `10.8-10` | 2783 | MUST | review-only | — | The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation. | +| `10.8-11` | 2783 | MUST | black-box-http | — | No case in this suite version exercises this clause. | +| `12.3-1` | 2890 | MUST | review-only | — | Whether a stream name "encodes predicate logic" is a semantic judgement no static check settles. The paragraph is headed "(non-normative)" yet carries a MUST NOT; recorded at the keyword's level and flagged for the spec lock. | +| `12.4-1` | 2896 | MUST | review-only | — | Needs a target that offers an extension capability and a hook that enables it; the suite has no such fixture. Missing case: a grant's reachable data is identical with the extension enabled and disabled. | +| `12.4-2` | 2896 | MUST | black-box-http | — | No case yet checks that each capability a target exercises is advertised in its declared metadata. Missing case: cross-check stream metadata and protected resource metadata against the capabilities the target answers. | +| `12.4-3` | 2896 | MUST | client-capture | — | Needs a client fixture response that advertises an unknown capability; no Section 9 client item names it. Missing case: the client under test completes its flow against metadata carrying an undeclared capability. | diff --git a/packages/conformance-suite/README.md b/packages/conformance-suite/README.md index c84d0abf2..93daa7cad 100644 --- a/packages/conformance-suite/README.md +++ b/packages/conformance-suite/README.md @@ -32,10 +32,10 @@ against a local AS + RS fixture and inspect its inbound request log. No case imp the target implementation. The bundled reference target under `src/targets/` implements the target contract and holds no privilege. -**Traceable to the clauses, not just the item numbers.** Section 9's 45 items are +**Traceable to the clauses, not just the item numbers.** Section 9's 123 items are summaries; a pass on one says nothing about which of the normative sentences it summarizes was exercised. [`matrix.ts`](./src/requirements/matrix.ts) enumerates -those sentences for Core sections 4-8 and 10 — 131 clauses at v0.1, each with its +those sentences for the locked Core v0.1.0 text — 708 clauses, 695 of them in sections 4-8 and 10, each with its verbatim text, level, role, observable boundary, the items that roll it up, the cases that exercise it, and for every uncovered MUST the exact missing hook. [`docs/reference/conformance-normative-matrix.md`](../../docs/reference/conformance-normative-matrix.md) @@ -44,7 +44,7 @@ lists each requirement's clauses and names the MUST clauses its cases did not reach, so a `PASS` cannot be read as covering the whole item. **Traceable to numbered requirements.** [`catalog.ts`](./src/requirements/catalog.ts) -transcribes all 45 numbered Section 9 items — AS 1–21, RS 1–16, Client 1–8 — keeping +transcribes all 123 numbered Section 9 items — AS 1–57, RS 1–33, Client 1–33 — keeping the spec's own numbering, so `RS-9` is Section 9 "Resource Server conformance" item 9. Each carries its normative level, applicability, and spec anchor. Every report records the spec document, version, and section it was judged against. diff --git a/packages/conformance-suite/scripts/render-matrix-md.ts b/packages/conformance-suite/scripts/render-matrix-md.ts index bb6a33efc..e6f974ac7 100644 --- a/packages/conformance-suite/scripts/render-matrix-md.ts +++ b/packages/conformance-suite/scripts/render-matrix-md.ts @@ -198,7 +198,7 @@ export function renderMatrixMarkdown(): string { "", ...renderRevisionNote(), "Section 9 conformance items that summarize it and to the suite cases that", - "exercise it. Section 9 is the index here, not the authority: its 45 numbered", + "exercise it. Section 9 is the index here, not the authority: its 123 numbered", "items are one-line precis of the clauses below.", "", "`Observable` records how a clause could be checked at all, independent of", diff --git a/packages/conformance-suite/spec/spec-core.md b/packages/conformance-suite/spec/spec-core.md deleted file mode 100644 index 1cc191403..000000000 --- a/packages/conformance-suite/spec/spec-core.md +++ /dev/null @@ -1,1916 +0,0 @@ -# Personal Data Portability Protocol (PDPP) v0.1.0 - -Status: Normative draft -Date: 2026-09-03 - ---- - -## 1. Introduction {#introduction} - -PDPP is an authorization and disclosure protocol for personal data. It defines how a user authorizes an application to access specific data from a data source that holds their records, and how a resource server enforces that authorization. - -The protocol specifies: - -- A **record model** for representing personal data as flat relational streams -- A **selection request** format, the structured request for consent a client submits during authorization (RFC 9396 envelope) -- A **grant** object representing user-approved, parameterized consent -- A **source declaration** describing the consent, record, selection, and query - surface exposed by either a connector-backed or provider-native source -- A **resource server interface** for serving records under grant enforcement - -**Design axiom:** Source declarations define what can be requested. Grants define what was approved. These are separate concerns and must not be conflated. - -Most source platforms do not yet expose a PDPP interface natively. Collection is the bridge for those sources: it brings their data into a resource server so the protocol's consent and enforcement layers can govern access to it. The informative [PDPP Collection Profile](spec-collection-profile) describes that bridge as builder guidance; it is not a conformance requirement. A connector conforms to PDPP by producing a source declaration valid under Section 5 and serving its data through a resource server conforming to Section 8. No particular collection method is required. The core protocol is useful without it: a resource server holding pre-collected data can serve that data under grant enforcement with no collection machinery involved, and data may also reach it via regulatory data exports, manual import, or platform-native APIs. The consent and enforcement layers defined in this specification (Sections 5-8) are agnostic to the collection method. - -Any implementation satisfying the role conformance criteria in Section 9 is PDPP-compliant. This specification does not depend on any specific network, token, ledger, infrastructure provider, hosted service, centralized registry lookup, or deployment of this repository. URI identifiers name sources, purposes, clients, and resources; they do not make the example registries in this document runtime dependencies. Consent integrity comes from the resolved grant and the exact source declaration snapshot retained by the authorization server. - -### Interoperable core sections - -Sections 4-8 define the protocol surfaces that implementations evaluate independently. - -| Section | Governs | Other layers | -| --- | --- | --- | -| [Section 4: Record Model](#record-model) | Portable record envelopes, stream identity, primary keys, blob references, resource references, stream semantics, and incremental-sync metadata. | Source collection, connector execution, and storage-engine choices. | -| [Section 5: Source Declaration](#source-declaration) | Common source identity, consent, record, selection, and query capabilities used by connector-backed and provider-native sources, and the conditions under which an authorization server accepts a declaration. | Connector acquisition and execution mechanics. | -| [Section 6: Selection Request](#selection-request) | What a client asks an authorization server to approve, plus declaration-backed validation and consent rendering before a grant is issued. | Product-specific consent flows, screen layouts, and hosted authorization-server deployments. | -| [Section 7: Grant](#grant) | The immutable consent artifact and the constraints a resource server enforces for a token-bound client. | Grant database schema, signed-token format, hosted registries, and deployment topology. | -| [Section 8: Resource Server Interface](#resource-server-interface) | The interoperable record-query and blob-fetch interface under grant enforcement, and the protected resource metadata a resource server publishes about itself. | Authorization-server deployment, storage backend, collection runtime, operator dashboard, and hosted service choices. | - -### Relationship to existing standards - -| Standard | Relationship | -|----------|-------------| -| [OAuth 2.0](https://www.rfc-editor.org/rfc/rfc6749) (RFC 6749) | PDPP is a profile of OAuth 2.0, carrying selection requests in RFC 9396 authorization_details. The grant is issued as the result of an OAuth authorization flow. | -| [RFC 9396](https://www.rfc-editor.org/rfc/rfc9396) (RAR) | PDPP uses the `authorization_details` envelope for selection requests. The `type` URI is `https://pdpp.dev/data-access`. | -| [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750) (Bearer Token) | PDPP transports both owner tokens and client tokens as RFC 6750 Bearer Tokens on the wire. The resource server distinguishes token kind via `pdpp_token_kind` in the introspection response, not by token syntax. | -| [RFC 7662](https://www.rfc-editor.org/rfc/rfc7662) (Token Introspection) | PDPP uses authenticated RFC 7662 token introspection where the authorization server and resource server are separated, so the resource server can resolve grant-bound tokens. Co-located deployments may use a local equivalent. | -| [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) (Protected Resource Metadata) | PDPP resource servers publish RFC 9728 protected resource metadata, so a client discovers the authorization server, the query base, and the supported token kinds from the resource itself rather than from prior configuration. Core defines four `pdpp_`-prefixed additional members (Section 8); the extension profiles define `capabilities`. | -| [OAuth 2.0 Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591) (RFC 7591) | PDPP reuses the RFC 7591 client metadata vocabulary (`client_name`, `logo_uri`, `policy_uri`, and similar fields) for the consent display. A dynamic client registration endpoint is a deployment choice and is required only where deployments need it; Core functions without it. | -| [Client ID Metadata Documents](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/) (CIMD, IETF OAuth WG draft) | A client identifier that is itself an `https` URL the authorization server fetches to obtain RFC 7591-shaped client metadata, with no prior registration handshake. Control of the URL's domain is the trust root. CIMD is how deployed MCP clients present themselves: the MCP authorization specification revision 2025-11-25 states that authorization servers and clients SHOULD support CIMD and MAY support RFC 7591 dynamic client registration, which is retained for backward compatibility. Core treats a validated CIMD document as one source of validated binding metadata (Section 6) and its verified domain as a trust signal; the fetch and validation obligations belong to the OAuth binding rather than to Core. | -| [SMART on FHIR](https://hl7.org/fhir/smart-app-launch/) | Follows the domain-profile-over-OAuth pattern PDPP adopts: OAuth handles authorization, and the profile adds a domain data model, consent semantics, and a conformance regime. SMART on FHIR reached ubiquity through regulatory adoption of SMART-on-FHIR-patterned API requirements (the ONC Cures Act rule). | -| [UK Open Banking](https://www.openbanking.org.uk/standards/) | Also follows the domain-profile-over-OAuth pattern PDPP adopts: OAuth handles authorization, and the profile adds a domain data model, consent semantics, and a conformance regime. UK Open Banking reached ubiquity through the CMA's Open Banking mandate for the largest UK banks. | -| [UMA 2.0](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html) (Kantara) | UMA is important prior art for PDPP's user-managed, standing, revocable access model, particularly where an outside party seeks access to user-controlled resources. PDPP's authorization protocol derives directly from OAuth 2.0 and RFC 9396. | -| [GNAP](https://www.rfc-editor.org/rfc/rfc9635) (RFC 9635) | GNAP is an IETF authorization protocol that revisits OAuth-style delegation with a new protocol design. Several design decisions are directly relevant to PDPP: (1) interaction modes beyond browser redirects (relevant to nonstandard authorization interaction patterns); (2) request continuation for multi-step consent negotiation (relevant to optional streams); (3) key-bound grants instead of bearer tokens (stronger security for ongoing personal data access); (4) built-in grant management with revocation and rotation (relevant to `continuous` access mode). PDPP v0.1 uses OAuth 2.0 + RFC 9396. A future version should evaluate whether GNAP is a better foundation. PDPP's entity-scoped `client_display` already follows GNAP's pattern of carrying client display metadata inline in the request. For key-bound tokens specifically, DPoP (RFC 9449) offers an OAuth-native path to GNAP-style sender-constrained tokens and is a candidate optional hardening profile for v0.2. | -| [Solid](https://solidproject.org) | Solid takes the full re-architecture approach: personal data moves into user-controlled pods with RDF/Linked Data semantics, which requires source platforms to adopt the model or users to migrate off-platform. PDPP instead layers on existing OAuth infrastructure and bootstraps data supply through the Collection Profile, without requiring source platforms to adopt anything. | -| [Data Transfer Project](https://github.com/dtinit/data-transfer-project) (DTI) | PDPP and DTI are complementary. The Data Transfer Project handles transfer mechanics, and DTI's stated position is that there is "no silver bullet" for portability: multiple approaches coexist. DTI's Data Trust Registry (post-pilot, 2026) addresses who is trusted: it vets services seeking access to platforms' portability interfaces so that platforms can rely on shared trust signals. PDPP addresses what was consented and how it is enforced (the grant and the resource server interface); a trust registry and PDPP's consent semantics compose rather than compete. The two protocols can chain. See Appendix B. | -| [Airbyte](https://airbyte.com) / [Singer](https://www.singer.io) | PDPP borrows the RECORD/STATE checkpoint pattern for incremental sync. This record and state-checkpoint lineage informs the Collection Profile companion specification; it appears here for reader orientation and is informative for Core. | -| [GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/oj) | PDPP implements data minimization through stream and field selection. It also carries machine-readable purpose declarations (`purpose_code`) that support consent display, local policy, and implementation-defined audit or transparency mechanisms, with an explicit protocol-level consent rule for `ai_training`. The internal version history required for incremental sync may support implementations that choose to expose historical access features to users. Whether such exposure is required is outside the scope of this specification. This alignment is informative only and is not a required v0.1 capability. | -| [DMA](https://eur-lex.europa.eu/eli/reg/2022/1925/oj) | The `continuous` access mode enables ongoing portability aligned with the DMA's requirements. Article 6(9) requires effective portability with continuous and real-time access to the end user's data; PDPP's continuous grants and incremental sync map to that requirement. This alignment is informative only and is not a required v0.1 capability. | -| [EU Data Act](https://eur-lex.europa.eu/eli/reg/2023/2854/oj) (Regulation 2023/2854) | The Data Act's Article 5(1) flow, where a user directs a data holder to make data available to a third party, is the same triangle as PDPP's owner, source, and client. Article 2(12) defines the user as a natural or legal person, matching PDPP's subject-neutral owner. It applies from 12 September 2025, requires access by design for connected products placed on the market after 12 September 2026, and names no protocol. | - -**Note:** The PDPP Collection Profile is one fulfillment mechanism. A conformance test suite for this specification is planned but is not defined in v0.1 (see Section 9). - -#### Why a profile rather than a new authorization framework {#profile-not-framework} - -PDPP is a data-portability profile, not a general authorization framework. It rides OAuth 2.0 and RFC 9396 because that is what is deployed. On top of those it adds what neither UMA nor GNAP defines: a portable record model, purpose-bound durable grants, a declared collection and source layer, and a query surface with grant enforcement. UMA is prior art for the user-managed standing access model and GNAP is a candidate future foundation; the table above records both. A GNAP transport binding remains possible later without changing the record model, the grant, or the resource server interface, because none of those depend on the authorization protocol that produced the grant. - -#### Relationship to sector-specific consent regimes {#sector-regimes} - -**Note (non-normative).** PDPP is a floor, not a ceiling. A source already operating under a stricter or more specific consent regime — open banking, or a health regime built on FHIR — does not lose or weaken any obligation of that regime by also being reachable through PDPP. Where the two differ, the stricter requirement governs that source. Nothing in this specification authorizes a disclosure that the source's own regime forbids, and a PDPP grant is not a substitute for a consent that regime requires. This is a scoping statement rather than a conformance criterion: PDPP defines no test for which of two regimes is stricter, and conformance to this specification is assessed against this specification alone. Compatibility profiles that map a sector regime's consent vocabulary onto PDPP's are a possible future addition and are not defined in v0.1. - ---- - -## 2. Terminology and Actors - -### Actors - -| Actor | Definition | -|-------|-----------| -| **User** | The person whose data is being accessed. Owns the data, approves grants, may revoke. | -| **Client** | An application or AI agent requesting user data. Identified by `client_id`. In OAuth terms, this is the client. | -| **Data Source** | Any external system from which a user's data originates: a consumer platform, a SaaS application, a device, a local archive, a financial institution, or other system. | - -The owner is the authenticated subject whose records the source holds. Typically this is a person; the protocol itself is subject-neutral, and an organization that authenticates as the account holder participates identically. - -### Protocol roles - -These roles may be co-located in a single deployment (e.g., a personal server acting as both authorization server and resource server) or separated. The spec defines the interfaces between roles, not the deployment topology. - -| Role | Responsibility | -|------|---------------| -| **Authorization Server** | Issues and manages grants. Validates selection requests against retained source declaration snapshots. Tracks grant lifecycle (active, expired, revoked). | -| **Resource Server** | Stores records as flat relational streams. Serves records to clients filtered by grant parameters. | - -The [PDPP Collection Profile](spec-collection-profile) defines a third role: - -| Role | Responsibility | -|------|---------------| -| **Connector Runtime** | Runs connectors. Writes collected records to the resource server. Manages incremental sync state. | - -In many deployments, a single **personal server** fills all three roles. The spec uses "personal server" when referring to a combined deployment, and the specific role name when the distinction matters. - -**Note on the Authorization Server interface:** This spec defines the resource server interface normatively because cross-deployment interoperability requires it: a client written against the interface works with any conformant resource server regardless of who operates it or where data lives. The authorization server interface is not normatively specified in v0.1 because user-facing authorization flows are deployment-specific. The reference implementation uses the OAuth authorization code flow with RFC 9396 authorization_details for client grants, and OAuth device authorization for owner tokens. - -**Token resolution:** User-facing authorization flows are deployment-specific and are not normatively specified in v0.1. However, when the AS and RS are deployed separately, the AS-to-RS token-resolution contract is normative: the RS MUST authenticate to the RFC 7662 introspection endpoint and resolve the complete grant enforcement context from its response. The RS MUST enforce the request from that response and MUST NOT make a second AS lookup. For co-located deployments, a local equivalent (shared database or function call) is acceptable. Self-contained JWTs may be used as an optimization but MUST NOT be the sole revocation mechanism (see Section 10). - -### Trust registry queries {#trust-registry-queries} - -Core refers to a trust registry as a source of requester identity metadata and a positive trust signal at consent, as an external mechanism supporting retention accountability, and as a deferred concern. Core relies on two abstract queries: whether a client identifier is recognized and authorized, and whether a source declaration authority is accepted. Neither query is a wire protocol; an authorization server MAY answer locally or through a remote service. - -A registry answer is an assertion that local authorization-server policy evaluates; it is not itself a decision. A conveyed answer identifies six things. It names the subject it concerns and the role, action, or scope the status qualifies. It carries the status itself, the governance framework under which the named issuer conferred it by URI, and the issuer or trust-anchor identifier. It bounds the answer with a validity window, as `valid_from` and `valid_until`. Without the subject and the issuer, an answer cannot be attributed or replayed against the right party; without the role or scope, a status does not say what the subject is authorized to do. - -An authorization server records the trust signal it relied on — subject, role or scope, status, governance-framework URI, issuer or trust-anchor identifier, `valid_from`, `valid_until`, and the time of lookup — on its acceptance record or resulting grant. The lookup time matters because a status may be withdrawn later, and the record has to show what was true when the server relied on it. - -Core does not define the registry, transport, recognition mechanism, or withdrawal propagation. A server that consults no registry remains conformant because registry answers inform local policy and never replace the owner's grant. ToIP's Trust Registry Query Protocol is an intended future profile binding, not a Core requirement. - -### Data concepts - -| Term | Definition | -|------|-----------| -| **Grant** | An immutable consent artifact specifying what data a client may access, under what constraints. | -| **Stream** | A named collection of records with a schema, primary key, and optional cursor field. Stream names are source-local (e.g., `messages`). The fully qualified identifier is an ordered pair `(source.id, stream_name)`, used in cross-source references and storage. Example: `("https://registry.pdpp.dev/connectors/spotify", "top_artists")`. | -| **Record** | A single data object within a stream. | -| **Connector** | A program that collects data from a data source, used when data is collected rather than served natively. One of possibly several producers of a source's streams. Defined in the Collection Profile. | -| **Source Declaration** | A source's versioned declaration of its identity, publisher, streams, schemas, consent surface, selection capabilities, and Resource Server query capabilities. It does not define connector acquisition or execution. | -| **Selection Request** | A client's request for specific data, expressed as RFC 9396 `authorization_details`. | -| **View** | An optional named field set a source declaration may define for a stream, composed from fields declared in the stream schema. When a client requests by view name, the resulting grant records the resolved field list, which is authoritative. Declared views are advisory; the authorization server is authoritative for views used in consent UI and issued grants. | - -### Requirements Language {#requirements-language} - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC 2119] [RFC 8174] when, and only when, they appear in all capitals, as shown here. - -This document is normative except where content is explicitly marked as an example, a note, or otherwise non-normative. - -This is the only normative document for PDPP Core v0.1 conformance. A profile defines no Core requirement; a future profile may define a separate named conformance claim only if its own status says so. The [PDPP Collection Profile](spec-collection-profile) and [PDPP Source Declaration Discovery and Trust](spec-discovery-and-trust) are informative and define no conformance requirements of their own. - ---- - -## 3. System Architecture - -Every PDPP deployment shares the same authorization core: a user grants a client consent, and the authorization server issues a grant. The resource server that then serves records under that grant is not shown here; how it is populated and operated is deployment-specific (see below). - -```mermaid -flowchart TB - User((User)) - Client[Client] - AS[Authorization Server] - - User -- consent --> AS - Client -- selection request --> AS - AS -- grant --> Client -``` - -What differs between deployments is how the resource server that fulfills the grant is populated and operated. This is not a closed set: a resource server may hold pre-collected data with no collection machinery involved, or receive data via regulatory export, manual import, or platform-native APIs ([Section 1](#introduction)). The two examples below illustrate the ends of that spectrum; the [PDPP Collection Profile](spec-collection-profile) is one fulfillment mechanism, not the only one. - -**Example A: source-native fulfillment.** The data source operates its own authorization and resource servers directly; there is no separate collection step. - -```mermaid -flowchart TB - Client[Client] - AS[Authorization Server] - RS[Resource Server] - Source[Data Source] - - Client -- query under grant --> RS - RS -- enforced records --> Client - - Source -.->|operates| AS - Source -.->|operates| RS -``` - -**Example B: personal-server fulfillment.** A connector runtime, governed by the Collection Profile, collects data from the source and syncs it into a resource server the user controls. - -```mermaid -flowchart TB - Client[Client] - AS[Authorization Server] - RS[Resource Server] - Runtime[Connector Runtime
Collection Profile] - Source[Data Source] - - Client -- query under grant --> RS - RS -- enforced records --> Client - - Runtime -- RECORD / STATE --> RS - Source -.->|collected by| Runtime -``` - -**Core defines no ingest path.** Core specifies the authorization server, the resource server, and what passes between them and a client. It does not specify how records reach the resource server, and conformance to Core does not depend on any particular answer. Examples A and B are illustrations, not a classification: a resource server holding data placed there by regulatory export, manual import, a provider's own write path, or a mechanism not yet described is equally within Core. The [PDPP Collection Profile](spec-collection-profile) describes one such mechanism, connector collection, and describes it for builders rather than as a conformance requirement. - -**On-behalf-of access is two ordinary grants, not one delegated grant.** At the upstream platform, the owner authorizes the personal server to read the owner's data. At the personal server, the owner separately authorizes a downstream client to read owner-held data. These are independent grants, each issued and revocable by the owner. A personal server MAY disclose owner-held data under the fresh downstream grant; it does not pass the platform grant or a downstream PDPP token to the other relationship. - -PDPP therefore defines no delegated-grant, sub-grant, or grant-chaining construct, and none is needed to build this. The same shape is already how account aggregation works in deployed systems: an open banking aggregator holds the user's consent at each bank and its own consent from the user, rather than subdividing one authorization. Where a binding needs to exchange one credential for another across the two relationships, OAuth Token Exchange (RFC 8693) is the existing mechanism, and it is a concern of the binding rather than of Core. - -Only live passthrough is bounded by the upstream grant: when the personal server does not hold a copy and reads from the platform to fulfill a downstream request, it MUST stay within the upstream grant. This limit does not constrain a fresh grant for owner-held data. A downstream PDPP token is never forwarded upstream. - -**Onward transfer is prohibited.** A recipient MUST NOT transfer its token or let an ungranted party exercise it. - -A different topology — the owner running their own authorization server in front of a platform's data, so that the platform holds the data but the owner's server decides who may read it — is not introduced by this specification. It is the arrangement UMA describes, it is not the arrangement above, and it is recorded as deferred rather than left ambiguous. - -### Protocol layering - -PDPP separates three concerns that other systems conflate: - -1. **Authorization**: the user's consent about what is disclosed, to whom, and under what constraints. This is the grant. It is the portable core of PDPP. - -2. **Disclosure**: the records the resource server returns given a valid grant. This is the resource server query API. - -3. **Collection**: how data gets into the resource server in the first place. This is the Collection Profile. It is one answer to this question; pre-loaded data, manual imports, and other mechanisms are equally valid. - -The grant and query API are the normative core. Collection is a companion mechanism. - -**Ingest and sync-state are Collection Profile concerns.** The core protocol defines the query API (disclosure) and grant semantics. The Collection Profile defines record ingest and sync-state management endpoints for implementations that claim Collection Profile support. - ---- - -## 4. Record Model {#record-model} - -**Note:** This section defines portable record envelopes, stream identity, primary keys, blob references, resource references, stream semantics, and incremental-sync metadata. Source collection, connector execution, and storage-engine choices are out of scope for this document (see the [PDPP Collection Profile](spec-collection-profile)). - -Personal data is represented as flat relational streams. This enables streaming, pagination, incremental sync, and compatibility with DTI canonical data models. - -### Streams - -A stream is a named collection of records with a consistent schema. Examples: `playlists`, `messages`, `sleep_sessions`. A stream's shape (its fields and schema) is declared in the source declaration; see [Section 5: Source Declaration](#source-declaration). - -### Stream semantics - -Each stream has one of two semantic types: - -| Semantics | Meaning | Examples | Resource server behavior | -|-----------|---------|----------|------------------------| -| `append_only` | Records are immutable events. New records are added; existing records are never modified. | messages, transactions, play_events, workouts | Insert only. Duplicate keys are idempotent. | -| `mutable_state` | Records represent current state of an entity. Records may be updated or deleted. | profile, settings, playlist_items, follow_lists | Upsert by primary key. Resource server maintains version history for incremental sync. | - -Approximately 95% of personal data by volume is `append_only`. The remaining 5% is `mutable_state`. Mutable state records (profiles, preferences, relationships) are often the highest-value context for AI agents. - -### Incremental sync for mutable streams - -For `mutable_state` streams, the resource server maintains internal version history to support incremental sync queries. This is an implementation detail: the protocol surface is a standard cursor-based query that returns records changed since a given cursor position (see [Section 8](#resource-server-interface)). The version history is not exposed as a separate stream. - -A client that has previously synced a `mutable_state` stream queries for changes by passing its last cursor. The resource server returns only records whose state has changed since that cursor, within the client's grant-authorized field projection. If no authorized fields changed on a record, that record does not appear in the response. - -This design ensures that a client authorized for fields A and B cannot infer that field C changed, even if C was modified after the client's last sync. The response is a function of the grant, not of the full record state. - -**Snapshot model:** `changes_since` returns the full current state of each record whose grant-authorized projection changed since the cursor position, plus tombstones for deletions. It does not return field-level diffs. The client receives a complete record object for any record that changed. - -**Cursor expiry:** Resource servers MAY expire historical version data after a retention period. If a client's cursor has expired, the resource server MUST return HTTP 410 Gone with error code `cursor_expired`. The client MUST perform a full re-sync to re-establish its baseline. - -**Two distinct cursor spaces:** `cursor`/`next_cursor` are pagination tokens within a single query execution; `changes_since`/`next_changes_since` are incremental sync tokens across sessions. A client MUST NOT use a `next_cursor` value as a `changes_since` parameter; they are different token spaces and will produce a protocol error if confused. The terminal page of a `changes_since` result MUST include a `next_changes_since` field. Paginating an incremental sync: pass `changes_since` on the first request, follow `next_cursor` for subsequent pages within that session, then store `next_changes_since` from the terminal page for the next session. - -**Tombstones:** When a record is deleted from a `mutable_state` stream, the resource server MUST include a tombstone entry in incremental sync responses for clients whose cursor predates the deletion. Tombstone format: - -```json -{ - "object": "record", - "id": "canonical-key-string", - "stream": "playlists", - "deleted": true, - "deleted_at": "2026-04-01T10:00:00Z", - "emitted_at": "2026-04-01T10:00:01Z" -} -``` - -Tombstones use the same `object: "record"` envelope as regular response records, with `deleted: true`. The `id` field is the canonical key string (see RECORD envelope, Compound key encoding below). Both `deleted_at` and `emitted_at` are required on tombstone objects. No `data` field is present on tombstones. - -A tombstone signals that a record left the stream. For subset or derived streams this means membership removal; it does not assert that the source record was deleted. See [Derived subset streams](#predicate-based-grant-scoping) (Section 12) for the non-normative discussion of this stream shape. - -`deleted_at` represents the time the record was deleted in the source system, if known; otherwise the time the RS processed the deletion directive. If the source system deletion time is unknown, the RS SHOULD use the `emitted_at` value of the delete directive as `deleted_at`. - -**Non-normative note (GDPR Article 15):** The version history maintained internally by the resource server to support `mutable_state` incremental sync may support implementations that choose to expose historical access features to users. Whether such exposure is required is outside the scope of this specification. This alignment is non-normative and is not a required v0.1 capability. - -### Split rule - -When modeling data from a source, apply this rule: - -- **Separate stream** if: has its own stable ID, unbounded cardinality, changes independently, or consumers query it independently. -- **Nested within a record** if: small, bounded, and only meaningful as part of the parent. - -Example: `conversations` and `messages` are separate streams (messages are unbounded, have their own IDs, change independently). A message's `content_blocks` array can be nested (bounded, only meaningful within the message). - -This is a common data modeling judgment call. The rule above is a guide, not a formula; declaration publishers exercise discretion. - -### The RECORD envelope - -RECORD is the universal data envelope. It is used in the Collection Profile and is the canonical shape for records stored in the resource server. - -```json -{ - "stream": "messages", - "key": "msg_abc123", - "data": { - "id": "msg_abc123", - "conversation_id": "conv_xyz", - "role": "user", - "content": "What is the weather like?", - "source_created_at": "2026-03-28T15:00:00Z" - }, - "emitted_at": "2026-03-28T15:01:00Z" -} -``` - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `stream` | string | yes | Stream name | -| `key` | string or string[] | yes | Primary key value. Array for compound keys; order matches the SourceDeclaration `primary_key`. | -| `data` | object | yes | Record payload conforming to the stream schema. | -| `emitted_at` | ISO 8601 | yes | When the record was emitted by its producer (processing time, not source time). | -| `op` | enum | no | `upsert` (default) or `delete`. This field is a directive to the resource server and is not stored as part of the record data. | - -**Compound key encoding:** When `primary_key` has multiple fields, `key` is an array of values in the order declared by the SourceDeclaration `primary_key`. The canonical string form of a compound key is the minified JSON array of key values (e.g., `["user_123","2026-04-01"]`). Each primary-key component MUST be serialized as a string in the canonical encoding. Non-string primary-key field values (e.g., integers, dates) MUST be converted to their string representation before encoding. URL path parameters and `resources[]` entries use percent-encoded canonical string form. The `resource_ref.record_id` field retains native `string | string[]` type. - -**Record identity:** For any record, the values of the `data` fields named by the stream's `primary_key` MUST match the values in the `key` envelope field (in order). A resource server or profile-defined write interface MUST reject a record before storage when those values disagree. The Collection Profile defines the HTTP error for its ingest endpoint. - -### Timestamps - -Two timestamp concepts appear in records: - -- **`emitted_at`**: when the producer emitted the record. Always present on the RECORD envelope. Producer-generated. -- **Source timestamps**: when the event occurred or the resource was created or modified on the source platform. These are fields within `data`, declared in the stream schema. The spec reserves two standard field names: `source_created_at` and `source_updated_at`. Connector authors SHOULD use these names when the platform provides them, rather than inventing platform-specific names. - -### Foreign keys - -Streams reference each other via foreign key fields in `data`: - -```json -{ - "stream": "conversations", - "key": "conv_xyz", - "data": { - "id": "conv_xyz", - "title": "Weather chat", - "source_created_at": "2026-03-28T14:00:00Z" - } -} -{ - "stream": "messages", - "key": "msg_abc123", - "data": { - "id": "msg_abc123", - "conversation_id": "conv_xyz", - "content": "...", - "source_created_at": "2026-03-28T15:00:00Z" - } -} -``` - -The SourceDeclaration declares `primary_key` per stream. Foreign key relationships are declared in its `relationships` field (see [Section 5](#source-declaration)). - -### Binary data (blob_ref) - -Binary data (photos, videos, audio, documents) is not inlined in records. The record contains metadata and a `blob_ref`: - -```json -{ - "stream": "media", - "key": "media_123", - "data": { - "id": "media_123", - "caption": "Sunset", - "media_type": "image", - "source_created_at": "2026-03-28T10:00:00Z", - "blob_ref": { - "blob_id": "blob_media_123", - "mime_type": "image/jpeg", - "size_bytes": 2048000, - "sha256": "a1b2c3..." - } - } -} -``` - -`mime_type` MUST be a valid IANA media type (see [IANA Media Types](https://www.iana.org/assignments/media-types/)). Record producers emit `blob_ref` without a `fetch_url`. The resource server injects `fetch_url` at read time when serving records via the query API. - -### Cross-stream references (resource_ref) - -When a record references a record in a different stream on the same resource server, use a `resource_ref`. This is a within-subject, within-server pointer. Cross-user or cross-server references are out of scope in v0.1. - -```json -{ - "stream": "tag_assignments", - "key": "assign_1", - "data": { - "tag_id": "tag_sunset", - "target": { - "source_id": "https://registry.pdpp.dev/connectors/instagram", - "stream": "media", - "record_id": "media_123" - } - } -} -``` - ---- - -## 5. Source Declaration {#source-declaration} - -**Note:** This section defines the common source surface used by Core, and the conditions under which an authorization server accepts a source declaration. Connector acquisition and execution behavior, including runtime bindings, setup, interaction, refresh, and collection state, is defined by the optional [PDPP Collection Profile](spec-collection-profile). The mechanics of discovering and retrieving a declaration are described in the informative [PDPP Source Declaration Discovery and Trust](spec-discovery-and-trust) document, which defines no requirements of its own. - -Each source publishes a `SourceDeclaration` describing its identity, publisher, consent surface, record semantics, selection capabilities, and Resource Server query capabilities. Connector-backed and provider-native sources use the same Core shape. The declaration defines what can be consented to. The resolved grant defines what was approved. - -### SourceDeclaration structure - -```json -{ - "protocol_version": "0.1.0", - "source": { - "kind": "connector", - "id": "https://registry.pdpp.dev/connectors/spotify" - }, - "declaration_version": "2026-08-11", - "publisher": { - "id": "https://registry.pdpp.dev/" - }, - "display": { - "name": "Spotify" - }, - "selection_presets": [ - { - "id": "listening_history", - "label": "Listening history", - "streams": [ - { "name": "top_artists" }, - { "name": "play_events" } - ] - } - ], - "streams": [ - { - "name": "top_artists", - "description": "Most-listened artists over time", - "display": { - "label": "Your top artists", - "detail": "Artist names, genres, and popularity scores. No listening timestamps or play counts." - }, - "semantics": "mutable_state", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "genres": { "type": "array", "items": { "type": "string" } }, - "popularity": { "type": "integer" }, - "source_updated_at": { "type": "string", "format": "date-time" } - }, - "required": ["id", "name"] - }, - "primary_key": ["id"], - "cursor_field": "source_updated_at", - "consent_time_field": "source_updated_at", - "selection": { - "fields": true, - "resources": false - }, - "query": { - "range_filters": { - "source_updated_at": ["gte", "gt", "lte", "lt"] - } - }, - "views": [ - { - "id": "basic", - "label": "Artist names and genres", - "fields": ["id", "name", "genres"] - }, - { - "id": "full", - "label": "Full artist data", - "fields": ["id", "name", "genres", "popularity", "source_updated_at"] - } - ] - }, - { - "name": "play_events", - "description": "Individual track play events", - "semantics": "append_only", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "id": { "type": "string" }, - "track_id": { "type": "string" }, - "played_at": { "type": "string", "format": "date-time" }, - "duration_ms": { "type": "integer" } - }, - "required": ["id", "track_id", "played_at"] - }, - "primary_key": ["id"], - "cursor_field": "played_at", - "consent_time_field": "played_at", - "selection": { - "fields": true, - "resources": false - } - } - ], - "extensions": {} -} -``` - -### SourceDeclaration fields - -| Field | Description | -|-------|-------------| -| `protocol_version` | Version of the PDPP SourceDeclaration schema. This contract requires exactly `0.1.0`. | -| `source` | Exactly `{ kind, id }`. `kind` is `connector` or `provider_native`; `id` is the absolute URI authorization identity for the source's data surface. | -| `declaration_version` | Opaque, non-empty revision identifier for this source declaration. It is not the connector software version and has no implied ordering. | -| `publisher.id` | Absolute URI identifying the declaration publisher. It is an attribution claim, not an authenticated identity. The authorization server MUST treat `publisher.id` as authenticated only where an accepted channel or configured mapping binds that publisher to the declaration; absent that binding it MUST NOT support source acceptance, redirect policy, attribution, or any other trust decision. | -| `display.name` | Human-readable source name for consent UIs. It is display metadata, not source identity. | -| `selection_presets` | Optional preset selections. The authorization server expands a selected preset into explicit stream terms before issuing a grant. | -| `streams[].name` | Unique non-empty stream name, source-local. `*` is request-only and is not a declaration stream name. | -| `streams[].description` | Optional short human-readable summary of the stream's contents (e.g., "Most-listened artists over time"). Not consent-surface metadata; see `streams[].display` for the fields the AS renders during consent. | -| `streams[].display` | Optional consent-surface metadata. See [Stream display metadata](#stream-display). | -| `streams[].semantics` | `append_only` or `mutable_state`. | -| `streams[].schema` | JSON Schema for the record's `data` field. `primary_key` and `cursor_field` MUST reference fields declared here. | -| `streams[].primary_key` | Fields that uniquely identify a record within the stream. | -| `streams[].cursor_field` | Field used for logical record ordering in cursor-based reads and incremental sync. List reads sort by `(cursor_field, primary_key)`, with null or absent cursor values sorting after present values. A cursor is an opaque token the server issues, encoding a logical sort position in the stream. | -| `streams[].consent_time_field` | The temporal consent boundary: the field against which `time_range` is evaluated. Absent means `time_range` is not applicable to this stream. MUST reference a field declared in the schema. | -| `streams[].selection` | Which selection parameters this stream supports (`fields`, `resources`). Time-range capability is derived from `consent_time_field` presence; absent means not time-range-capable. The AS MUST reject grants that request `time_range` on a stream without a `consent_time_field`, or that request an unsupported selection parameter. | -| `streams[].views` | Named field projections the declaration publisher suggests. Advisory; the AS is authoritative. Each view has `id`, `label`, and `fields` (top-level field names only). | -| `streams[].relationships` | Declared foreign key relationships to other streams. Structural graph metadata only; does not by itself make a relation expandable in the read API. Expandability is declared separately as a query capability; see [`expand[]`](#list-records). | -| `streams[].query` | Stream-specific query capability declaration. `range_filters` declares range-queryable fields and operators. `expand` declares expandable relations and per-relation limits. `search` and `aggregations` preserve source-neutral capability declarations used by companion query profiles; their operation semantics and conformance are not defined by Core. | -| `extensions` | Optional object keyed by absolute profile URI. Core preserves or ignores an unknown extension value and does not parse it. An operation that explicitly invokes an unsupported profile is rejected. An extension cannot redefine or weaken Core semantics. | - -The public SourceDeclaration JSON Schema declares JSON Schema 2020-12. Each `streams[].schema` uses that dialect when `$schema` is absent. If `$schema` is present, it MUST equal `https://json-schema.org/draft/2020-12/schema`. This dialect choice does not by itself guarantee identical validator behavior. The AS MUST meta-validate each embedded stream schema before accepting the declaration. Embedded `$ref` and `$dynamicRef` values MUST be local fragment references. A declaration MUST NOT make consent interpretation depend on a mutable remote schema. - -`source.id` is the authorization identity. In a retained declaration or grant, `source.kind` records the provenance class that the AS accepted from its declaration authority. It is not authorization equality, a runtime type, or a Collection Profile claim, and it never selects serving or acquisition runtime. -A connector declaration with no Collection extension remains valid Core input. -The declaration never contains owner-specific account or instance handles. -Those appear on each stream in the selection request and resolved grant. - -### Stream display metadata {#stream-display} - -Streams MAY include a `display` object with human-readable metadata for the consent UI. This metadata is authored by the accepted declaration publisher, not the requesting client. - -| Field | Type | Description | -|-------|------|-------------| -| `display.label` | string | Short human-readable name shown in the consent card (e.g., "Who you follow"). If absent, the AS SHOULD display `streams[].description` or fall back to the stream name. | -| `display.detail` | string | Consent-oriented description of what data is included and, where relevant, what is excluded (e.g., "Usernames and account IDs of accounts you follow. No DMs, profile details, or follower lists."). If absent, the AS MAY generate a description from the stream schema, or display no detail. | - -**Authorship principle:** `display.label` and `display.detail` describe the data itself, not the requester's purpose. They are attributed to the accepted declaration publisher. The requesting client MUST NOT override or supplement these descriptions in the selection request. Publisher attribution is authenticated only under the conditions in [SourceDeclaration fields](#source-declaration). - -```json -{ - "name": "following_accounts", - "description": "Accounts the user follows", - "display": { - "label": "Who you follow", - "detail": "Usernames and account IDs of accounts you follow. No DMs, profile details, or follower lists." - }, - "semantics": "mutable_state", - "schema": { "..." : "..." } -} -``` - -### consent_time_field - -The `consent_time_field` is the field on each record that the resource server evaluates `time_range` against. It represents the stream's temporal consent boundary: when the user consents to "data from the last 6 months," the `consent_time_field` is the field that determines whether a given record falls within that window. - -The `consent_time_field` may be the same field as `cursor_field`, but they serve different purposes and MUST be declared separately: - -- `cursor_field` governs incremental sync mechanics (which records to fetch since the last run). -- `consent_time_field` governs consent-time filtering (which records fall within the authorized time window). - -For many `append_only` streams, both fields will be the same (e.g., `played_at` for play events). For some `mutable_state` streams they may differ: a playlists stream might use `source_updated_at` as the cursor (for efficient incremental sync) but `source_created_at` as the `consent_time_field` (because the user's consent to "playlists from the last 6 months" most naturally means playlists they created in that period, not playlists they edited). - -The `consent_time_field` MUST be rendered in human-readable consent UX. A grant with `time_range: { since: "2026-01-01" }` on the `playlists` stream should be presented as "playlists created on or after January 1, 2026," not just "playlists in time_range." - -Streams that cannot define a stable `consent_time_field` simply omit it. The absence of `consent_time_field` is the normative signal that the stream does not support time-range filtering. - -### Relationships - -```json -{ - "name": "conversations", - "relationships": [ - { - "name": "messages", - "stream": "messages", - "foreign_key": "conversation_id", - "cardinality": "has_many" - } - ] -} -``` - -| Field | Description | -|-------|-------------| -| `name` | Relation name (used in `expand[]` on the query API). | -| `stream` | The related stream name. | -| `foreign_key` | The field on the related stream that references this stream's primary key. | -| `cardinality` | `has_many` or `has_one`. | - -### Views {#views} - -Views are named field projections that the authorization server may define for a stream, composed from fields declared in the stream schema. Views are the unit of consent when a client requests access by view name rather than by explicit field list. A view names a field set within a single stream, while a selection preset expands to stream-level selections at request scope. - -Declaration publishers MAY suggest views. These suggestions are advisory. The authorization server is authoritative for views used in consent UI and issued grants. The AS MUST NOT define a view that includes fields absent from the retained SourceDeclaration schema for the relevant stream. - -**View evolution:** Grants are bound to the resolved field set at issuance time: `fields` in the `StreamGrant` is authoritative, not the view name. View evolution (adding new fields to a view) never silently widens existing grants. Re-consent is required before a client can access new fields, even if those fields are subsequently added to a named view the client already has a grant for. - -**Authority and registry:** Views defined under `pdpp.dev` URI namespaces are controlled by PDPP maintainers via a public change process. Implementations MUST treat unrecognized view URIs as opaque identifiers. - -**Note:** Canonical view naming conventions (standard view names with consistent semantics across connectors) are intentionally deferred. The protocol reserves this space; naming conventions will be informed by implementation experience. - -### Versioning and snapshots - -`declaration_version` is an opaque exact revision identifier. Core does not assign semantic-version ordering or compatibility meaning to it. A connector software release can leave the declaration unchanged, and a declaration can change without a connector release. - -The authorization server retains the exact declaration snapshot used to validate the request and render consent. It uses that same snapshot for narrowing, issuance, and retained consent evidence. The issued grant records the declaration revision and every resolved authorization fact. A version label alone is not a snapshot. - -Adding a field, view, stream, or capability to a later declaration never widens an existing grant. Removing or changing a currently served capability may make a request technically unsupported, but it does not reinterpret what the user approved. The Resource Server enforces the resolved grant without a current declaration lookup. Current serving metadata may only route, describe current capabilities, narrow, or reject. - -### Declaration acceptance {#declaration-acceptance} - -An authorization server accepts a source declaration only through explicit owner or operator onboarding, an installed catalog, an accepted registry entry, or explicit local provisioning. A client MUST NOT introduce a new source authority or declaration URI during authorization. - -For a `provider_native` source, `source.id` MUST be identical to the protected-resource identifier the authorization server has already accepted for that resource. The authorization server MUST reject any mismatch before consent or grant issuance. - -`publisher.id` is an unauthenticated claim unless an accepted channel or configured mapping binds that publisher to the declaration. Without such a binding, the authorization server MUST NOT rely on `publisher.id` for source acceptance, attribution, redirect policy, or any other trust decision. - -An accepted revision is keyed by its accepted authority binding, `source.id`, and `declaration_version`. Different parsed content under an accepted key is equivocation: the authorization server MUST reject it and retain the previously accepted content, and MUST NOT infer ordering or freshness from `declaration_version`. - -When retrieving a declaration, the authorization server MUST use HTTPS without ambient credentials. It MUST validate every redirect target and the final URL against its accepted declaration pointer and network policy. It MUST reject a declaration that requires automatic retrieval of a remote schema, and MUST fail closed when any check fails. - -The address check is bound to the connection, not to the URL. The authorization server MUST validate the destination address against its network policy immediately before each connection attempt, including each redirect hop. It MUST connect only to an address from that validated result. An address accepted for an earlier attempt does not authorize a later resolution. The declaration location is not the source identity. Section 10 states the retrieval limits an authorization server sets by local judgment. - -Declaration display values are untrusted input. An authorization server MUST render declaration display values safely for the output context, by context-appropriate escaping, by sanitization, or by any construction that guarantees the value cannot be interpreted as markup, script, or a control sequence in that context. The requirement is on the outcome; this specification does not mandate one technique. Current declaration capabilities MUST NOT widen an issued grant. - ---- - -## 6. Selection Request {#selection-request} - -**Note:** This section defines what a client asks an authorization server to approve, plus declaration-backed validation and consent rendering before a grant is issued. Product-specific consent flows, screen layouts, and hosted authorization-server deployments are out of scope for this document. - -A client requests specific personal data by including `authorization_details` in an OAuth 2.0 authorization request, following RFC 9396. - -```json -{ - "response_type": "code", - "client_id": "music_recommendations", - "redirect_uri": "https://app.example.com/callback", - "scope": "openid", - "client_display": { - "name": "Concert Finder", - "uri": "https://concertfinder.example.com", - "logo_uri": "https://concertfinder.example.com/logo.png", - "policy_uri": "https://concertfinder.example.com/privacy", - "tos_uri": "https://concertfinder.example.com/terms" - }, - "authorization_details": [ - { - "type": "https://pdpp.dev/data-access", - "source": { "id": "https://registry.pdpp.dev/connectors/spotify" }, - "purpose_code": "https://pdpp.dev/purpose/personalization", - "purpose_description": "Recommend concerts based on your listening history", - "access_mode": "single_use", - "streams": [ - { - "name": "top_artists", - "necessity": "required", - "instance_ids": ["spotify-account-a"], - "time_range": { "since": "2025-09-28T00:00:00Z" } - }, - { - "name": "play_events", - "necessity": "optional" - } - ], - "client_claims": { - "commitments": ["Data used only for concert recommendations"] - } - } - ] -} -``` - -### Client display metadata {#client-display} - -The top-level `client_display` object carries inline client display metadata for the requesting application. PDPP reuses the human-readable client metadata model from OAuth 2.0 Dynamic Client Registration (RFC 7591 Section 2.2), but transports it inline in the authorization request rather than requiring a dynamic client registration endpoint. - -Inside `client_display`, PDPP drops the `client_` prefix from `client_name` and `client_uri` because the enclosing object is already client-scoped. The metadata model is otherwise aligned with RFC 7591. - -| Field | Type | Required | Status | Description | -|-------|------|----------|--------|-------------| -| `client_display.name` | string | yes | Inline client metadata | Inline equivalent of RFC 7591 `client_name`. Human-readable application name. | -| `client_display.uri` | URI | no | Inline client metadata | Inline equivalent of RFC 7591 `client_uri`. The client's homepage. | -| `client_display.logo_uri` | URI | no | Inline client metadata | RFC 7591 `logo_uri`. URL to a square image representing the client. | -| `client_display.policy_uri` | URI | no | Inline client metadata | RFC 7591 `policy_uri`. URL for the client's privacy policy. | -| `client_display.tos_uri` | URI | no | Inline client metadata | RFC 7591 `tos_uri`. URL for the client's terms of service. | - -`client_display` is entity-scoped: it describes the client, not a specific authorization request. It appears at the top level of the authorization request, outside `authorization_details`. - -`client_display` is an inline carrier, not necessarily the AS's final rendered identity record. The AS MAY replace or augment inline values with locally registered metadata, validated binding metadata, validated software-statement metadata, or trust-registry metadata. - -**Validated binding metadata** is client metadata the AS obtained and verified through the mechanism that binds the client to the authorization protocol in use, rather than metadata the client asserted inline in this request. Under the OAuth binding it is the metadata a client ID metadata document or a dynamic registration record supplies, after the binding's own validation succeeded. Core does not define how a binding validates it; Core defines only that validated binding metadata outranks inline `client_display`, because the AS checked it and the client did not merely assert it. - -**URL-hosted client identity.** A client ID metadata document is *valid* for this rule when three conditions hold. It is syntactically valid per the client ID metadata document specification the OAuth binding names. The authorization server retrieved it over HTTPS from the `client_id` URL itself. And the `client_id` inside the document is identical to the URL it was retrieved from — the check that stops a document from claiming to be a different client. - -Two obligations follow, and they are separate. The *interoperability* obligation: a conforming authorization server MUST NOT reject a valid client ID metadata document solely because the client is not preregistered. The *local-authority* obligation, which the first does not weaken: the server MAY still deny authorization, rate-limit the client, or require a registry-derived trust or admission result, under local policy and for any reason other than the absence of preregistration. A conformance test therefore exercises two distinct outcomes — an unregistered valid document that is accepted as an identity, and a policy denial that is not a rejection of the identity form. - -For PDPP Core v0.1 interoperability, a conforming authorization server MUST accept a valid URL-hosted client identity unless local policy denies authorization. - -**Metadata resolution and rendering obligations:** - -1. The AS MUST resolve requester identity metadata from the best available source. Source precedence is local registration or trust-registry metadata, then validated software-statement metadata if supported, then validated binding metadata, then inline `client_display`, then `client_id` fallback. -2. If the resolved metadata contains a display name, the AS MUST display it to the user during consent. If no display name is available, the AS MUST display `client_id` as the requester identity. -3. If the resolved metadata contains `policy_uri` or `tos_uri`, the AS MAY display them as secondary links or disclosures. -4. If the server has a positive trust signal for the client (e.g., verified domain control, trust registry membership), it MUST render that status distinctly (e.g., a "verified" badge). If it has no positive trust signal, it MUST treat the client as unverified and SHOULD display an "unverified app" indicator. -5. **Domain control as a trust signal.** The binding may identify a client by a URL that client controls. Where the AS both retrieved that client's metadata from that URL over HTTPS and confirmed the retrieved document identifies the same client, the AS has verified that the client controls that domain. The AS MAY treat verified domain control as a positive trust signal under obligation 4, and when it does it MUST name the verified domain rather than assert an unqualified verification (for example "Verified domain: example.com", not "Verified app"). Domain control establishes only that the operator of that domain published this client's metadata. It is not an assertion about the client's conduct, its data practices, or any review by the AS operator, and the AS MUST NOT present it as one. -6. The AS MUST treat `logo_uri` as untrusted content until it has been accepted under local policy. It MUST NOT fetch and render a client-supplied remote logo in the consent UI unless the client is verified or the asset has been proxied, cached, and approved under local policy. For unverified clients, the AS SHOULD generate a monogram from the resolved display name. -7. If neither resolved metadata nor inline `client_display` provides a display name, the consent UI SHOULD clearly indicate that the client has not provided display metadata. - -### Pre-registered public client discovery {#pre-registered-public-clients} - -An authorization server MAY support clients that are public and pre-registered by the deployment rather than dynamically registered. Dynamic public-client self-registration is the preferred discoverable path when `registration_endpoint` is advertised. When AS metadata advertises `pre_registered_public` in `pdpp_registration_modes_supported`, the reference publishes `pdpp_pre_registered_public_clients` so agents and third-party clients can discover usable fallback or example `client_id` values without an out-of-band walkthrough. - -Each `pdpp_pre_registered_public_clients` entry contains `client_id`, `client_name`, and `token_endpoint_auth_method`. These entries are public client metadata, not authority to access data. The user grant remains the authorization boundary, and the field MUST NOT contain secrets, access tokens, owner-scoped clients, dynamically registered clients, or private registration state. - -### Client claims {#client-claims} - -The optional `client_claims` object within each `authorization_details` entry carries client-authored, non-enforceable statements about the specific authorization being requested. These are request-scoped, not entity-scoped: a client may make different commitments for different authorization requests. - -| Field | Type | Required | Status | Description | -|-------|------|----------|--------|-------------| -| `client_claims.commitments` | string[] | no | Attributed client claim | Free-text policy commitments relevant to this request (e.g., "Data used only for this study"). | - -**Trust boundary:** Client claims are self-asserted and unverifiable by the server. The AS MUST render `client_claims` content separately from protocol-enforced grant terms and MUST attribute it to the client (e.g., "[client name] says:"). The AS MUST NOT render client claims in the same visual register as protocol-enforced grant terms, structured policy declarations, or declaration-authored data descriptions. - -If rendered on the final owner review surface, `client_claims` MUST be normalized and bound exactly, with client attribution, into the immutable final approval artifact and review revision. Retained consent evidence MUST preserve that binding. They are material consent context, not grant rights. They remain outside authorization equality, the resolved grant, introspection rights, and RS enforcement. - -**Relationship to `purpose_description`:** `purpose_description` is a first-class request field describing what the authorization is for. It is part of the authorization semantics the user reviews. `client_claims.commitments` are supplementary promises that are not reducible to structured protocol fields. Both are client-authored, but `purpose_description` is the primary purpose statement while `commitments` are additional assurances. - -**Commitments that ARE machine-readable:** Structured grant fields (e.g., `retention.max_duration`, `access_mode`) SHOULD be rendered by the AS as server-generated display text (e.g., "Deleted within 90 days", "Ongoing access until you revoke it"). Clients SHOULD NOT duplicate machine-readable constraints as free-text commitments. If a commitment duplicates a structured field, the structured field is authoritative. - -### Semantic classes and consent-surface rendering - -PDPP uses three primary semantic classes across selection requests and grants: - -- **Protocol-enforced constraints:** Values the AS and/or RS actually validate or enforce, such as stream selection, field projection, `time_range`, `resources`, and `access_mode`. -- **Structured policy declarations:** Machine-readable statements that matter for consent, local policy, and implementation-defined audit or transparency mechanisms, but are not generally self-enforcing at the protocol layer. In v0.1 this includes `purpose_code`, `purpose_description`, and `retention`, with one explicit exception: `https://pdpp.dev/purpose/ai_training` adds a protocol-level consent requirement. -- **Attributed client claims:** Client-authored statements that may matter to the user but are not protocol facts. In v0.1 this is `client_claims`. - -`client_display` is a separate category: requester identity metadata used to identify who is asking, not a grant constraint. Inline values may be client-asserted, but the AS renders them under its own resolution and trust policy. - -PDPP does not standardize consent screen layout, visual design, or copywriting. It does normatively constrain semantic rendering. A conformant AS MUST preserve the distinction between protocol-enforced terms, structured policy declarations, declaration-authored data descriptions, and client-authored claims. It MUST NOT flatten these categories into a single undifferentiated consent surface. - -### Request-level parameters - -| Parameter | Type | Required | Status | Description | -|-----------|------|----------|--------|-------------| -| `source` | object | yes | Protocol-enforced | Source binding. `id` is required and is the stable absolute URI authorization identity for the data surface. A request carries `id` alone: provenance is derived by the authorization server from the accepted declaration, not asserted by the client. | -| `purpose_code` | URI | yes | Structured policy declaration | Machine-readable purpose (absolute URI). See Appendix A for the initial registry. The AS MUST accept any syntactically valid absolute-URI purpose code. For unrecognized codes, the AS MUST display `purpose_description` if present, or the raw URI if not, and MUST NOT reject the request solely because the purpose code is unrecognized. Consent properties associated with purpose codes in the registry are advisory, not protocol-enforced, with the exception of `https://pdpp.dev/purpose/ai_training` (see below). | -| `purpose_description` | string | no | Structured policy declaration | Human-readable purpose, displayed to the user during consent. Clients SHOULD provide this field. When present, the AS MUST display it. For standard purpose codes, the AS MAY display a human-readable label from the registry when `purpose_description` is absent. | -| `access_mode` | enum | yes | Protocol-enforced | `single_use` or `continuous`. See [Section 7](#grant). | -| `retention` | object | no | Structured policy declaration | Requested retention constraints: `{ max_duration, on_expiry }`. | -| `streams` | StreamRequest[] | yes (unless `selection_preset` is used) | Protocol-enforced | Requested streams with per-stream parameters. | -| `selection_preset` | string | no | Protocol-enforced at issuance time | Reference to a SourceDeclaration preset, as an alternative to explicit streams. | -| `client_claims` | object | no | Attributed client claim | Client-authored, non-enforceable claims about this request. See [Client claims](#client-claims). | - -#### Source kinds {#source-kinds} - -These are the two provenance classes an authorization server derives and carries forward. They are defined here because this is where a reader meets the source binding, not because a client sends one. - -| `source.kind` | Meaning | -|---|---| -| `"connector"` | The declaration authority represents a connector-backed source. `source.id` is its public source URI, not a local package name, connector key, storage namespace, account identifier, or runtime binding. A connector declaration remains Core-valid without Collection Profile data. | -| `"provider_native"` | The declaration authority represents the provider's own PDPP data surface. `source.id` is normally the OAuth protected-resource identifier for that surface, not merely the provider's legal-entity URI. | - -A selection request does not carry `source.kind`. The authorization server derives the provenance class from the declaration it accepted for `source.id`, and records it in consent evidence and any issued grant, where a client reads it back through introspection. A client whose policy depends on provenance therefore reads it from the issued grant rather than asserting an expectation in the request; Section 9 states that as a client requirement. The OAuth/RAR binding returns RFC 9396 `invalid_authorization_details` for invalid authorization details. - -#### AI training consent {#ai-training-consent} - -The AS MUST obtain explicit affirmative user consent before issuing any grant with `purpose_code` value `https://pdpp.dev/purpose/ai_training`. This is the sole purpose code with a mandatory consent requirement at the protocol level. - -### Stream selection parameters - -Per-stream, within the `streams` array. All are optional except `name`. - -| Parameter | Type | Status | Description | -|-----------|------|--------|-------------| -| `name` | string | Protocol-enforced | Stream name, or `*` for all streams (resolved at consent time against the retained SourceDeclaration). | -| `necessity` | enum | Consent-flow control at issuance time | `required` (default) or `optional`. Optional streams are presented as user choices during consent. | -| `instance_ids` | string[] | Protocol-enforced | Optional opaque owner-instance handles for this stream. Handles are scoped to issuer, subject, `source.id`, and stream. Omission never means fan-in (reading across more than one connected instance of the same source). The AS resolves exactly one eligible handle or requires an explicit owner choice. | -| `time_range.since` | ISO 8601 | Protocol-enforced | Earliest data to include (inclusive, >=), evaluated against the stream's `consent_time_field`. | -| `time_range.until` | ISO 8601 | Protocol-enforced | Latest data to include (exclusive, <), evaluated against the stream's `consent_time_field`. A hard cap: applies to future resources as well as past ones. | -| `view` | string | Protocol-enforced at issuance time | Named view defined by the authorization server. Mutually exclusive with `fields` in a request; both MUST NOT be present simultaneously. AS returns 400 `invalid_request` if both are present. | -| `fields` | string[] | Protocol-enforced | Field allowlist. Schema-required fields are always included regardless of this list. In v0.1, restricted to top-level field names only. Mutually exclusive with `view`. | -| `resources` | string[] | Protocol-enforced | Specific record IDs to authorize. Values are canonical key strings: minified JSON array for compound keys, plain string for simple keys. The AS validates arity and type against the retained declaration's `primary_key` at grant issuance. The RS filters by exact primary-key match. | - -**Note on `fields`:** At consent resolution, schema-required fields are always included in the resolved field set, regardless of the requested field list, because a record missing its schema-required fields is not a valid record of that stream; the per-stream consent floor is its required fields. - -**Note on `time_range`:** `time_range` is only valid for streams that declare a `consent_time_field`. The authorization server MUST reject selection requests that specify `time_range` on a stream without that field. Its presence in the retained declaration is the authoritative signal that a stream is time-range-capable. - -**Note on wildcards:** `"streams": [{ "name": "*" }]` requests all streams declared by the source. This is resolved against the retained snapshot and frozen as an explicit list in the grant. If the wildcard request includes `instance_ids`, the AS applies the requested handles to every expanded stream and verifies that each handle is eligible for that stream. If it omits `instance_ids`, the usual exactly-one eligible instance rule applies to every expanded stream. - -A wildcard entry MUST be the only entry in `streams`. Otherwise stream names MUST be unique within the request. - -**Note on `streams` vs `selection_preset`:** Exactly one is required. Source validation fails if both or neither are present. The OAuth/RAR binding maps this failure to RFC 9396 `invalid_authorization_details`. - -**Note on defaults:** In a request, omitting `fields` and `view` asks the AS to resolve all permitted fields from the retained snapshot. Omitting `time_range` asks for no temporal constraint. Omitting `instance_ids` never asks for fan-in. The issued grant contains explicit non-empty `fields` and `instance_ids`. Clients SHOULD request only the data they need (see [Section 11, Data Minimization](#data-minimization)). - -### Selection presets - -Source declarations may define selection presets. A client can reference a preset instead of constructing explicit stream selections: - -```json -{ - "type": "https://pdpp.dev/data-access", - "source": { "id": "https://registry.pdpp.dev/connectors/instagram" }, - "purpose_code": "https://pdpp.dev/purpose/personalization", - "access_mode": "single_use", - "selection_preset": "social_summary" -} -``` - -The authorization server expands the preset from the retained snapshot into explicit streams and fields before final owner review and issuance. -Each selection preset MUST NOT contain the same stream name more than once. -Duplicate stream names make the declaration invalid. They are not deferred to grant issuance. - -Every field in the issued grant is derived from either the selection request, client registration, or authorization server policy. The grant never contains values whose source is ambiguous. - ---- - -## 7. Grant {#grant} - -**Note:** This section defines the immutable consent artifact and the constraints a resource server enforces for a token-bound client. Grant database schema, signed-token format, hosted registries, and deployment topology are out of scope for this document. - -A grant is an immutable consent artifact. It is the output of the authorization flow. - -The authorization server issues an access token bound to the grant. The client uses the access token (not the raw grant) to authenticate with the resource server. The resource server resolves the token to the grant and enforces its constraints on every request. Grant lifecycle (active, expired, revoked) is tracked by the authorization server, not stored in the grant itself. - -```json -{ - "version": "0.1.0", - "grant_id": "grt_8f72a1b3", - "issued_at": "2026-04-06T15:00:00Z", - "subject": { "id": "user_abc123" }, - "client": { "client_id": "music_recommendations" }, - "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/spotify" }, - "source_declaration": { "version": "2026-08-11" }, - "purpose_code": "https://pdpp.dev/purpose/personalization", - "purpose_description": "Recommend concerts based on your listening history", - "access_mode": "single_use", - "streams": [ - { - "name": "top_artists", - "instance_ids": ["spotify-account-a"], - "fields": ["id", "name", "genres", "popularity", "source_updated_at"], - "time_constraint": { - "field": "source_updated_at", - "since": "2025-09-28T00:00:00Z" - } - } - ], - "retention": { - "max_duration": "P1Y", - "on_expiry": "delete" - }, - "expires_at": "2027-04-06T00:00:00Z" -} -``` - -### Grant fields - -**The following field table is normative.** [Section 13](#typescript-types)'s TypeScript types are a non-normative convenience mirror; on conflict, this table wins. - -| Field | Type | Required | Status | Description | -|-------|------|----------|--------|-------------| -| `version` | string | yes | Protocol metadata | Tracks the version of this specification's grant schema (not a URL; there is no external schema document to resolve). This contract requires exactly `0.1.0`. | -| `grant_id` | string | yes | Protocol metadata | Unique identifier. | -| `issued_at` | ISO 8601 | yes | Protocol metadata | When the grant was issued. | -| `subject` | object | yes | Identity binding | Exactly `{ id }`. The `subject.id` is an opaque string, unique within the issuing AS's namespace. No format constraint is imposed. | -| `client` | object | yes | Identity binding | Exactly `{ client_id }` or `{ client_id, client_display }`. `client_display`, when retained, is the requester identity metadata resolved by the AS, not unverified inline input. | -| `source` | object | yes | Protocol-enforced | Exact `{ kind, id }` retained from the accepted SourceDeclaration. `id` is authorization identity; `kind` is provenance metadata. | -| `source_declaration` | object | yes | Protocol evidence | `{ version }` records the opaque revision of the exact declaration snapshot used for consent and issuance. It is evidence metadata, not a live lookup authority. | -| `purpose_code` | URI | yes | Structured policy declaration | Machine-readable purpose (see Appendix A). | -| `purpose_description` | string | no | Structured policy declaration | Human-readable purpose. | -| `access_mode` | enum | yes | Protocol-enforced | `single_use` or `continuous`. | -| `streams` | StreamGrant[] | yes | Protocol-enforced | Granted streams. Always expanded; no wildcards. See StreamGrant fields table below. | -| `selection_preset` | string | no | Informational | Which SourceDeclaration preset was selected. The resolved streams and fields remain authoritative. | -| `retention` | object | no | Structured policy declaration | Policy commitment by the data recipient (see below). | -| `expires_at` | ISO 8601 | no | Protocol-enforced | Grant expiry. Absent means no expiry. | - -### StreamGrant fields - -| Field | Type | Required | Status | Description | -|-------|------|----------|--------|-------------| -| `name` | string | yes | Protocol-enforced | Unique stream name within the grant. Always concrete; no wildcards in issued grants. | -| `instance_ids` | string[] | yes | Protocol-enforced | Unique non-empty opaque instance handles scoped to issuer, subject, source ID, and this stream. Multiple handles authorize fan-in only when explicitly listed. Example: two connected Gmail accounts are two instances of the same source kind, each with its own handle. | -| `fields` | string[] | yes | Protocol-enforced | Unique non-empty resolved field allowlist, authoritative for RS enforcement. Top-level field names only. | -| `time_constraint` | object | no | Protocol-enforced | Frozen `{ field, since?, until? }` resolved from the retained declaration. `field` is required and at least one bound is present. `since` is inclusive; `until` is exclusive. | -| `resources` | string[] | no | Protocol-enforced | Authorized record IDs in canonical key string encoding. Absent means all records. | - -Request-only conveniences such as wildcard names, `view`, omitted fields, and omitted instance handles are fully resolved before final owner review and issuance. They are not continuing authority in the grant. Selection provenance may be retained at grant level through `selection_preset`; the concrete stream rows remain authoritative. - -Before the final approval surface is shown, the AS MUST resolve omitted `instance_ids` to exact eligible instance handles or require an explicit owner choice. The final approval artifact MUST include the exact resolved `instance_ids`, stream names, fields, resources, temporal field, `since`, `until`, purpose, retention, client identity, and grant expiry. If `client_claims` are rendered during final review, the final approval artifact and review revision MUST also bind the normalized exact claims with client attribution. Retained consent evidence MUST preserve that binding. The approval mutation MUST bind to an immutable review revision or digest over the authorization decision fields. `client_claims` MUST remain outside the resolved grant and RS enforcement. If instance eligibility or the reviewed revision becomes stale before approval, the AS MUST reject approval and require a new review. - -### Time concepts - -The grant carries three orthogonal time-related concepts that must not be conflated: - -| Concept | Fields | Meaning | -|---------|--------|---------| -| Grant validity period | `issued_at`, `expires_at` | How long the authorization itself is active. | -| Data temporal scope | `streams[].time_constraint` | Which records the client is authorized to see and which record field defines that boundary. | -| Access pattern | `access_mode` | Whether the grant can be exercised once or continuously. | - -A grant can be short-lived (expires in 1 hour) but cover all historical data (no `time_constraint`). A grant can be long-lived but cover only data from the last 6 months (`time_constraint.since`). A grant can be `single_use` but cover a large historical window. These combinations are all valid and distinct. - -### Version layering - -Three independent version axes exist in PDPP. They MUST NOT be conflated: - -| Axis | Field | Meaning | -|------|-------|---------| -| Grant schema version | `grant.version` | Version of the PDPP grant schema. RS MUST reject grants with unsupported major versions, returning 400 `unsupported_version`. | -| Source declaration revision | `grant.source_declaration.version` | Identifies the exact retained declaration snapshot used for consent and issuance. It is opaque evidence metadata. The RS enforces the resolved grant and does not fetch that revision for authorization. | -| HTTP API contract version | `PDPP-Version` request header | Version of the RS HTTP API contract. RS returns 400 `unsupported_version` if the requested version is not supported. If the header is absent, the RS uses the current stable version and returns the selected version in the response header (see [Section 8](#resource-server-interface)). | - -The current persisted-authorization-state reader MUST reject any persisted authorization state whose version or shape it cannot validate against a supported contract before its caller continues introspection or route handling. The reader MUST NOT reconstruct missing authorization or binding facts from current configuration. A deployment that cannot support or explicitly migrate such state MUST require fresh consent. - -### Access modes {#access-modes} - -| Mode | Behavior | -|------|----------| -| `single_use` | The grant is consumed at first token issuance. The AS marks the grant consumed atomically with issuance of the first client access token. The AS MUST reject subsequent attempts to issue new client access tokens against the same consumed grant. The RS honors all tokens issued against the grant until token expiry or revocation. The client MAY retry or resume pagination using the same access token. Failure to complete retrieval before token expiry does not un-consume the grant. | -| `continuous` | The grant is fulfilled repeatedly. The client may query the resource server incrementally over time. Active until expiry or revocation. | - -### Time constraint semantics - -The selection request's `time_range` is resolved against the retained stream `consent_time_field` into the grant's `time_constraint`. The grant freezes that field with the bounds. The filter is: - -``` -record[time_constraint.field] >= time_constraint.since (if since is present) -record[time_constraint.field] < time_constraint.until (if until is present) -``` - -`time_constraint.until` is a hard cap. It applies equally to records that existed at grant issuance and to records created afterward. A `continuous` grant with `time_constraint.until` set to a past date is valid: it is a historical-only grant that will never disclose new records. This is not an error. - -For `continuous` grants without `time_constraint.until`, future records in a granted stream are included as they are collected, provided the frozen field falls within any `since` constraint. Stream names and fields are frozen at consent time; future stream types or fields require a new grant. - -### Standing authorization - -Grants freeze stream names, instance handles, fields, and any time constraint at consent time. Within that resolved scope, future records are included for `continuous` grants. Future stream types or fields are not included; they require a new grant. - -### Grant narrowing - -Grant narrowing (reducing the scope of an existing grant) is not supported in v0.1. Scope reduction is achieved via revoke-and-reissue: the client revokes the existing grant and the user issues a new, narrower grant. Authorization server UIs SHOULD model this flow as revocation followed by a new grant request. - -### Records from revoked grants - -Revocation stops future access only. Records already delivered to the client before revocation are governed by the grant's `retention` policy and applicable legal obligations. PDPP does not retroactively reach into client-side data stores. - -### Retention - -Retention is a structured policy declaration and policy commitment by the data recipient (the client). PDPP does not technically enforce retention. Enforcement is through legal agreements or contractual obligations; a trust registry supports admission and accountability for a retention commitment rather than enforcing it, because Core defines no compliance-evidence query a registry could answer. This is consistent with how OAuth 2.0 treats scope compliance: the protocol makes the commitment legible and machine-readable; external mechanisms enforce it. - -```json -{ - "max_duration": "P6M", - "on_expiry": "delete" -} -``` - -| Field | Type | Description | -|-------|------|-------------| -| `max_duration` | ISO 8601 duration | How long the client may retain collected data. | -| `on_expiry` | enum | `delete` or `anonymize`. Note: `archive` is not supported in v0.1. | - -### Examples - -**Specific data, single use:** -```json -{ - "version": "0.1.0", - "grant_id": "grt_001", - "issued_at": "2026-04-06T15:00:00Z", - "subject": { "id": "user_abc" }, - "client": { "client_id": "concert_app" }, - "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/spotify" }, - "source_declaration": { "version": "2026-08-11" }, - "purpose_code": "https://pdpp.dev/purpose/personalization", - "access_mode": "single_use", - "streams": [ - { - "name": "top_artists", - "instance_ids": ["spotify-account-a"], - "fields": ["id", "name", "genres", "popularity", "source_updated_at"], - "time_constraint": { - "field": "source_updated_at", - "since": "2025-09-28T00:00:00Z" - } - } - ] -} -``` - -**Continuous access for an AI agent:** -```json -{ - "version": "0.1.0", - "grant_id": "grt_002", - "issued_at": "2026-04-06T15:00:00Z", - "subject": { "id": "user_abc" }, - "client": { "client_id": "personal_agent" }, - "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/openai" }, - "source_declaration": { "version": "2026-08-11" }, - "purpose_code": "https://pdpp.dev/purpose/agent_context", - "access_mode": "continuous", - "streams": [ - { - "name": "conversations", - "instance_ids": ["openai-account-a"], - "fields": ["id", "title", "source_created_at", "source_updated_at"] - }, - { - "name": "messages", - "instance_ids": ["openai-account-a"], - "fields": ["id", "conversation_id", "role", "content", "source_created_at"] - } - ] -} -``` - -**Health data with field selection and retention:** -```json -{ - "version": "0.1.0", - "grant_id": "grt_003", - "issued_at": "2026-04-06T15:00:00Z", - "subject": { "id": "user_abc" }, - "client": { "client_id": "sleep_analysis" }, - "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/oura" }, - "source_declaration": { "version": "2026-08-11" }, - "purpose_code": "https://pdpp.dev/purpose/analytics", - "access_mode": "single_use", - "streams": [ - { - "name": "sleep_sessions", - "instance_ids": ["oura-account-a"], - "fields": ["day", "total_sleep_duration", "sleep_score"], - "time_constraint": { - "field": "day", - "since": "2026-01-01T00:00:00Z", - "until": "2026-04-01T00:00:00Z" - } - } - ], - "retention": { "max_duration": "P90D", "on_expiry": "delete" } -} -``` - ---- - -## 8. Resource Server Interface {#resource-server-interface} - -**Note:** This section defines the interoperable record-query and blob-fetch interface under grant enforcement. Authorization-server deployment, storage backend, collection runtime, operator dashboard, and hosted service choices are out of scope for this document. - -The resource server stores records and serves them to clients filtered by grants. This section is normative: a compliant resource server must implement this interface for cross-deployment interoperability. - -### Grant enforcement - -On every request, the resource server: - -1. Resolves the access token through authenticated RFC 7662 introspection or a local equivalent for co-located deployments. Positive introspection results MUST NOT be cached longer than `min(token_exp, 60 seconds)`. -2. Determines `pdpp_token_kind` from the introspection response, then branches: - - **Client:** requires an active resolved authorization context (`active: true`, a resolved grant). Verifies that the requested stream appears in the grant's `streams` list. Selects records only from the explicitly granted `instance_ids` and enforces the grant's `time_constraint`, `fields`, and `resources` constraints. - - **Owner:** enforces subject, source, connection, and operation scope derived from the introspection response. Does not require or synthesize a client grant — an owner token carries none. -3. If all checks pass, returns records filtered accordingly. -4. If any check fails, returns a structured error (see Errors below). - -For owner-token current-capability reads, the effective filter is the permitted owner request filter alone: an owner token carries no grant, so there is no grant filter to intersect. Request filters can only narrow the current owner read and cannot widen it. - -In v0.1, client-token reads do not have request-time predicate filters (see List records below); the resource server enforces the frozen grant constraints and rejects a client request-time predicate filter rather than evaluating it. A future client-filter capability may define intersection semantics. - -The RS MUST NOT re-validate authorization against the current SourceDeclaration. All enforcement constraints are in the resolved grant. Current serving metadata MAY route a granted instance, describe current schemas or query capabilities, or reject a request that cannot currently be served. It MUST NOT widen or reinterpret a stream, instance, field, time field, bound, or resource key. - -**Token type distinction:** The format of the access token is opaque to the Resource Server. The RS MUST determine the token's properties (including `pdpp_token_kind`) solely from the introspection response, never from token syntax. - -### Token introspection - -For separated AS/RS deployments, the RS MUST authenticate to the AS introspection endpoint as required by RFC 7662. The introspection response combines standard RFC 7662 fields with PDPP-defined extensions: - -| Field | Type | Description | -|-------|------|-------------| -| `active` | boolean | Whether the token is currently valid. | -| `pdpp_token_kind` | string | `"owner"` or `"client"`. | -| `subject_id` | string | The subject (user) identifier. | -| `grant_id` | string | The associated grant identifier. Present for client tokens. | -| `client_id` | string | The client identifier. Present for client tokens. | -| `exp` | integer | Optional expiry timestamp (Unix epoch). Omitted when the token has no expiration. | -| `authorization_details` | array | The approved RFC 9396 detail for a client token. It carries the resolved grant enforcement constraints defined in Section 7. | - -The introspection response MUST contain the complete context needed to enforce the request. The separated RS MUST enforce only from that response and MUST NOT make a second AS lookup while handling the request. A co-located AS and RS MAY resolve the same context through a local equivalent. - -**Token kind extensibility:** This specification defines `owner` and `client`. Deployments MAY introduce additional token kinds in companion profiles. A resource server that receives a `pdpp_token_kind` value it does not recognize MUST treat the token as unauthorized for all operations defined in this specification. - -Positive introspection results MUST NOT be cached longer than `min(token_exp, 60 seconds)`. Self-contained JWTs (e.g., signed JWTs) are allowed as an optimization but MUST NOT be the sole revocation mechanism; the RS MUST still be able to check active status through introspection or local equivalent. - -### Authentication - -Two authentication boundaries exist: - -**Owner operations:** `Authorization: Bearer `. Core owner tokens are scoped to a single subject's data store and support the Core owner and self-export operations defined here. The RS MUST derive the `subject_id` from the introspection response and MUST reject any request attempting to access data outside that subject's scope. Companion profiles may define additional owner-authenticated operations. How the owner obtains this token is out of scope (device code flow, API key, or any other mechanism). - -**Client operations** (query records, list streams, fetch blobs): `Authorization: Bearer `. Access tokens are bound to a specific grant. Both token types use RFC 6750 Bearer Token format. The RS distinguishes them via `pdpp_token_kind` in the introspection response. - -**Self-export:** An owner holding a valid owner token MAY query their own data using the standard client query endpoints without a client grant. This is the v0.1 self-export mechanism and does not require a separate grant. Conformant Core RS implementations SHOULD support this capability (see Section 9 conformance item 13). - -### Protected resource metadata {#protected-resource-metadata} - -A resource server MUST publish OAuth 2.0 Protected Resource Metadata as defined in RFC 9728. RFC 9728 Section 3 fixes the document's location: the well-known URI string `/.well-known/oauth-protected-resource` is inserted into the resource identifier between the host component and any path or query component. - -A client that reaches the resource server without a usable access token learns that location from the response itself. On a 401, the resource server MUST include a `WWW-Authenticate: Bearer` challenge, as RFC 6750 Section 3 requires. It MUST set `error="invalid_token"` when a token was presented and rejected. And it MUST include the `resource_metadata` parameter RFC 9728 Section 5.1 defines, carrying the URL of this document. This is the bootstrap path: without it a client holding no token has no specified way to discover where to authenticate. - -The `resource` member is the resource server's own identifier, as RFC 9728 Section 2 requires. For a `provider_native` source it is the same identifier as the declaration's `source.id`, which is the binding Section 5 already requires an authorization server to check before consent. A resource server that serves several sources publishes one metadata document per resource identifier rather than one document listing them. - -PDPP defines four additional members. RFC 9728 Section 2 permits additional parameters and RFC 9728 Section 3.2 requires a reader to ignore any parameter it does not understand, so a generic OAuth client is unaffected by their presence. Each name carries the `pdpp_` prefix to keep it distinct from a future registered parameter; RFC 9728 does not itself prescribe a naming convention. - -| Member | Meaning | -| --- | --- | -| `pdpp_core_query_base` | The base path the Section 8 endpoint paths extend, so a client composes a record query without assuming a version segment. | -| `pdpp_token_kinds_supported` | The `pdpp_token_kind` values this resource server accepts, drawn from the kinds Section 8 defines. | -| `pdpp_self_export_supported` | Whether an owner token may read the owner's own data through the client query endpoints without a client grant. | -| `pdpp_provider_connect_version` | The PDPP version this resource server's interface implements, which a client would otherwise learn only from the `PDPP-Version` negotiation on a first request. | - -`resource_name` is RFC 9728's own member for a human-readable resource name, not a PDPP extension; a resource server SHOULD publish it because a consent surface has no other name to display for the resource. - -RFC 9728 Section 2 makes `authorization_servers` OPTIONAL, and notes that in some deployments the set of authorization servers is not enumerable. A PDPP resource server MUST publish `authorization_servers` when that set is enumerable, so a client can reach the issuer without prior configuration. When the set is not enumerable, the resource server MUST omit the member rather than publish a partial list. The client then obtains the issuer from the grant it already holds, or from the deployment's own onboarding. Core defines no other discovery path for that case. - -The `capabilities` member is defined by the extension profiles that advertise into it, not by Core. Core neither requires it nor constrains its contents, and a resource server that implements no extension omits it. - -### Endpoints - -#### List streams - -``` -GET /v1/streams -Authorization: Bearer -``` - -Returns streams with record counts, scoped by token kind: for a client token, the streams present in the resolved authorization context; for an owner token, the streams in the subject-scoped data store the owner token is scoped to. - -**Response:** -```json -{ - "object": "list", - "data": [ - { - "object": "stream", - "name": "conversations", - "record_count": 2196, - "last_updated": "2026-04-06T15:01:00Z", - "freshness": { - "captured_at": "2026-04-06T15:01:00Z", - "status": "current", - "last_attempted_at": "2026-04-06T15:01:00Z" - } - }, - { - "object": "stream", - "name": "messages", - "record_count": 48302, - "last_updated": "2026-04-06T15:01:00Z", - "freshness": { - "captured_at": "2026-04-06T15:01:00Z", - "status": "current", - "last_attempted_at": "2026-04-06T15:01:00Z" - } - } - ] -} -``` - -#### Get stream metadata {#stream-metadata} - -``` -GET /v1/streams/{stream} -Authorization: Bearer -``` - -A client-token caller may fetch metadata only for a stream present in its resolved authorization context. An owner-token caller may fetch metadata for streams in the subject's data store the owner token is scoped to. Once access is authorized, the response body is actor-specific: - -- **Owner token:** the metadata document is returned whole — full current schema, query capabilities, views, and relationships — rather than field-projected by any grant. An owner token carries no grant, so there is nothing to project against. -- **Client token:** the response is a closed projection derived from the resolved authorization context: only the granted stream's explicitly granted fields, and only immutable/frozen grant facts. Current query, view, relationship, filter, expansion, and aggregation capabilities MUST NOT appear unless that capability is explicitly part of a future frozen grant vocabulary. Current metadata MAY report availability/freshness or reject an unavailable operation, but MUST NOT make the grant appear broader or semantically different than what was issued. A source declaration change made after the grant was issued (e.g., a new field) MUST NOT become visible through this endpoint for that grant. - -**Owner-token response** (full current metadata): - -```json -{ - "object": "stream_metadata", - "name": "top_artists", - "schema": { }, - "primary_key": ["id"], - "cursor_field": "source_updated_at", - "consent_time_field": "source_updated_at", - "selection": { - "fields": true, - "resources": false - }, - "query": { - "range_filters": { - "source_updated_at": ["gte", "gt", "lte", "lt"] - }, - "expand": [ - { "name": "messages", "default_limit": 10, "max_limit": 50 } - ] - }, - "freshness": { - "captured_at": "2026-04-06T15:01:00Z", - "status": "current", - "last_attempted_at": "2026-04-06T15:01:00Z" - }, - "views": [ - { "id": "basic", "label": "Artist names and genres", "fields": ["id", "name", "genres"] } - ], - "relationships": [ - { "name": "messages", "stream": "messages", "foreign_key": "conversation_id", "cardinality": "has_many" } - ] -} -``` - -**Client-token response** (closed projection of a grant frozen to fields `id`, `name`, `source_updated_at`; no range filters, views, or relationships were granted): - -```json -{ - "object": "stream_metadata", - "name": "top_artists", - "schema": { - "properties": { - "id": { }, - "name": { }, - "source_updated_at": { } - } - }, - "primary_key": ["id"], - "cursor_field": "source_updated_at", - "consent_time_field": "source_updated_at", - "selection": { - "fields": true, - "resources": false - }, - "query": { }, - "freshness": { - "captured_at": "2026-04-06T15:01:00Z", - "status": "current", - "last_attempted_at": "2026-04-06T15:01:00Z" - }, - "views": [], - "relationships": [] -} -``` - -#### Freshness metadata - -A resource server MAY attach a `freshness` object to stream listings, stream metadata, and record-list responses. - -Freshness is server-observed disclosure metadata, not a grant constraint. It reports what the server knows about the recency of the underlying data relevant to the response. It does not guarantee that the source has not changed since `captured_at`, and it does not widen or narrow access rights. - -| Field | Type | Description | -|-------|------|-------------| -| `captured_at` | ISO 8601 or null | Time of the most recent successful collection or source confirmation that could have affected the response. null if unknown. | -| `status` | enum | `current`, `stale`, or `unknown`. `stale` means the server believes the stored data may no longer reflect source state based on local collection policy or failed refresh attempts. | -| `last_attempted_at` | ISO 8601 or null | Time of the most recent attempted refresh relevant to the response, if tracked. | - -#### List records {#list-records} - -``` -GET /v1/streams/{stream}/records -Authorization: Bearer -``` - -Returns records from a stream, filtered by the grant and any additional request parameters. - -**Query parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `limit` | integer | Records per page. Default 25, max 100. A request for more than 100 is clamped to 100 and the response carries a non-fatal `limit_clamped` warning (see below), not an error. | -| `cursor` | string | Opaque pagination token from a previous response. Clients MUST NOT parse or construct cursor tokens. | -| `order` | enum | `desc` (default) or `asc`. | -| `filter[{field}]` and `filter[{field}][op]` | string | Owner-token current-capability filters only. Client-token requests MUST reject exact and range forms in v0.1. | -| `view` | string | Owner-token current-capability request for records projected to a named view. Client-token records requests MUST reject `view`; clients use explicit `fields` or the field projection already frozen into the grant. Mutually exclusive with `fields`. | -| `fields` | comma-separated | Sparse fieldset. Schema-required fields are always included. In v0.1, restricted to top-level field names only. Mutually exclusive with `view`. | -| `expand[]` | string | Owner-token current-capability request to expand a relation declared under `query.expand`. Depth is 1. Expanded relations appear under the `expanded` key on the parent record. Client-token requests MUST reject this parameter in v0.1. | -| `expand_limit[{relation}]` | integer | Owner-token current-capability limit for an expanded `has_many` relation. Valid only for relations declared under `query.expand`; defaults and limits come from that declaration. Client-token requests MUST reject this parameter in v0.1. | -| `changes_since` | string | Opaque incremental-sync token from a previous session (distinct token space from `cursor`). Returns only records whose grant-authorized projection changed since that cursor, plus tombstones for deletions. Use `next_changes_since` from the terminal page to seed the next session. Returns HTTP 410 Gone with error code `cursor_expired` if the cursor has expired. | - -The durable client-token base query surface in v0.1 is: `limit`, `cursor`, `order`, `fields`, `changes_since`, and blob fetch. Exact and range `filter[...]`, `expand[]`, and `expand_limit[...]` parameters are not part of the client-token surface. Owner-token current-capability reads MAY support exact and declared range filters, `view`, and declared expansion; those reads consult current serving metadata. Advanced stream-specific query power MUST be declared in stream metadata under `query`. - -Unknown query parameters and unsupported query shapes MUST be rejected with HTTP 400 and MUST NOT be silently ignored. - -**Non-fatal warnings:** A list response MAY carry a `meta.warnings[]` array reporting non-fatal lossiness that the server resolved without failing the request. Each entry has a stable `code` and a human-readable `message`; clients SHOULD branch on `code`, not on message text. A `limit` above the maximum is the canonical case: the RS returns the bounded page and a `limit_clamped` warning rather than silently dropping the excess or returning an error. Clients page forward with the returned cursor instead of expecting a larger page. Warnings are not errors and MUST NOT change the HTTP status. - -Client-token requests that contain any exact or range `filter[...]` parameter MUST be rejected with HTTP 400 `invalid_request` before the RS consults current SourceDeclaration or serving metadata. This rejection applies regardless of whether the field or operator would otherwise be declared. Owner-token current-capability reads MAY accept exact filters on declared top-level scalar fields and range filters explicitly declared by current serving metadata. -Unknown fields, non-scalar fields, and unsupported range shapes return HTTP -400. Owner subject, source, and connection scope are enforced independently. An -owner token has no client grant field projection. - -Client-token requests that contain `expand[]` or `expand_limit[...]` MUST be rejected with HTTP 400 `invalid_request` before the RS consults current SourceDeclaration or serving metadata. A v0.1 resolved grant does not freeze relationship identity, target stream, foreign-key join semantics, cardinality, or expansion limits. Current relationship metadata therefore cannot interpret client grant rights. Owner-token current-capability reads MAY use declared expansion against current serving metadata. - -For owner-token current-capability reads, range filters (`gte`, `gt`, `lte`, `lt`) apply only to fields declared in `query.range_filters`. Nested paths, arrays, OR grammar, and full-text search are not part of v0.1. - -For owner-token current-capability reads, expansion is declaration-driven. A relation is structurally present if listed under `relationships`, but it is only expandable if declared under `query.expand`. `expand_limit[{relation}]` is only valid for declared `has_many` relations. - -**Stable sort:** Records are sorted by `(cursor_field, primary_key)` for cursor safety. Null or absent `cursor_field` values sort after present values. - -Page cursors are direction-bound: a client MUST follow a `next_cursor` with the same `order` value that produced it. To change direction, the client MUST restart pagination without a cursor. Resource servers MUST reject order-mismatched page cursors as `invalid_cursor`. - -**Incremental sync for mutable streams:** Pass `changes_since` to retrieve only records changed since a previous sync. The resource server returns changed records within the grant's authorized field projection. If a record was deleted, a tombstone entry is included. If the cursor has expired (HTTP 410 Gone with error code `cursor_expired`), the client MUST perform a full re-sync. - -Eligibility for `changes_since` MUST be computed on the grant-authorized projection, not on the unprojected record. Returning a record whose authorized projection is unchanged is a protocol violation because it leaks that hidden fields changed. - -If a `changes_since` response is paginated, all pages in that session MUST be anchored to the same session horizon selected on the first page. New writes arriving after page 1 MUST NOT appear in later pages of that same session; they surface in the next session via the terminal-page `next_changes_since`. - -**Invalid owner filter:** An owner-token current-capability filter on an unknown, non-scalar, or unsupported field/operator returns HTTP 400 `invalid_request` or `unknown_field`, as applicable. Client-token predicate filters are rejected earlier under the v0.1 client-filter rule. - -**Expansion:** A client-token expansion request is rejected with 400 `invalid_request` before declaration lookup. For an owner-token current-capability read, requesting an undeclared relation returns 400 `invalid_expand`. Expansion never widens the current owner read scope. - -**Response:** -```json -{ - "object": "list", - "url": "/v1/streams/conversations/records", - "has_more": true, - "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMy0yNVQxODoyMjoxMVoiLCJpZCI6ImNvbnZfMDFKUVc4TTJSNyJ9", - "next_changes_since": "eyJjaGFuZ2VzX3NpbmNlIjoiMjAyNi0wNC0wNlQxNTowMTowMFoifQ", - "freshness": { - "captured_at": "2026-04-06T15:01:00Z", - "status": "current", - "last_attempted_at": "2026-04-06T15:01:00Z" - }, - "data": [ - { - "object": "record", - "id": "conv_01JQW8M2R7", - "stream": "conversations", - "data": { - "id": "conv_01JQW8M2R7", - "title": "Trip planning", - "source_created_at": "2026-03-25T18:22:11Z" - }, - "emitted_at": "2026-04-06T15:01:00Z" - } - ] -} -``` - -The terminal page of a `changes_since` request (i.e., `has_more: false`) MUST include `next_changes_since`. - -#### Get a single record - -``` -GET /v1/streams/{stream}/records/{id} -Authorization: Bearer -``` - -Returns a single record by primary key. The `{id}` path parameter is the percent-encoded canonical key string. Owner-token current-capability reads support `expand[]`; client-token requests reject it in v0.1 before declaration lookup. - -#### Delete a record (owner-authenticated) - -``` -DELETE /v1/streams/{stream}/records/{id} -Authorization: Bearer -``` - -Permanently removes a record from the stream. The RS may implement this as a tombstone internally. Returns 204 No Content on success. The `{id}` path parameter is the percent-encoded canonical key string. - -#### Get a blob - -``` -GET /v1/blobs/{blob_id} -Authorization: Bearer -``` - -The resource server authorizes blob access by verifying that: - -1. The grant includes a stream containing a record that references this `blob_id`. -2. The referencing record passes all grant filters. -3. The `blob_ref` field is included in the grant's authorized field projection. - -A `blob_id` alone does not grant access. The client MUST have discovered the blob through an authorized record. - -**Direct response** MUST include: -- `Content-Type` (IANA media type) -- `Content-Length` if known -- `Cache-Control: private, no-store` -- `Accept-Ranges: bytes` if range requests are supported - -**Redirect response** (HTTP 302) MUST include: -- `Location` header pointing to a short-lived signed URL (valid for at least 60 seconds) -- `Cache-Control: no-store` - -A stale or unknown `blob_id` returns 404 `blob_not_found`. - -`HEAD` is supported for size checks. `Range` headers are recommended for large files. - -### Errors {#errors} - -Every non-2xx response returns a structured error: - -```json -{ - "error": { - "type": "permission_error", - "code": "grant_stream_not_allowed", - "message": "Grant does not include stream 'messages'.", - "param": "expand[0]", - "request_id": "req_01JQXA3N9Y" - } -} -``` - -Clients MUST treat unrecognized error codes as opaque and fall back to the actual HTTP status code and applicable response headers. - -**Authority and forward compatibility.** - -1. The actual HTTP status code and applicable response headers are authoritative for generic HTTP semantics, including success or failure, authentication challenges, redirection, and retry timing. -2. A recognized `error.type` or `error.code` MAY refine PDPP-specific category, presentation, or recovery behavior only when its defined semantics are compatible with the actual status code and headers. -3. An absent, unknown, malformed, or status-incompatible `type` or `code` is opaque and MUST NOT override the actual status code or relevant headers. -4. Unknown identifiers MUST NOT cause parse failure. - -Clients MAY retain unknown identifiers for diagnostics, subject to local size limits, safe rendering/escaping, and privacy policy. - -This makes a future error code safe to introduce: an older client keeps handling the response by status and headers, whatever the new `code` or `type` says. - -| Code | HTTP Status | Type | Meaning | -|------|------------|------|---------| -| `invalid_cursor` | 400 | `invalid_request_error` | Cursor token is malformed or unrecognized. | -| `invalid_request` | 400 | `invalid_request_error` | Malformed request parameter or mutually exclusive parameters. | -| `invalid_expand` | 400 | `invalid_request_error` | Relation is not declared as expandable. | -| `unknown_field` | 400 | `invalid_request_error` | Requested field not in stream schema. | -| `unsupported_version` | 400 | `invalid_request_error` | `PDPP-Version` header specifies unsupported version, or grant references unsupported schema version. | -| `authentication_error` | 401 | `authentication_error` | Missing or invalid access token. | -| `authorization_state.unsupported_legacy_shape` | 401 | `authentication_error` | Persisted authorization state does not match a supported shape. Fresh consent is required when no migration applies. | -| `field_not_granted` | 403 | `permission_error` | Requested client field exceeds the grant's authorized field projection. | -| `insufficient_scope` | 403 | `permission_error` | Expansion requests a stream not in the grant. | -| `grant_stream_not_allowed` | 403 | `permission_error` | Stream not in grant. | -| `grant_time_range_exceeded` | 403 | `permission_error` | Request filters exceed the grant's frozen `time_constraint`. | -| `grant_expired` | 403 | `permission_error` | Grant has expired. | -| `grant_revoked` | 403 | `permission_error` | Grant has been revoked. | -| `grant_invalid` | 403 | `permission_error` | Resolved grant is malformed or cannot be served without changing its authorization meaning. | -| `blob_not_found` | 404 | `not_found_error` | `blob_id` is unknown or stale. | -| `not_found` | 404 | `not_found_error` | Stream or record not found. | -| `cursor_expired` | 410 | `gone_error` | `changes_since` cursor is too old; full re-sync required. | -| `rate_limit_exceeded` | 429 | `rate_limit_error` | Too many requests. Includes `Retry-After` header. | -| `api_error` | 500 | `api_error` | Internal server error. | - -### API versioning - -API version is specified via header: - -``` -PDPP-Version: 2026-04-06 -``` - -If the `PDPP-Version` header is absent, the RS uses the current stable version and returns the selected version in the `PDPP-Version` response header. If the requested version is not supported, the RS returns 400 `unsupported_version`. - -Every response includes a `Request-Id` header for debugging. - ---- - -## 9. Conformance {#conformance} - -This section defines what it means to implement each PDPP role. Conformance claims should reference this section. - -Conformance is role- and behavior-based. A conformant implementation is not required to use any particular vendor-hosted service, token, chain, centralized registry operator, domain, or repository deployment. - -### Authorization Server conformance - -A conformant authorization server: - -1. Accepts selection requests using the RFC 9396 `authorization_details` envelope with `type: "https://pdpp.dev/data-access"`. -2. Validates selection requests against one retained SourceDeclaration snapshot: rejects unknown streams, unsupported selection parameters (e.g., `time_range` on a stream without `consent_time_field`), and unrecognized selection presets. -3. Issues grants that conform to the grant schema defined in Section 7 (normative field tables). All grant fields are derived from the selection request, client registration, or AS policy. -4. Expands wildcards and selection presets into explicit stream names, fields, per-stream instance handles, resources, and frozen time constraints before issuing the grant. -5. Produces a binding-neutral Source validation failure when a request contains both or neither of `streams` and `selection_preset`. The OAuth/RAR binding maps it to RFC 9396 `invalid_authorization_details`. -6. MUST NOT reject a `purpose_code` solely because it is not in the PDPP registry. For unrecognized codes, displays `purpose_description` if present, or the raw URI. MAY reject a `purpose_code` based on local policy. -7. Renders requester identity metadata, declaration-authored data descriptions, structured policy declarations, and client-authored claims as semantically distinct categories during consent. MUST attribute `client_claims` to the client and MUST NOT present them as protocol-enforced terms. If `client_claims` are rendered during final review, binds the normalized exact claims into the immutable final approval artifact and review revision, and preserves that binding in retained consent evidence, without adding them to the resolved grant or RS enforcement. -8. Tracks grant lifecycle (active, expired, revoked). Reflects revocation immediately in introspection responses (`active: false`). -9. Issues access tokens bound to specific grants. Access tokens include the PDPP introspection extension fields. -10. For `single_use` grants, consumes the grant atomically with first client-token issuance and rejects subsequent attempts to issue new client access tokens against that grant. -11. Validates stream/field/view/resource-id shape at grant issuance. -12. MUST NOT define a view including fields absent from the retained SourceDeclaration schema. -13. Resolves view names to field lists at issuance time; stores resolved `fields` in the `StreamGrant`. Client-token record reads reject query-time `view` in v0.1. Owner-token current-capability reads MAY resolve current views. -14. Obtains explicit affirmative user consent before issuing grants with `purpose_code: "https://pdpp.dev/purpose/ai_training"`. -15. Resolves omitted instance IDs before the final approval surface. Binds - exact resolved instances and all final decision fields to an immutable - review revision or digest. Rejects stale approval if eligibility or the - reviewed revision changes before approval. -16. Retains one exact SourceDeclaration snapshot through request validation, consent display, narrowing, issuance, and consent evidence. A later current declaration never substitutes for it. -17. Returns 400 `unsupported_version` if `PDPP-Version` header specifies an unsupported version. -18. For a separated AS and RS, authenticates the RS at the RFC 7662 introspection endpoint and returns the complete grant enforcement context in one response. -19. Consumes each OAuth authorization code atomically on its first successful redemption. Rejects every later redemption with `invalid_grant` and does not issue another token. -20. Issues refresh tokens only for `continuous` grants, or for a grant package only when every child grant is `continuous`. It rotates refresh tokens by family. Reuse of a superseded token revokes the family and every family-linked access token, returns `invalid_grant`, and requires fresh authorization. -21. Rejects unsupported persisted authorization state before introspection or request handling. Does not reconstruct missing facts from current configuration and requires fresh consent when no migration applies. - -### Resource Server conformance - -A conformant Core RS: - -1. Implements the query endpoints defined in Section 8: list streams, get stream metadata, list records, get a single record, get a blob, delete a record (owner-authenticated). -2. Enforces grant constraints on every client request: stream membership, explicit instance handles, frozen `time_constraint`, `fields` allowlist, and `resources` filter. -3. In a separated deployment, resolves access tokens through authenticated RFC 7662 introspection, enforces only from that response, and makes no second AS lookup while handling the request. A co-located deployment may use a local equivalent. Caches positive results no longer than `min(token_exp, 60 seconds)`. -4. Distinguishes owner tokens from client tokens via `pdpp_token_kind`. -5. For owner tokens, computes the effective filter as the permitted owner request filter alone (there is no grant filter). For client tokens in v0.1, rejects request-time predicate filters and enforces the frozen grant constraints. -6. Returns structured errors as defined in Section 8 (unified error table). -7. Supports incremental sync via `changes_since` for `mutable_state` streams, including tombstone entries, omission of records whose grant-authorized projection did not change, and HTTP 410 with error code `cursor_expired` on cursor expiry. -8. Returns `next_changes_since` on the terminal page of every `changes_since` response. -9. Rejects client-token exact and range `filter[...]` parameters with 400 - `invalid_request` before consulting current declaration metadata; owner-token current-capability reads MAY retain declared filter behavior. -10. Rejects unknown query parameters and unsupported query shapes with 400 instead of silently ignoring them. -11. Implements the `PDPP-Version` header negotiation. -12. Scopes owner token access to a single subject's data store; derives `subject_id` from introspection response. -13. SHOULD support owner-authenticated access to the `/v1/streams/{stream}/records` query endpoints without a client grant, allowing the data subject to export their own data directly (self-export). -14. For owner-token stream-metadata reads, returns the full current stream metadata within the owner's subject/source/connection scope, including current query, view, and relationship capabilities. -15. For client-token stream-metadata reads, returns only a projection derived from the resolved authorization context: the granted stream and its explicitly granted fields, and immutable/frozen grant facts. MUST NOT include current view, relationship, filter, expansion, or aggregation capability unless that capability is explicitly part of a future frozen grant vocabulary, and MUST NOT surface a source-declaration change made after grant issuance. -16. Publishes RFC 9728 protected resource metadata at the location RFC 9728 Section 3 derives from its resource identifier, carrying `resource`, the four `pdpp_`-prefixed members defined in Section 8, and `authorization_servers` when its issuer set is enumerable. Returns a `WWW-Authenticate: Bearer` challenge on 401 per RFC 6750 Section 3, carrying the RFC 9728 `resource_metadata` parameter. - -Collection resource servers, connector runtimes, and connectors make no separate conformance claim in v0.1. A connector conforms to PDPP as Section 1 states: by producing a source declaration valid under Section 5 and serving its data through a resource server conforming to Section 8. The informative [PDPP Collection Profile](spec-collection-profile) describes runtime behavior and defines no conformance requirement. - -### Client conformance - -A conformant client: - -1. Submits selection requests using the RFC 9396 `authorization_details` envelope. -2. Uses access tokens (not raw grants) to authenticate with the resource server. -3. Treats `cursor` and `changes_since` tokens as opaque and from distinct token spaces. MUST NOT use a `next_cursor` value as a `changes_since` parameter. -4. Stores `next_changes_since` from the terminal page of a `changes_since` response for use in the next sync session. -5. Respects HTTP 410 `cursor_expired` responses by performing a full re-sync rather than retrying with the expired cursor. -6. Honors retention commitments declared in the grant. -7. Treats unrecognized error codes as opaque, falling back to the exact HTTP status code and applicable response headers rather than failing on an unknown code. Takes the actual status code and headers as the authoritative outcome. Uses a recognized `error.type` only to refine category or presentation, and only when compatible with that outcome. Ignores an absent, unrecognized, or status-incompatible `type` for control flow, and never fails to parse on an unknown `code` or `type`. -8. Where local policy depends on source provenance, MUST read `source.kind` from the issued grant and apply that policy before first use of the records. A client MUST NOT assume a provenance class it did not read from the grant. A client with no provenance-dependent policy has nothing to check. - -### Conformance test suite - -A formal conformance test suite is planned but is not defined in v0.1. This is out of scope for the current version. - ---- - -## 10. Security Considerations {#security} - -### Declaration retrieval hygiene - -An authorization server SHOULD enforce configured response-size, time, and redirect-depth limits when retrieving a declaration, and SHOULD resolve DNS freshly for each connection attempt. These limits are set by local judgment: they do not alter the declaration or grant semantics that peers must share, and a peer cannot observe which values a server chose. - -Two related properties are not tuning choices and are stated in Section 5 as requirements. The safety of what a server renders is a shared invariant. So is the timing of the address check. A stale validation is what a rebinding attack exploits, so Section 5 requires the destination address be validated immediately before each connection attempt. The freshness of the DNS result above is a hygiene preference; the timing of the address check is not. - -### Token security - -PDPP defines two token kinds at the resource server boundary: owner tokens and client tokens. Both use RFC 6750 Bearer Token format on the wire. The RS distinguishes them via `pdpp_token_kind` in the introspection response, not by token syntax. - -For separated AS/RS deployments, the RS MUST authenticate to the AS introspection endpoint (RFC 7662) and enforce only from its response. It MUST NOT make a second AS lookup while handling the request. For co-located deployments, a local equivalent (shared database lookup or function call) is acceptable. Self-contained JWTs are allowed as an optimization but MUST NOT be the sole revocation mechanism. - -Positive introspection results MUST NOT be cached longer than `min(token_exp, 60 seconds)`. This bounds the propagation window for revocation. - -An access token issued with or from a refresh-token family MUST be linked to that family and MUST have a short, token-specific expiration no later than the family or grant expiration. A token response MUST derive `expires_in` from the access token's persisted expiration. It MUST omit `expires_in` when the access token has no expiration. An RFC 7662 response MUST likewise omit `exp` when no expiration exists. - -Every successful OAuth token response that contains an access token or refresh token MUST include `Cache-Control: no-store` and `Pragma: no-cache` before the response is serialized. This applies to authorization-code, refresh-token, and device-code exchanges, including package-scoped variants. - -An authorization code MUST be consumed atomically on its first successful redemption. A later redemption, including one with the same valid PKCE verifier, MUST return `invalid_grant` and MUST NOT issue another token. - -When an authorization server issues refresh tokens for a `continuous` grant, each token MUST belong to a family and MUST rotate after successful use. The AS MUST atomically supersede the presented token and issue one active successor. Reuse of any superseded token, including a retry after a lost successful response, MUST revoke the token family and every access token linked to that family, return `invalid_grant`, and require fresh authorization. -Introspection MUST report every family-linked access token inactive after the replay is detected. An AS MUST NOT issue refresh tokens for a `single_use` grant. It MUST NOT issue one for a grant package unless every child grant is `continuous`. On upgrade, an implementation MUST NOT infer family linkage for an existing bearer. Any live refresh family without persisted bearer linkage MUST be revoked together with its grant- or package-bound bearer tokens and MUST require fresh authorization. This behavior follows [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700), Section 4.14.2. - -**Sender-constrained tokens (non-normative):** Bearer tokens (RFC 6750) are the v0.1 baseline. Deployments handling sensitive standing access SHOULD consider sender-constrained tokens, which bind a token to a client-held key so that possession of the token alone is not sufficient to use it. DPoP (RFC 9449) and mutual-TLS certificate binding (RFC 8705) are both compatible with PDPP's introspection-based design. A formal optional hardening profile is a candidate for a future version. - -### Grant integrity - -The grant is designed to be signable. The `subject` and `client` fields support future JWS/JWT signatures. Implementations MUST treat grants as tamper-sensitive. Grant signing and a formal token format are deferred to a future version; the current design is compatible with adding them without breaking changes. - -Large `authorization_details` payloads may exceed URL length limits. Production deployments SHOULD use Pushed Authorization Requests (PAR, RFC 9126). - -### Credential handling - -INTERACTION_RESPONSE messages in the Collection Profile may contain passwords and OTP codes. Runtimes MUST NOT log or persist credential data. See the [PDPP Collection Profile](spec-collection-profile) for details. - -### Connector trust - -In the Collection Profile, connectors receive credentials via the INTERACTION channel. A malicious connector could exfiltrate credentials. Production deployments SHOULD mitigate this by sandboxing connector processes (restricting network egress), using connectors from trusted registries only, or having the runtime authenticate on behalf of the connector and pass only session tokens. A formal connector trust model is deferred. - -### Trust boundary responsibilities - -| Role | Responsibilities | -|------|----------------| -| **Authorization Server** | Validates purpose-code syntax and local policy; authenticates user; preserves semantic distinctions on the consent surface; validates stream/field/view/resource-id shape at grant issuance; resolves views to field lists; issues access tokens; maintains grant lifecycle. | -| **Resource Server** | Validates token via introspection; for client requests, enforces stream, instance, fields, frozen time constraints, and resources against the resolved grant; for owner requests, enforces subject/source/connection/operation scope without requiring a grant; never reinterprets authorization from a current declaration. | -| **Client** | Submits well-formed selection requests; uses access tokens; terminates on revocation; honors retention commitments. | - -### Revocation {#revocation} - -There is no push revocation channel in v0.1. Revocation propagation is bounded by the introspection cache TTL (maximum 60 seconds). The AS MUST reflect revocation immediately in introspection responses (`active: false`). A client will receive a 403 `grant_revoked` response no later than 60 seconds after revocation. - -Upon receiving any 403 `grant_revoked` response, the client MUST stop further requests against that grant. Companion profiles define how their active work is terminated. - -Revocation stops future access only. Data already delivered to the client before revocation is governed by the grant's `retention` policy and applicable legal obligations. - -Revocation is not deletion. v0.1 does not define an active erasure signal or downstream deletion callback. - ---- - -## 11. Privacy Considerations {#privacy} - -### Data minimization {#data-minimization} - -Stream-level and field-level selection implements the GDPR principle of data minimization. Clients SHOULD request only the data they need for their stated purpose. Authorization servers SHOULD display the specific fields and streams being requested during consent. - -### Purpose limitation - -The `purpose_code` URI enables purpose declaration, consent display, registration policy, and implementation-defined audit or transparency mechanisms. Authorization servers MAY restrict client registrations to specific purpose codes. - -### Auditability and transparency boundary - -PDPP core defines the authorization, grant, and disclosure semantics that make auditing and transparency possible. This includes stable identifiers and state transitions such as `grant_id`, `client_id`, `subject_id`, `purpose_code`, stream and resource identifiers, timestamps, and grant lifecycle states. - -PDPP core does not define a local audit-log schema, storage model, retention period for operational logs, or a user-facing disclosure-history interface. Implementations MAY maintain local records of grant issuance, disclosure, sync, token use, and revocation under local policy. - -If interoperable audit or transparency events are standardized in the future, they SHOULD be defined in a separate companion profile rather than by extending the core grant or query semantics. - -### Retention - -The `retention` field is a structured policy declaration and policy commitment by the data recipient. PDPP does not technically enforce retention. Enforcement is through legal agreements or contractual obligations, with a trust registry supporting admission and accountability rather than enforcement. This is an intentional design choice, consistent with how OAuth 2.0 treats scope compliance. - ---- - -## 12. Scope and Boundaries - -### In scope (v0.1) - -- Parameterized grants for user-owned data -- Flat relational streams with declared schemas -- Binary data references (`blob_ref`) -- Cross-stream references within a single subject (`resource_ref`) -- SourceDeclaration shared by connector-backed and provider-native sources -- Resource server query API with cursor-based pagination and grant enforcement -- Incremental sync for `mutable_state` streams via `changes_since` -- Tombstones for deleted records -- Owner-authenticated user erasure (`DELETE /v1/streams/{stream}/records/{id}`) -- Self-export via owner token (SHOULD-level Core RS conformance, see Section 9 item 13) -- Conformance definitions for all roles - -### Out of scope (v0.1) - -| Concern | Status | -|---------|--------| -| Authorization server interface | Introspection endpoint contract defined here; full AS interface informational only in v0.1 | -| Ingest and sync-state endpoints | Defined by the Collection Profile; not required for Core RS | -| Conformance test suite | Planned but not defined in v0.1 | -| Webhook / push ingestion | Deferred; see spec-deferred | -| Source lifecycle actions | Deferred (e.g., deleting source data after export); see spec-deferred | -| Event-driven collection triggers | Deferred; architecturally distinct from the pull-based Collection Profile | -| Grant signing and token format | Deferred; current design is compatible | -| Trust registry and connector certification | Deferred | -| Consent screen visual design | Surface-specific; semantic rendering obligations remain in scope | -| Local audit-log schema and user-facing access history | Deployment-specific; core defines auditable protocol primitives only | -| Interoperable audit/transparency event format | Separate companion profile if standardized | -| Point-in-time reconstruction | Deferred (reconstructing full state at a past timestamp) | -| Canonical view naming vocabulary | Deferred; will be informed by implementation experience | -| Predicate-based grant scoping | Deferred; see spec-deferred for subset template design direction | -| Derivative data | Deferred and unresolved; v0.1 authorizes reads of declared streams and states no default for the output of compute over them. See spec-deferred | -| Cross-source category grants | Deferred; grants bind to a single `source.id` in v0.1. See spec-deferred | -| Real-time streaming | Different spec needed | - -### Predicate-based grant scoping {#predicate-based-grant-scoping} - -v0.1 grants narrow access only by stream selection, named view or field projection, time range, and explicit resource identifiers. Generic predicate expressions (e.g., `filter[sender_domain]=amazon.com` as a grant parameter) are not supported. - -**Request-time filters are not grant scope.** Owner-token current-capability reads MAY use `filter[...]` to narrow a result set, but those filters do not narrow any client grant. Client-token requests do not support request-time filters in v0.1; a client that needs a semantically bounded subset requests a named stream declared by the SourceDeclaration. - -**Derived subset streams (non-normative).** A stream MAY represent either a source-native collection or a derived subset, provided its semantics are stable, versioned through the SourceDeclaration, and human-reviewable in consent UI. Implementations that need semantically bounded consent in v0.1 SHOULD prefer named streams with human-readable semantics (e.g., a source that exposes `amazon_messages` as a distinct stream) over ad hoc technical predicates. Stream names MUST NOT encode predicate logic or synthesize per-request subsets; derived streams MUST be statically declared in the SourceDeclaration. - -The recommended future direction for this capability is declaration-defined parameterized subset templates with typed bound parameters and publisher-authored consent display strings. See spec-deferred for the design constraints and open questions that must be resolved before specifying this. - -### Extensions - -PDPP capabilities beyond this specification (for example, search or aggregation interfaces) are defined in companion profiles, not by extending Core semantics. Implementations MUST NOT change the meaning of Core-granted access via extensions: a grant issued under this specification authorizes exactly what Sections 7 and 8 define, regardless of what additional capabilities a deployment offers. Optional capabilities MUST be discoverable via declared metadata rather than assumed to be present. Unrecognized declared capabilities MUST be ignorable by clients. A full capability-advertisement grammar is deliberately deferred to a future version. - -### Specification governance - -PDPP protocol changes are proposed through public repository pull requests. In this repository, non-trivial protocol, reference contract, or architecture changes are tracked with OpenSpec before implementation so reviewers can audit the rationale, tasks, and requirement deltas. - -Current active editors and maintainers are listed in `MAINTAINERS.md`. This repository uses a three-license split: PDPP protocol specification text (all root `spec-*.md` files and their mirrored site pages) is made available under the Community Specification License 1.0 (SPDX: Community-Spec-1.0; see `LICENSE-specs`); software packages, examples, and generated artifacts use Apache-2.0 (see `LICENSE`) unless a narrower file-local notice says otherwise; and user-facing documentation prose outside the specification uses CC BY 4.0 (see `LICENSE-docs`). - ---- - -## 13. TypeScript Types {#typescript-types} - -**Note:** TypeScript types in this section are non-normative. The normative definitions are the prose field tables in Sections 5, 6, and 7. - -```typescript -// --- Record model --- - -interface BlobRef { - blob_id: string; - mime_type: string; // IANA media type - size_bytes: number; - sha256: string; - fetch_url?: string; // Injected by resource server at read time; absent in producer output -} - -interface ResourceRef { - source_id: string; // SourceDeclaration source.id absolute URI - stream: string; - record_id: string | string[]; -} - -// --- Selection (request-time) --- - -interface TimeRange { - since?: string; // ISO 8601, inclusive >= - until?: string; // ISO 8601, exclusive < -} - -interface StreamRequest { - name: string; - necessity?: 'required' | 'optional'; - instance_ids?: string[]; - time_range?: TimeRange; - view?: string; // Mutually exclusive with fields - fields?: string[]; // Top-level field names only in v0.1; mutually exclusive with view - resources?: string[]; // Canonical key strings per compound key encoding -} - -interface PresetStreamSelection { - name: string; - view?: string; // Mutually exclusive with fields - fields?: string[]; // Top-level field names only in v0.1; mutually exclusive with view -} - -// --- Source binding --- - -interface SourceObject { - kind: 'connector' | 'provider_native'; - id: string; // Stable absolute URI for the authorization and data surface -} - -interface SourceRequestObject { - id: string; // Stable absolute URI for the authorization and data surface - // No `kind`: the AS derives provenance from the accepted declaration -} - -type SelectionRequest = { - type: 'https://pdpp.dev/data-access'; - source: SourceRequestObject; - purpose_code: string; - purpose_description?: string; - access_mode: 'single_use' | 'continuous'; - retention?: { - max_duration: string; - on_expiry: 'delete' | 'anonymize'; - }; - client_claims?: { commitments?: string[] }; -} & ( - | { streams: StreamRequest[]; selection_preset?: never } - | { selection_preset: string; streams?: never } -); - -// --- Grant (post-consent, immutable) --- - -interface TimeConstraint extends TimeRange { - field: string; -} - -interface StreamGrant { - name: string; - instance_ids: string[]; // Non-empty, unique, and stream-scoped - fields: string[]; // Non-empty and authoritative for RS enforcement - time_constraint?: TimeConstraint; - resources?: string[]; // Canonical key strings per compound key encoding -} - -interface DataGrant { - version: '0.1.0'; - grant_id: string; - issued_at: string; - subject: { id: string }; - client: { client_id: string; client_display?: Record }; - source: SourceObject; - source_declaration: { version: string }; - purpose_code: string; // URI - purpose_description?: string; - access_mode: 'single_use' | 'continuous'; - streams: StreamGrant[]; - selection_preset?: string; - retention?: { - max_duration: string; // ISO 8601 duration - on_expiry: 'delete' | 'anonymize'; - }; - expires_at?: string; // ISO 8601; absent means no expiry -} - -// --- Source Declaration --- - -interface StreamView { - id: string; - label: string; - fields: string[]; -} - -interface StreamRelationship { - name: string; - stream: string; - foreign_key: string; - cardinality: 'has_many' | 'has_one'; -} - -interface StreamExpandCapability { - name: string; - default_limit?: number; - max_limit?: number; -} - -interface StreamQueryCapabilities { - range_filters?: Record>; - expand?: StreamExpandCapability[]; - search?: { - lexical_fields?: string[]; - semantic_fields?: string[]; - }; - aggregations?: { - count?: true; - count_distinct?: string[]; - group_by?: string[]; - group_by_time?: string[]; - max?: string[]; - min?: string[]; - sum?: string[]; - }; -} - -interface SourceDeclarationStream { - name: string; - description?: string; - display?: { label?: string; detail?: string }; - semantics: 'append_only' | 'mutable_state'; - schema: Record; - primary_key: string[]; - cursor_field?: string; // Logical ordering field for cursor-based reads and incremental sync - consent_time_field?: string; // Absent means time_range not supported for this stream - selection: { - // time_range capability derived from consent_time_field presence - fields: boolean; - resources: boolean; - }; - views?: StreamView[]; - relationships?: StreamRelationship[]; - query?: StreamQueryCapabilities; -} - -interface SourceDeclaration { - protocol_version: '0.1.0'; - source: SourceObject; - declaration_version: string; - publisher: { id: string }; - display: { name: string }; - selection_presets?: Array<{ - id: string; - label: string; - streams: PresetStreamSelection[]; - }>; - streams: SourceDeclarationStream[]; - extensions?: Record; -} - -// --- Token introspection response (PDPP extensions to RFC 7662) --- - -interface PDPPIntrospectionResponse { - active: boolean; - pdpp_token_kind?: string; // Core defines 'owner' and 'client'; companion profiles may add kinds (unknown kinds are unauthorized for Core operations) - subject_id?: string; - grant_id?: string; // Present for client tokens - client_id?: string; // Present for client tokens - exp?: number; // Unix timestamp - authorization_details?: Array>; // Approved RFC 9396 detail with Section 7 enforcement constraints -} - -// --- Tombstone (response object) --- - -interface TombstoneRecord { - object: 'record'; - id: string; // Canonical key string - stream: string; - deleted: true; - deleted_at: string; // ISO 8601, required - emitted_at: string; // ISO 8601, required - // No data field -} -``` - ---- - -## Appendix A: Purpose Code Registry - -**Registry governance:** Purpose code registries under `pdpp.dev` are controlled by PDPP maintainers via a public change process. Implementations MUST treat unrecognized purpose URIs as opaque identifiers and MUST NOT reject requests solely because a purpose code is unrecognized. - -Purpose codes are URIs. The following codes are defined by PDPP. Implementers may define additional codes using their own URI namespaces. - -| Code | Description | -|------|-------------| -| `https://pdpp.dev/purpose/personalization` | Tailoring the application experience to the user. | -| `https://pdpp.dev/purpose/analytics` | Analyzing user data to produce insights for the user. | -| `https://pdpp.dev/purpose/export` | Exporting data for the user's own use. | -| `https://pdpp.dev/purpose/agent_context` | Providing context to a personal AI agent. | -| `https://pdpp.dev/purpose/ai_training` | Using data to train AI models. The AS MUST obtain explicit affirmative user consent before issuing any grant with this purpose code. This is a protocol-level requirement, not merely advisory. | -| `https://pdpp.dev/purpose/research` | Academic or market research. | - ---- - -## Appendix B: Relationship to the Data Transfer Project (DTI) - -PDPP and DTI are complementary protocols addressing different concerns. PDPP defines parameterized consent and disclosure semantics (the grant is the consent artifact; the query API is the disclosure mechanism). DTI defines canonical data models and transfer adapters (the mechanics of moving data between systems). - -The two protocols can chain: a PDPP grant can authorize access to data that a DTI transfer then moves, using PDPP stream schemas to carry DTI canonical data model payloads. Formal integration between PDPP grants and DTI transfer manifests is a separate effort; no integration document is currently specified. - -Note: "Data Transfer Project" is referred to as DTI (Data Transfer Initiative) in current usage, reflecting its evolution from the original DTP initiative. diff --git a/packages/conformance-suite/src/requirements/catalog.ts b/packages/conformance-suite/src/requirements/catalog.ts index f8a2561b1..9b3c9dd31 100644 --- a/packages/conformance-suite/src/requirements/catalog.ts +++ b/packages/conformance-suite/src/requirements/catalog.ts @@ -58,15 +58,10 @@ export interface Requirement { */ export const SPEC_SOURCE = { document: "PDPP Core", - version: "v0.1.0 public-review draft (3 September 2026)", + version: "v0.1.0 locked text (27 October 2026)", section: "9", - /** - * Repository-relative path of the normative source. This is a frozen copy of - * spec-core.md at babd0ef3fb, the text this catalogue was transcribed from. The - * v0.1.0 lock changed spec-core.md after that; until the catalogue and matrix - * are re-transcribed, the suite judges against this copy, not the root file. - */ - path: "packages/conformance-suite/spec/spec-core.md", + /** Repository-relative path of the normative source. */ + path: "spec-core.md", } as const; const AUTHORIZATION_SERVER: readonly Requirement[] = [ @@ -77,7 +72,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - 'Accepts selection requests using the RFC 9396 `authorization_details` envelope with type "https://pdpp.dev/data-access".', + 'Accepts selection requests using the RFC 9396 `authorization_details` envelope with `type: "https://pdpp.dev/data-access"`.', specAnchor: "#conformance", }, { @@ -87,8 +82,8 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Validates selection requests against one retained SourceDeclaration snapshot: rejects unknown streams, unsupported selection parameters, and unrecognized selection presets.", - specAnchor: "#conformance", + "Validates public request structure and owner-independent capabilities, rejecting public invalidity with `invalid_authorization_details`. Resolves selections against one retained SourceDeclaration snapshot. Treats an otherwise valid selection that is infeasible under privately accepted capabilities as owner-dependent eligibility, only within authenticated review. There, omits an optional unapproved stream or refuses under the required-stream/minimum rules with indistinguishable `access_denied`, without client-visible private diagnostics, as Authorization privacy boundary requires.", + specAnchor: "#authorization-privacy", }, { id: "AS-3", @@ -97,7 +92,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Issues grants conforming to the Section 7 grant schema, with all fields derived from the selection request, client registration, or AS policy.", + "For Core `data-access` details, issues grants that conform to the grant schema defined in Section 7 (normative field tables). The AS generates `grant_id` and `issued_at`; it resolves other grant fields from authenticated identities, the selection request, the accepted source declaration, eligible instance handles, user approval, AS policy, or AS-resolved client metadata.", specAnchor: "#grant", }, { @@ -117,7 +112,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Produces a Source validation failure when a request contains both or neither of `streams` and `selection_preset`.", + "Produces a binding-neutral Source validation failure when a Core `data-access` detail contains both or neither of `streams` and `selection_preset`. The OAuth/RAR binding maps it to RFC 9396 `invalid_authorization_details`. Rejects a request in which any authorization-details object has an unsupported type or is invalid under its type's definition (Section 6).", specAnchor: "#selection-request", }, { @@ -127,7 +122,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "MUST NOT reject a `purpose_code` solely because it is absent from the PDPP registry; displays `purpose_description` or the raw URI for unrecognized codes.", + "MUST NOT reject a complete detail solely because its `purpose_code` is not in Appendix A. Rejects a detail with a code outside Appendix A that lacks a non-empty `purpose_description`, and displays that description. MAY reject a `purpose_code` based on local policy.", specAnchor: "#selection-request", }, { @@ -137,7 +132,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Renders requester identity, declaration-authored descriptions, policy declarations, and client claims as distinct categories; MUST attribute `client_claims` to the client and MUST NOT present them as protocol-enforced terms.", + "Renders requester identity metadata, declaration-authored data descriptions, structured policy declarations, and client-authored claims as semantically distinct categories during consent. MUST attribute `client_claims` to the client and MUST NOT present them as protocol-enforced terms. If `client_claims` are rendered during final review, binds the normalized exact claims into the immutable final approval artifact and review revision, and preserves that binding in retained consent evidence, without adding them to the resolved grant or RS enforcement. Records in the issued grant's `client.client_display` the exact `policy_uri` and `tos_uri` it presented during consent, and omits any it did not present.", specAnchor: "#client-claims", }, { @@ -147,7 +142,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Tracks grant lifecycle (active, expired, revoked) and reflects revocation immediately in introspection responses (`active: false`).", + "Tracks grant lifecycle (active, expired, revoked). Reflects revocation immediately in introspection: the revoked grant is inactive at once, and a token is `active: false` when no grant it covers is active.", specAnchor: "#revocation", }, { @@ -156,7 +151,8 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ specItem: 9, level: "must", applicability: "always", - statement: "Issues access tokens bound to specific grants, carrying the PDPP introspection extension fields.", + statement: + "For Core `data-access` details, issues access tokens bound to specific grants. Access tokens include the PDPP introspection extension fields. Binds each Core client token to exactly one grant. Without a companion credential binding, rejects more than one Core data-access detail with `invalid_authorization_details`.", specAnchor: "#token-introspection", }, { @@ -167,7 +163,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the AS supports `single_use` grants", statement: - "For `single_use` grants, consumes the grant atomically with first client-token issuance and rejects later attempts to issue new client access tokens against it.", + "For `single_use` grants, consumes the grant atomically with first client-token issuance. Gives that access token a finite expiration set by local policy and, when the grant has `expires_at`, no later than `expires_at`; that expiry ends the grant's one read window. Lets a later token in the same refresh-token family carry the grant only inside the window, reports the grant inactive for reads after it, and otherwise rejects access-token issuance against the consumed grant.", specAnchor: "#access-modes", }, { @@ -176,7 +172,8 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ specItem: 11, level: "must", applicability: "always", - statement: "Validates stream/field/view/resource-id shape at grant issuance.", + statement: + "Validates stream/field/view/resource-id shape at grant issuance. Rejects `resources` on a stream with more than one instance. Rejects `resources` entries that are not canonical under Section 4 and does not percent-decode JSON `resources` values.", specAnchor: "#conformance", }, { @@ -196,7 +193,8 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "conditional", appliesWhen: "the AS supports views", - statement: "Resolves view names to field lists at issuance time and stores resolved `fields` in the StreamGrant.", + statement: + "Resolves view names to field lists at issuance time; stores resolved `fields` and `required_fields` in the `StreamGrant`. Freezes `cursor_field` as `sort_field` when its format is `date` or `date-time` and the grant includes it; otherwise omits `sort_field`. Client-token record reads reject query-time `view` in v0.1. Owner-token current-capability reads MAY resolve current views.", specAnchor: "#views", }, { @@ -207,7 +205,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the AS issues grants carrying the ai_training purpose code", statement: - "Obtains explicit affirmative user consent before issuing grants with `purpose_code: https://pdpp.dev/purpose/ai_training`.", + "Rejects a detail whose `processing_permissions` is malformed or contains a duplicate, unknown, or unsupported value, with `invalid_authorization_details`, and never ignores, substitutes, or narrows such a value or issues an ordinary grant in its place. Advertises in `pdpp_processing_permissions_supported`, and grants, only permissions whose complete defining contract it implements. Treats `https://pdpp.dev/processing/ai_training` as unsupported until the PDPP AI Training Profile is published and its registration is approved, and afterwards supports it only by conforming to that profile.", specAnchor: "#ai-training-consent", }, { @@ -217,7 +215,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Resolves omitted instance IDs before final approval, binds resolved instances and decision fields to an immutable review revision, and rejects stale approval when eligibility or the reviewed revision changed.", + "For Core `data-access` details, resolves omitted instance IDs before the final approval surface. Binds exact resolved instances and all final decision fields to an immutable review revision or digest. Rejects stale approval if eligibility or the reviewed revision changes before approval.", specAnchor: "#grant", }, { @@ -227,16 +225,18 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Retains one exact SourceDeclaration snapshot through validation, consent, narrowing, issuance, and consent evidence; a later declaration never substitutes for it.", + "Retains one exact SourceDeclaration snapshot through request validation, consent display, narrowing, issuance, and consent evidence. A later current declaration never substitutes for it.", specAnchor: "#declaration-acceptance", }, { id: "AS-17", role: "authorization-server", specItem: 17, - level: "must", - applicability: "always", - statement: "Returns 400 `unsupported_version` when the `PDPP-Version` header specifies an unsupported version.", + level: "may", + applicability: "conditional", + appliesWhen: "Never: Core marks this item not used.", + statement: + "Not used. `PDPP-Version` negotiation is a Resource Server requirement and does not version Authorization Server endpoints.", specAnchor: "#errors", }, { @@ -247,7 +247,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the AS and RS are separated deployments", statement: - "Authenticates the RS at the RFC 7662 introspection endpoint and returns the complete grant enforcement context in one response.", + "For a separated AS and RS, authenticates the RS at the RFC 7662 introspection endpoint and returns the complete grant enforcement context in one response, including `pdpp_instance_bindings`. Never returns `pdpp_instance_bindings` to a client.", specAnchor: "#token-introspection", }, { @@ -257,7 +257,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Consumes each OAuth authorization code atomically on first successful redemption and rejects every later redemption with `invalid_grant`.", + "Consumes each OAuth authorization code atomically on its first successful redemption. Rejects every later redemption with `invalid_grant` and does not issue another token.", specAnchor: "#conformance", }, { @@ -268,7 +268,7 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the AS issues refresh tokens", statement: - "Issues refresh tokens only for `continuous` grants, rotates by family, and on reuse of a superseded token revokes the family and every family-linked access token, returning `invalid_grant`.", + "For a token response that covers only Core `data-access` grants, issues a refresh token only when at least one covered grant is `continuous`. Never extends a `single_use` grant's read window through refresh. Rotates refresh tokens by family. Reuse of a superseded token revokes the family and every family-linked access token, returns `invalid_grant`, and requires fresh authorization.", specAnchor: "#access-modes", }, { @@ -278,9 +278,372 @@ const AUTHORIZATION_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Rejects unsupported persisted authorization state before introspection or request handling, without reconstructing missing facts from current configuration.", + "Rejects unsupported persisted authorization state before introspection or request handling. Does not reconstruct missing facts from current configuration and requires fresh consent when no migration applies.", specAnchor: "#errors", }, + { + id: "AS-22", + role: "authorization-server", + specItem: 22, + level: "must", + applicability: "always", + statement: + "Rejects a source declaration containing an unrecognized `source.kind` or `streams[].semantics` value. Rejects a language map or `display.default_language` that breaks a structural or language-tag rule in Display text languages. MAY reject a declaration whose language versions it detects do not state the same meaning.", + specAnchor: "#display-text-languages", + }, + { + id: "AS-23", + role: "authorization-server", + specItem: 23, + level: "must", + applicability: "conditional", + appliesWhen: "The AS issues a grant with `grantors`.", + statement: + "Issues a grant with `grantors` only after each listed representative approves the same immutable final review revision. Authenticates each representative and, before showing any subject-specific information or accepting approval, verifies their authority under Representative approval. Binds each identity and capacity to that revision and retained consent evidence, and keeps the data subject in `subject`.", + specAnchor: "#representative-approval", + }, + { + id: "AS-24", + role: "authorization-server", + specItem: 24, + level: "must", + applicability: "always", + statement: + "For Core `data-access` details, lets the owner approve or decline each detail of a request on its own. Issues one grant for each approved, resolved detail, covering one subject, one source, and one purpose, with its own lifecycle. Identifies grants by `grant_id` and does not treat subject, source, and purpose as a grant key.", + specAnchor: "#conformance", + }, + { + id: "AS-25", + role: "authorization-server", + specItem: 25, + level: "must", + applicability: "conditional", + appliesWhen: "The AS offers client-initiated revocation.", + statement: + "For Core `data-access` details, if it offers client-initiated revocation, ends grants as Section 10 defines. Revoking a refresh token, current or superseded, ends every grant its family covers; revoking an access token ends its grants only when no refresh-token family covers them. In the OAuth binding, implements it as RFC 7009 token revocation and advertises `revocation_endpoint` in RFC 8414 metadata. Accepts `pdpp_disposition` on that request and records it only when the request carries current client authentication.", + specAnchor: "#conformance", + }, + { + id: "AS-26", + role: "authorization-server", + specItem: 26, + level: "must", + applicability: "always", + statement: + "Ends one grant among several that share a credential without revoking that credential or ending the other grants.", + specAnchor: "#conformance", + }, + { + id: "AS-27", + role: "authorization-server", + specItem: 27, + level: "must", + applicability: "always", + statement: + "Rejects a declaration that names an ineligible `consent_time_field` or a non-boolean `consent_time_content_may_exceed`. Rejects a `time_range` with no bound, a bound whose type does not match the field's format, an invalid bound, or a `since` not before `until`; the OAuth/RAR binding returns `invalid_authorization_details`. This error applies only to public invalidity. A failure that depends on a declaration accepted privately for the owner is an eligibility failure under Authorization privacy boundary and MUST NOT produce a client-visible declaration-dependent validation error. Never converts or rounds a bound. Sets `time_constraint.type` from the field's format in the retained declaration.", + specAnchor: "#authorization-privacy", + }, + { + id: "AS-28", + role: "authorization-server", + specItem: 28, + level: "must", + applicability: "always", + statement: + "For each time-bounded stream, renders the exact bounds with their inclusive and exclusive meaning and the declared meaning of the field. Shows a relative label only when it has verified it for those bounds. Discloses that matched records can contain data from outside the bounds when the stream declares `consent_time_content_may_exceed: true`. For a `continuous` grant, discloses that the client can see records leave the window.", + specAnchor: "#conformance", + }, + { + id: "AS-29", + role: "authorization-server", + specItem: 29, + level: "must", + applicability: "always", + statement: + "Gives each client pairwise identifiers: `subject.id`, `grantors[].id`, instance handles, and any ID Token `sub` differ between clients for the same subject, grantor, or instance. They stay the same for one client, are never reassigned, and cannot be reversed by a client. Sets ID Token `sub` to the authenticated person's identifier, which is a grantor's identifier when a representative authenticates. Uses a globally resolvable `subject.id` only when the owner explicitly chooses to disclose it to that client, and binds that identifier and the client into the final approval artifact and retained consent evidence. Returns an internal subject identifier and `pdpp_instance_bindings` only to the RS.", + specAnchor: "#conformance", + }, + { + id: "AS-30", + role: "authorization-server", + specItem: 30, + level: "must", + applicability: "always", + statement: + "Resolves each request as Request resolution defines before final approval. Treats requested instances, resources, time bounds, and the field ceiling as upper limits, never narrows below schema-required fields, and adds nothing to compensate for a refused selection. Checks required streams and each retained stream's `minimum` before issuing a grant or client token. Reports an unmet minimum on a required stream as `access_denied` and a malformed `minimum` as `invalid_authorization_details`.", + specAnchor: "#request-resolution", + }, + { + id: "AS-31", + role: "authorization-server", + specItem: 31, + level: "must", + applicability: "always", + statement: + "Resolves purpose and retention only from the current request or recipient-authorized standing terms, and retains the evidence identifying those terms with the consent evidence. Rejects a `retention.max_duration` that is not in the form `PnD`. Discloses during owner review that envelope identifiers are part of every disclosed record.", + specAnchor: "#conformance", + }, + { + id: "AS-32", + role: "authorization-server", + specItem: 32, + level: "must", + applicability: "always", + statement: + "Returns one `{ type, grant }` element per covered grant in every token response that issues a client access token, including refresh responses, as Client-visible authorization result defines. Returns the same element shape in introspection.", + specAnchor: "#authorization-result", + }, + { + id: "AS-33", + role: "authorization-server", + specItem: 33, + level: "must", + applicability: "always", + statement: + "Advertises `https://pdpp.dev/data-access` and `https://pdpp.dev/grant-lifecycle` in `authorization_details_types_supported` and rejects unknown detail types, and a detail that carries a member it does not define.", + specAnchor: "#conformance", + }, + { + id: "AS-34", + role: "authorization-server", + specItem: 34, + level: "must", + applicability: "always", + statement: + "Chooses one version of each language-map display value it renders by the procedure in Display language. Does not reject a request solely because a requested language is unsupported. Binds the identity, exact string, and any chosen language tag of each declaration display value shown on final review into the final approval artifact, the review revision or digest, and retained consent evidence. Records display text from an earlier consent step as Section 7 requires.", + specAnchor: "#display-language", + }, + { + id: "AS-35", + role: "authorization-server", + specItem: 35, + level: "should", + applicability: "always", + statement: + "SHOULD give every client access token a finite expiration and SHOULD issue it from a refresh-token family for a `continuous` grant, which allows renewal without re-consent while the family remains valid.", + specAnchor: "#conformance", + }, + { + id: "AS-36", + role: "authorization-server", + specItem: 36, + level: "must", + applicability: "always", + statement: + "For a detail that carries a processing permission, requires a distinct affirmative owner selection, never preselects it, and never makes another detail depend on it. Renders each permission as a labeled term distinct from the purpose. Requires a non-empty `purpose_description` on an AI training detail. Issues the grant with its exact permission set or not at all, and never adds a permission to a detail that did not request it. Binds that set into final owner review, the review revision, retained consent evidence, the grant, token results, and introspection, and requires a new review when the set changes after review. Includes a reviewed `expires_at` in every training grant. On the approval surface for an AI training detail, makes the statements and avoids the claims that AI training consent lists.", + specAnchor: "#ai-training-consent", + }, + { + id: "AS-37", + role: "authorization-server", + specItem: 37, + level: "must", + applicability: "conditional", + appliesWhen: "The AS supports a processing permission.", + statement: + "When it supports a processing permission, keeps read access and processing authority distinct as Read access and processing authority defines. Issues no further processing authority after grant revocation or grant expiry. When the grant has `expires_at`, issues no processing authority that outlasts it. Never lets a processing credential confer read access. Does not treat read-window closure, access-token expiry, or refresh-token replay detection alone as ending processing authority or prohibiting its renewal.", + specAnchor: "#processing-authority", + }, + { + id: "AS-38", + role: "authorization-server", + specItem: 38, + level: "must", + applicability: "always", + statement: + "Its operator does not use owner data held in the AS role for AI training, except under a training grant issued to it as a client.", + specAnchor: "#conformance", + }, + { + id: "AS-39", + role: "authorization-server", + specItem: 39, + level: "must", + applicability: "always", + statement: + "Provides the grant lifecycle operation and advertises `pdpp_grant_lifecycle_endpoint`, as Grant lifecycle operation defines. For queries, accepts any token it issued to the client that covers the grant, whether active, expired, superseded, or revoked. Requires current client authentication for writes. Returns indistinguishable `invalid_grant` entries for unknown, foreign, and uncovered grants. Stores token digests, not tokens, and can disable any token it issued for lifecycle use. Admits at least one query per grant per retry interval from each client. Records a client-conveyed `delete` as that client's `received` report only when the request reports the client's first receipt time.", + specAnchor: "#grant-lifecycle", + }, + { + id: "AS-40", + role: "authorization-server", + specItem: 40, + level: "must", + applicability: "always", + statement: + "Returns a positive assessment only when no erasure instruction applies to the grant, whatever its read state, and only from state ordered after every terminal event it has acknowledged.", + specAnchor: "#conformance", + }, + { + id: "AS-41", + role: "authorization-server", + specItem: 41, + level: "must", + applicability: "always", + statement: + "On every interface through which the owner ends a grant, requires an explicit disposition for each grant in the action's scope, rejects a request without one, and applies no default. Presents `keep` and `delete` with neither preselected, and lists the complete affected set as Ending paths defines, including already-ended and consumed grants whose held data may remain and grants that carry a processing permission.", + specAnchor: "#ending-paths", + }, + { + id: "AS-42", + role: "authorization-server", + specItem: 42, + level: "must", + applicability: "always", + statement: + "Records each erasure instruction durably before acknowledging it and preserves its terminal effect. An immediate instruction revokes an active grant. An advance instruction takes effect at grant end and does not itself end the grant earlier. Records acceptance, first return, reported receipt, and reported completion or exception separately. Accepts `retained_until` only in its three forms, and shows a `retained` report's end in that form, never as completed deletion. From the effective time, issues no further processing authority that permits use of the covered data.", + specAnchor: "#conformance", + }, + { + id: "AS-43", + role: "authorization-server", + specItem: 43, + level: "must", + applicability: "always", + statement: + "Shows the owner the stop-use and deletion statements that Erasure requires, and the time by which covered processing stops. Never shows a receipt-based deletion deadline calculated from acceptance or first return. Until a receipt is reported, shows the latest recorded stage and no deletion date.", + specAnchor: "#erasure", + }, + { + id: "AS-44", + role: "authorization-server", + specItem: 44, + level: "must", + applicability: "always", + statement: + "Meets Lifecycle durability: records terminal events before acknowledging them, in a history that no restore or failover through a path its mechanism detects can make an enforcement point treat as current while it omits an acknowledged terminal event. Records the other events that section lists before a response discloses or relies on them, and never reuses a `grant_id` or `instruction_id`. Never returns a positive assessment, an active introspection result, or a token that contradicts an acknowledged terminal event, on any issuance or assessment path, including code, device-code, and refresh exchanges, token re-delivery, lifecycle write authentication, and package fan-out. This holds except after a restore or rollback path that it discloses as undetected. After restore or failover, treats a history as current only when its declared mechanism establishes that the history includes every acknowledged terminal event, and otherwise fails closed. Describes the durability it actually provides, including the disk and host losses it does not survive, the restore and rollback paths its mechanism does not detect, and what such a path can revive. While its history is unavailable, accepts, records, and later applies owner revocations, dispositions, and erasure requests. For an action affecting several grants, records a result per grant and reports the action complete only when every requested ending and disposition is recorded. Keeps lifecycle and authentication bindings for as long as that section requires.", + specAnchor: "#lifecycle-durability", + }, + { + id: "AS-45", + role: "authorization-server", + specItem: 45, + level: "must", + applicability: "always", + statement: + "Shows in final owner review what happens to held data after the grant ends, as Recipient commitments and approval defines.", + specAnchor: "#recipient-commitments", + }, + { + id: "AS-46", + role: "authorization-server", + specItem: 46, + level: "must", + applicability: "conditional", + appliesWhen: "The AS ends a grant for a security reason.", + statement: + "On security revocation, shows the owner the ending and its reason and offers the disposition. Before decommission, offers a disposition for every grant whose held data may remain, shows the stage of each disposition, and warns about each `delete` that no client has received. In a narrowing flow, states the effect of each disposition as Grant narrowing defines. Lets the owner request erasure for an ended grant while its held data may remain.", + specAnchor: "#grant-narrowing", + }, + { + id: "AS-47", + role: "authorization-server", + specItem: 47, + level: "must", + applicability: "always", + statement: "Supports lifecycle recovery, and gives a recovery token no read access and no refresh token.", + specAnchor: "#lifecycle-recovery", + }, + { + id: "AS-48", + role: "authorization-server", + specItem: 48, + level: "must", + applicability: "always", + statement: + "Retrieves declarations over HTTPS without ambient credentials. Validates every redirect and final URL against its accepted pointer and network policy. Validates the destination immediately before each connection and connects only to that validated address. Rejects automatic remote-schema retrieval and fails closed on any failed check, as Section 5 requires.", + specAnchor: "#conformance", + }, + { + id: "AS-49", + role: "authorization-server", + specItem: 49, + level: "must", + applicability: "always", + statement: + "Renders all declaration display values and client-authored values, including `client_display`, resolved client metadata, `purpose_description`, and `client_claims`, safely for their output context under Section 5.", + specAnchor: "#conformance", + }, + { + id: "AS-50", + role: "authorization-server", + specItem: 50, + level: "must", + applicability: "always", + statement: + "Accepts a valid client ID metadata document as an identity without requiring preregistration. Keeps that identity-form acceptance distinct from authorization or admission denial under local policy, as Section 6 requires.", + specAnchor: "#conformance", + }, + { + id: "AS-51", + role: "authorization-server", + specItem: 51, + level: "must", + applicability: "always", + statement: + "Freezes each stream's schema-required consent floor from the retained declaration into `required_fields` before issuance. Includes the set in reviewed decision evidence, the client-visible grant, and the grant returned through introspection. Includes an empty array for an empty floor. Keeps the set unchanged for the life of that grant.", + specAnchor: "#conformance", + }, + { + id: "AS-52", + role: "authorization-server", + specItem: 52, + level: "must", + applicability: "always", + statement: + "Applies Authorization privacy boundary to every supported authorization entry point. Performs owner-independent syntax and binding checks without querying private owner availability. Defers owner-dependent resolution to authenticated review.", + specAnchor: "#authorization-privacy", + }, + { + id: "AS-53", + role: "authorization-server", + specItem: 53, + level: "must", + applicability: "always", + statement: + "Keeps optional ineligible streams unapproved. Refuses required ineligible streams and unmet required authorization minima with `access_denied`. Preserves `invalid_authorization_details` for malformed or unsupported details and any error defined for a specific authorization-details type.", + specAnchor: "#conformance", + }, + { + id: "AS-54", + role: "authorization-server", + specItem: 54, + level: "must", + applicability: "always", + statement: + "Demonstrates equivalent pre-authentication observables when only protected owner state varies. Demonstrates equivalent client-visible eligibility denials after review. Includes status, response shape and size, redirects, headers, transaction behavior, and timing in its evidence.", + specAnchor: "#conformance", + }, + { + id: "AS-55", + role: "authorization-server", + specItem: 55, + level: "must", + applicability: "always", + statement: + "Records the entry points, protected-state variants, timing method, and observation limits exercised by its authorization-privacy tests. Reports an unobserved supported path as UNVERIFIED rather than passed.", + specAnchor: "#conformance", + }, + { + id: "AS-56", + role: "authorization-server", + specItem: 56, + level: "must", + applicability: "always", + statement: + "States during final owner review that ordinary access confers no independent-recipient disclosure authority. Distinguishes parties acting for the client. Does not claim that this duty technically prevents onward disclosure. Applies the closed processing-permission rules without substitution or fallback.", + specAnchor: "#conformance", + }, + { + id: "AS-57", + role: "authorization-server", + specItem: 57, + level: "must", + applicability: "always", + statement: + "Passes a differential authorization-privacy case that varies private declaration acceptance, temporal capability, and view availability against a matched owner refusal. The client-visible result, the callback, and any transaction-status channel are equivalent across the variants, and no unapproved diagnostic reaches the client.", + specAnchor: "#conformance", + }, ]; const RESOURCE_SERVER: readonly Requirement[] = [ @@ -291,7 +654,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Implements the Section 8 query endpoints: list streams, get stream metadata, list records, get a single record, get a blob, delete a record (owner-authenticated).", + "Implements the query endpoints defined in Section 8: list streams, get stream metadata, list records, get a single record, get a blob, delete a record (owner-authenticated).", specAnchor: "#resource-server-interface", }, { @@ -301,7 +664,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Enforces grant constraints on every client request: stream membership, explicit instance handles, frozen `time_constraint`, `fields` allowlist, and `resources` filter.", + "Enforces grant constraints on every client request: stream membership, explicit instance handles, frozen `time_constraint`, `fields` allowlist, `required_fields`, frozen `sort_field`, and `resources` filter. Authorizes each disclosure under one grant and never combines the constraints of different grants. Uses Section 4's canonical key representation, never percent-decodes JSON key values, and decodes an HTTP path key exactly once before comparison.", specAnchor: "#resource-server-interface", }, { @@ -312,7 +675,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the deployment separates AS and RS", statement: - "Resolves access tokens through authenticated RFC 7662 introspection, enforces only from that response, makes no second AS lookup while handling the request, and caches positive results no longer than min(token_exp, 60 seconds).", + "In a separated deployment, resolves access tokens through authenticated RFC 7662 introspection, enforces only from that response, and makes no second AS lookup while handling the request. A co-located deployment may use a local equivalent. Caches positive results for no more than 60 seconds and never past `exp` when present.", specAnchor: "#token-introspection", }, { @@ -322,7 +685,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Distinguishes owner tokens from client tokens via `pdpp_token_kind`, determined solely from the introspection response and never from token syntax.", + "Distinguishes owner tokens from client tokens via `pdpp_token_kind`, and authorizes Core operations only for those two kinds, each under its own rules.", specAnchor: "#token-introspection", }, { @@ -332,7 +695,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "For owner tokens computes the effective filter as the permitted owner request filter alone; for client tokens in v0.1 rejects request-time predicate filters and enforces the frozen grant constraints.", + "For owner tokens, computes the effective filter as the permitted owner request filter alone (there is no grant filter). For client tokens in v0.1, rejects request-time predicate filters and enforces the frozen grant constraints.", specAnchor: "#list-records", }, { @@ -341,7 +704,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ specItem: 6, level: "must", applicability: "always", - statement: "Returns structured errors as defined in the Section 8 unified error table.", + statement: "Returns structured errors as defined in Section 8 (unified error table).", specAnchor: "#errors", }, { @@ -352,7 +715,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the target serves at least one `mutable_state` stream", statement: - "Supports incremental sync via `changes_since` for `mutable_state` streams, including tombstones, omission of records whose grant-authorized projection did not change, and HTTP 410 `cursor_expired` on expiry.", + "Supports incremental sync via `changes_since` on every stream, starting from `beginning`, with tombstone entries and HTTP 410 with error code `cursor_expired` on cursor expiry. Returns every record not visible at h0 and visible at h1, and omits a record visible at both horizons whose projection did not change, unless the previous session withheld it, or redacted a reference in it, after a deletion. The projection is the grant-authorized projection for a client token, and the session's effective `fields` or resolved `view` projection for an owner token.", specAnchor: "#list-records", }, { @@ -362,7 +725,8 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "conditional", appliesWhen: "the target supports `changes_since`", - statement: "Returns `next_changes_since` on the terminal page of every `changes_since` response.", + statement: + "Returns `next_changes_since` on the terminal page of every `changes_since` response and on no other page.", specAnchor: "#list-records", }, { @@ -372,7 +736,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Rejects client-token exact and range `filter[...]` parameters with 400 `invalid_request` before consulting current declaration metadata.", + "Rejects client-token exact and range `filter[...]` parameters with 400 `invalid_request` before consulting current declaration metadata; owner-token current-capability reads MAY retain declared filter behavior.", specAnchor: "#list-records", }, { @@ -391,8 +755,9 @@ const RESOURCE_SERVER: readonly Requirement[] = [ specItem: 11, level: "must", applicability: "always", - statement: "Implements `PDPP-Version` header negotiation.", - specAnchor: "#errors", + statement: + "Implements API versioning: supports the exact `0.1` contract, selects it for an absent header, rejects malformed or unsupported values without fallback, reports the selected contract in responses, and publishes the matching default in protected resource metadata. Binds cursors and synchronization tokens to the selected contract and preserves all grant and profile enforcement independently of it.", + specAnchor: "#api-versioning", }, { id: "RS-12", @@ -402,7 +767,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the target issues owner tokens", statement: - "Scopes owner token access to a single subject's data store, deriving `subject_id` from the introspection response.", + "Scopes owner token access to a single subject's data store; derives `subject_id` from introspection response. Never discloses the introspection `subject_id` to a client.", specAnchor: "#resource-server-interface", }, { @@ -413,7 +778,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the target declares `pdpp_self_export_supported`", statement: - "SHOULD support owner-authenticated access to the record query endpoints without a client grant (self-export).", + "SHOULD support owner-authenticated access to the `/v1/streams/{stream}/records` query endpoints without a client grant, allowing the data subject to export their own data directly (self-export).", specAnchor: "#resource-server-interface", }, { @@ -424,7 +789,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the target issues owner tokens", statement: - "For owner-token stream-metadata reads, returns the full current stream metadata within the owner's scope, including current query, view, and relationship capabilities.", + "For owner-token stream-metadata reads, returns the full current stream metadata within the owner's subject/source/connection scope, including current query, view, and relationship capabilities.", specAnchor: "#stream-metadata", }, { @@ -434,7 +799,7 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "For client-token stream-metadata reads, returns only a projection derived from the resolved authorization context, excluding current view/relationship/filter/expansion/aggregation capability and any post-issuance declaration change.", + "For client-token stream-metadata reads, returns only a projection derived from the resolved authorization context: the granted stream and its explicitly granted fields, and immutable/frozen grant facts. MUST NOT include current view, relationship, filter, expansion, or aggregation capability unless that capability is explicitly part of a future frozen grant vocabulary, and MUST NOT surface a source-declaration change made after grant issuance.", specAnchor: "#stream-metadata", }, { @@ -444,19 +809,191 @@ const RESOURCE_SERVER: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Publishes RFC 9728 protected resource metadata at the RFC 9728 Section 3 location carrying `resource` and the four `pdpp_` members, and returns a `WWW-Authenticate: Bearer` challenge with `resource_metadata` on 401.", + "Publishes RFC 9728 protected resource metadata at the location RFC 9728 Section 3 derives from its resource identifier, carrying `resource`, the four required `pdpp_`-prefixed members defined in Section 8, and `authorization_servers` when its issuer set is enumerable. Also carries `pdpp_freshness_granularity_seconds` when it attaches `freshness` to client-token responses. Returns a `WWW-Authenticate: Bearer` challenge on 401 per RFC 6750 Section 3, carrying the RFC 9728 `resource_metadata` parameter.", specAnchor: "#protected-resource-metadata", }, -]; - -const CLIENT: readonly Requirement[] = [ { - id: "CL-1", - role: "client", - specItem: 1, + id: "RS-17", + role: "resource-server", + specItem: 17, level: "must", applicability: "always", - statement: "Submits selection requests using the RFC 9396 `authorization_details` envelope.", + statement: + "Does not interpret an unrecognized stream semantic as `append_only` or `mutable_state`, or an unrecognized grant `source.kind` as a known provenance class.", + specAnchor: "#conformance", + }, + { + id: "RS-18", + role: "resource-server", + specItem: 18, + level: "must", + applicability: "always", + statement: + "Serves a blob only when a record the requesting token may currently read, including its field projection, references it. Otherwise returns 404 `blob_not_found`. Gives a redirect's signed URL an expiry no later than the positive-status cache expiry, or, when no result was cached, 60 seconds after the RS sent the introspection request that validated the token. The expiry is also no later than the token or grant expiration when present. A delayed response never restarts this deadline.", + specAnchor: "#conformance", + }, + { + id: "RS-19", + role: "resource-server", + specItem: 19, + level: "must", + applicability: "always", + statement: + 'Returns 401 with `WWW-Authenticate: Bearer error="invalid_token"` for every inactive access token. Uses the structured code `grant_expired` or `grant_revoked` only when authenticated context establishes that cause, otherwise `authentication_error`.', + specAnchor: "#conformance", + }, + { + id: "RS-20", + role: "resource-server", + specItem: 20, + level: "must", + applicability: "always", + statement: + "Emits a tombstone for each record visible at the previous horizon and not visible at the new one, and for no other record. Retains the state needed to determine visibility at every unexpired horizon, and otherwise fails the session with 410 `cursor_expired`. Uses the exact client tombstone shape for client tokens. For owner tokens, adds `removal`, and `deleted_at` and `emitted_at` only for a deletion.", + specAnchor: "#conformance", + }, + { + id: "RS-21", + role: "resource-server", + specItem: 21, + level: "must", + applicability: "always", + statement: + "Binds each page cursor and `changes_since` token to its grant, or owner subject and scope, and to its query context, and rejects a mismatch as `invalid_cursor`. Orders a `changes_since` session by `instance_id` and then canonical key string, does not serve a record deleted after the session's first page, and rejects `expand[]` and `expand_limit[...]` on `changes_since`. Issues tokens and page cursors that reveal no readable position, time, or field value, and whose equality or ordering signals no change outside the grant or owner scope.", + specAnchor: "#conformance", + }, + { + id: "RS-22", + role: "resource-server", + specItem: 22, + level: "must", + applicability: "always", + statement: + "Compares time values and bounds by the grant's frozen `time_constraint.type`, never a current declaration, as calendar dates or exact instants, without a host, viewer, or owner time zone. Validates values itself under RFC 3339, including its leap-second rule. Excludes a record whose value is absent, null, or invalid from every read surface. Applies the same rules to owner-token range filters on `date` and `date-time` fields, using the declared format.", + specAnchor: "#conformance", + }, + { + id: "RS-23", + role: "resource-server", + specItem: 23, + level: "must", + applicability: "always", + statement: + "Puts `instance_id` on every record and tombstone it serves: the grant's handle for a client token, and the owner's handle for an owner token. Serves each `resource_ref` in the resolved or redacted form according to the token-kind, source, and sync-horizon rules in Section 4. In the next session, returns each referring record that the previous session served with a reference redacted after a target deletion, as Section 4 requires. Identifies records within a stream by instance and canonical key in sync, removals, cursors, and single-record reads. Requires `instance_id` on a single-record read or record delete when the query context covers more than one instance of the stream, and on every blob fetch. Rejects a missing one with 400 `invalid_request`.", + specAnchor: "#conformance", + }, + { + id: "RS-24", + role: "resource-server", + specItem: 24, + level: "must", + applicability: "conditional", + appliesWhen: "The deployment separates the AS and the RS.", + statement: + "In a separated deployment, resolves every handle through `pdpp_instance_bindings`. Fails a client request with 403 `grant_invalid` when a handle the request needs has no binding or names no stored instance. Treats an owner instance without a binding as outside the owner scope.", + specAnchor: "#conformance", + }, + { + id: "RS-25", + role: "resource-server", + specItem: 25, + level: "must", + applicability: "conditional", + appliesWhen: "The RS attaches `freshness` to a response.", + statement: + "When it attaches `freshness`, lists exactly one entry for each instance that the query context authorizes for the stream, whatever the page contains. Computes `last_success_at` over the full authorized scope of the query, counts only successful operations as Section 8 defines them, and reports null when it cannot establish relevance safely. In a client-token response, carries no failure time, truncates each value to its declared granularity of at least 3600 seconds, and reports no value before its interval ends. Takes a snapshot of the array on the first page of a `changes_since` session and repeats that exact snapshot on every later page. Reports no staleness verdict.", + specAnchor: "#conformance", + }, + { + id: "RS-26", + role: "resource-server", + specItem: 26, + level: "must", + applicability: "always", + statement: + "Builds disclosed `data` only from the top-level members the grant and the request-time field selection permit, with no nulls or fabricated values for withheld members. Refuses a read it cannot serve that way with 403 `disclosure_unavailable`.", + specAnchor: "#conformance", + }, + { + id: "RS-27", + role: "resource-server", + specItem: 27, + level: "must", + applicability: "always", + statement: + "Enforces the resolved grant independently of provider action permission, as Existing permissions and action results defines. Never treats a data grant as action permission or lets a broad read scope widen a grant, and discloses existing data in an action response only under the applicable data grant.", + specAnchor: "#existing-permissions", + }, + { + id: "RS-28", + role: "resource-server", + specItem: 28, + level: "must", + applicability: "always", + statement: + "Delivers an `evidence` item only when it implements the item's type, the proof is valid over the served values and authenticates the attester, the item's record binding verifies for the caller, and the item does not widen the disclosure. Decides per record whether to deliver an item only when that decision is independent of hidden data. Adds no member to an item, delivers no item with an unresolved local reference, never passes an unchecked item through, and marks no item as verified or unverified. Sends no evidence on a tombstone, and does not return a record in a `changes_since` session only because its evidence changed. Delivers evidence on owner-token reads only for types whose profile supports them.", + specAnchor: "#conformance", + }, + { + id: "RS-29", + role: "resource-server", + specItem: 29, + level: "must", + applicability: "always", + statement: + "Never treats `processing_permissions` or a processing credential as read authority. Its operator does not use owner data it holds in the RS role for AI training, except under a training grant issued to it as a client.", + specAnchor: "#conformance", + }, + { + id: "RS-30", + role: "resource-server", + specItem: 30, + level: "must", + applicability: "always", + statement: + "Validates each StreamGrant's explicit `required_fields` and its containment in `fields` before disclosure. Rejects missing or malformed context with 403 `grant_invalid`. Applies the frozen floor to every client record-data projection and its evidence without declaration lookup. Preserves tombstone shape and existing sync eligibility rules.", + specAnchor: "#conformance", + }, + { + id: "RS-31", + role: "resource-server", + specItem: 31, + level: "must", + applicability: "always", + statement: + "For a client request whose explicit `fields` contains both granted and ungranted names, returns 403 `field_not_granted` and no records. The error names only the ungranted names.", + specAnchor: "#conformance", + }, + { + id: "RS-32", + role: "resource-server", + specItem: 32, + level: "must", + applicability: "always", + statement: + "Given the same request and grant, returns the same status, headers, and body, apart from `request_id`, `Request-Id`, and `Date`, when the same well-formed outside-grant name exists in the current schema and when it is invented. It performs no source-schema or record-data lookup before rejection.", + specAnchor: "#conformance", + }, + { + id: "RS-33", + role: "resource-server", + specItem: 33, + level: "must", + applicability: "always", + statement: + "For a client list read whose grant omits the declared date `cursor_field`, uses descending `(instance_id, canonical key)` order by default, even when date order differs. When the grant includes the field, it uses the frozen `sort_field`.", + specAnchor: "#conformance", + }, +]; + +const CLIENT: readonly Requirement[] = [ + { + id: "CL-1", + role: "client", + specItem: 1, + level: "must", + applicability: "always", + statement: "Submits selection requests using the RFC 9396 `authorization_details` envelope.", specAnchor: "#selection-request", }, { @@ -475,7 +1012,7 @@ const CLIENT: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Treats `cursor` and `changes_since` tokens as opaque and from distinct token spaces; MUST NOT use a `next_cursor` value as a `changes_since` parameter.", + "Treats `cursor` and `changes_since` tokens as opaque and from distinct token spaces. MUST NOT use a `next_cursor` value as a `changes_since` parameter.", specAnchor: "#list-records", }, { @@ -486,7 +1023,7 @@ const CLIENT: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the client performs incremental sync", statement: - "Stores `next_changes_since` from the terminal page of a `changes_since` response for the next sync session.", + "Stores `next_changes_since` from the terminal page of a `changes_since` response for use in the next sync session.", specAnchor: "#list-records", }, { @@ -497,7 +1034,7 @@ const CLIENT: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the client performs incremental sync", statement: - "Respects HTTP 410 `cursor_expired` by performing a full re-sync rather than retrying with the expired cursor.", + "Respects HTTP 410 `cursor_expired` responses by starting again from `beginning`, rather than retrying with the expired token. After every completed `beginning` session, replaces its copy of the stream synchronized under that grant and query context with that session's result. Applies each returned record as a replacement of its copy within the same projection, never as a partial update: omission of an optional field inside that projection removes it, and a present `null` is a stored null.", specAnchor: "#list-records", }, { @@ -506,7 +1043,8 @@ const CLIENT: readonly Requirement[] = [ specItem: 6, level: "must", applicability: "always", - statement: "Honors retention commitments declared in the grant.", + statement: + "Honors retention commitments declared in the grant. Counts each record's retention ceiling from its first acquisition, never restarts it, and applies `on_expiry` at the ceiling to the record and its derivatives with no grace period.", specAnchor: "#retention", }, { @@ -516,7 +1054,7 @@ const CLIENT: readonly Requirement[] = [ level: "must", applicability: "always", statement: - "Treats unrecognized error codes as opaque, falling back to the actual HTTP status code and headers, and never fails to parse on an unknown `code` or `type`.", + "Treats unrecognized error codes as opaque, falling back to the exact HTTP status code and applicable response headers rather than failing on an unknown code. Takes the actual status code and headers as the authoritative outcome. Uses a recognized `error.type` only to refine category or presentation, and only when compatible with that outcome. Ignores an absent, unrecognized, or status-incompatible `type` for control flow, and never fails to parse on an unknown `code` or `type`.", specAnchor: "#errors", }, { @@ -527,9 +1065,258 @@ const CLIENT: readonly Requirement[] = [ applicability: "conditional", appliesWhen: "the client has provenance-dependent local policy", statement: - "Reads `source.kind` from the issued grant and applies provenance policy before first use of the records; MUST NOT assume a provenance class it did not read from the grant.", + "Where local policy depends on source provenance, MUST read `source.kind` from the issued grant and apply that policy before first use of the records. A client MUST NOT assume a provenance class it did not read from the grant, and MUST NOT treat an unrecognized `source.kind` as either known value. A client with no provenance-dependent policy has nothing to check.", specAnchor: "#source-kinds", }, + { + id: "CL-9", + role: "client", + specItem: 9, + level: "must", + applicability: "always", + statement: + "Stops using an access token on a 401 with Bearer error `invalid_token`, and stops requests against a grant on `grant_revoked`. Does not treat an inactive token as proof of revocation. Starts a grant lifecycle query before further use of held data of that grant.", + specAnchor: "#conformance", + }, + { + id: "CL-10", + role: "client", + specItem: 10, + level: "must", + applicability: "always", + statement: + "Identifies each record within a stream by its `instance_id` and `id` together. Treats a tombstone for a record it does not hold as a no-op. Reconciles records it obtained outside sync sessions itself, because the sync guarantee covers only a copy built from sync sessions.", + specAnchor: "#conformance", + }, + { + id: "CL-11", + role: "client", + specItem: 11, + level: "must", + applicability: "always", + statement: + "Does not infer from an empty result, the end of pagination, a tombstone, or a 404 that a record does not exist at the source or that collection is complete. Treats a null `last_success_at` only as the absence of a reportable timestamp.", + specAnchor: "#conformance", + }, + { + id: "CL-12", + role: "client", + specItem: 12, + level: "must", + applicability: "always", + statement: "Establishes that the AS advertises `https://pdpp.dev/data-access` before requesting it.", + specAnchor: "#conformance", + }, + { + id: "CL-13", + role: "client", + specItem: 13, + level: "must", + applicability: "always", + statement: + "Inspects the authorization result in each token response, including refresh responses, before requesting record disclosure, and requests no disclosure when it cannot obtain or interpret the result. Does not infer authorization from its original request, a tool listing, or token syntax.", + specAnchor: "#conformance", + }, + { + id: "CL-14", + role: "client", + specItem: 14, + level: "must", + applicability: "always", + statement: "Does not validate a projected `data` object as a full stream record.", + specAnchor: "#conformance", + }, + { + id: "CL-15", + role: "client", + specItem: 15, + level: "must", + applicability: "always", + statement: + "Ignores any member that it does not recognize in a Core-defined JSON object that a Section 8 endpoint returns. Does not fail to parse because of that member, as Response forward compatibility requires. This rule does not reach inside profile-defined objects such as an evidence item's `proof`; their profiles define parsing and verification failure.", + specAnchor: "#response-forward-compatibility", + }, + { + id: "CL-16", + role: "client", + specItem: 16, + level: "must", + applicability: "always", + statement: + "Treats an evidence item as absent unless it verifies the item under the profile for its type. Ignores an item with an unknown `type` or `attester.role`. Treats a local reference to an item that the response does not carry as absent, and retrieves a URI reference only as the type's profile defines. Infers record provenance from an item only when it has verified the item's binding to its own record context. For a composed binding, it also verifies the whole chain and trusts the mapping attester. Does not treat delivery as verification, and decides itself whether to trust the attester.", + specAnchor: "#conformance", + }, + { + id: "CL-17", + role: "client", + specItem: 17, + level: "must", + applicability: "always", + statement: + "Trains on PDPP inputs only when every contributing PDPP input has covering training permission and valid processing authority, as AI training defines, regardless of purpose code or acquisition grant. Applies this rule to derivatives and imposes it on every recipient or subprocessor receiving PDPP inputs. Treats the legacy purpose code `https://pdpp.dev/purpose/ai_training` as conferring no training authority.", + specAnchor: "#ai-training", + }, + { + id: "CL-18", + role: "client", + specItem: 18, + level: "must", + applicability: "always", + statement: + "Requests a processing permission only when the AS advertises it in `pdpp_processing_permissions_supported`, in a detail separate from any ordinary access, and with a `purpose_description` where Section 6 requires one.", + specAnchor: "#conformance", + }, + { + id: "CL-19", + role: "client", + specItem: 19, + level: "must", + applicability: "always", + statement: + "After a grant ends, uses its held data only within the grant's surviving-use terms, including only the limited continuation of already-issued processing authority permitted by the defining profile. Otherwise keeps, but does not use, data held only under a grant that carries a processing permission.", + specAnchor: "#conformance", + }, + { + id: "CL-20", + role: "client", + specItem: 20, + level: "must", + applicability: "always", + statement: + "Holds a positive assessment before first use of a grant's held data. Never treats record delivery, introspection, a read error, or a package read status as an assessment.", + specAnchor: "#conformance", + }, + { + id: "CL-21", + role: "client", + specItem: 21, + level: "must", + applicability: "always", + statement: + "While it runs and it or a party acting for it holds held data of a grant, attempts a lifecycle query at least once per reconciliation interval, starting one interval after first acquisition. Retries failed or unanswered attempts as Positive assessment and reconciliation requires, including its `Retry-After` and transport-failure widening rules, until an attempt succeeds or it holds no held data of the grant. Also starts a query before further use after a failure that shows the grant may have ended, and after resuming from suspension or power-off if an attempt fell due meanwhile. Does not treat a failed or unanswered query as a reason to stop use the grant's terms permit.", + specAnchor: "#reconciliation", + }, + { + id: "CL-22", + role: "client", + specItem: 22, + level: "must", + applicability: "always", + statement: + "On learning of an erasure instruction, stops use as soon as practicable and no later than the stop-use bound after its effective time, or at once if it learns of the instruction after that deadline. Deletes the covered data, with the scope and by the deadline Erasure defines. Records its first receipt, never restarts a deadline, and treats the instruction as terminal.", + specAnchor: "#erasure", + }, + { + id: "CL-23", + role: "client", + specItem: 23, + level: "must", + applicability: "always", + statement: + "Applies the overdue-disposal rules in Erasure before further use. Treats backups and legally retained data as Erasure defines.", + specAnchor: "#erasure", + }, + { + id: "CL-24", + role: "client", + specItem: 24, + level: "must", + applicability: "always", + statement: + "Imposes the held-data duties on every party acting for it, and relays erasure instructions with absolute deadlines to it, as Downstream holders defines.", + specAnchor: "#downstream-holders", + }, + { + id: "CL-25", + role: "client", + specItem: 25, + level: "must", + applicability: "conditional", + appliesWhen: "The client offers its own disconnect flow.", + statement: + "If it offers its own disconnect, states what happens to held data and offers `keep` and `delete` with neither preselected. Treats `delete` as an erasure instruction effective at once.", + specAnchor: "#conformance", + }, + { + id: "CL-26", + role: "client", + specItem: 26, + level: "must", + applicability: "always", + statement: + "Keeps the records that Erasure requires. Exercises already-issued processing authority after effective erasure only within that section's limited continuation and the defining profile's stop requirements.", + specAnchor: "#erasure", + }, + { + id: "CL-27", + role: "client", + specItem: 27, + level: "must", + applicability: "always", + statement: + "Sends a non-empty `purpose_description` that the owner can understand with any purpose code outside Appendix A.", + specAnchor: "#conformance", + }, + { + id: "CL-28", + role: "client", + specItem: 28, + level: "must", + applicability: "always", + statement: + "Retains the authenticated AS issuer with every grant and does not infer it from `grant_id`, `source.id`, or an untrusted credential, as Section 8 requires.", + specAnchor: "#conformance", + }, + { + id: "CL-29", + role: "client", + specItem: 29, + level: "must", + applicability: "always", + statement: + "Discloses PDPP data to an independent recipient only under an expressly covering registered processing permission and valid authority under its defining profile, except for the required-by-law case in Section 7.", + specAnchor: "#conformance", + }, + { + id: "CL-30", + role: "client", + specItem: 30, + level: "must", + applicability: "always", + statement: + "Classifies a receiving party by the operation it performs. Does not treat independent use as processing for the client. Applies the rule to derivatives and service outputs as well as source records.", + specAnchor: "#conformance", + }, + { + id: "CL-31", + role: "client", + specItem: 31, + level: "must", + applicability: "always", + statement: + "Does not infer independent-recipient disclosure authority from purpose, an owner action instruction, a claim, a positive assessment, an erasure exception, or read-token activity. Records any required-by-law disclosure as Section 7 requires.", + specAnchor: "#conformance", + }, + { + id: "CL-32", + role: "client", + specItem: 32, + level: "must", + applicability: "always", + statement: + "Pins a supported HTTP contract with `PDPP-Version` on each Core RS operation, including retries and pagination, and verifies the selected version before using a successful response. Does not infer a contract from a URL path, grant schema, document release, or profile version, and does not silently fall back after a version failure.", + specAnchor: "#conformance", + }, + { + id: "CL-33", + role: "client", + specItem: 33, + level: "must", + applicability: "always", + statement: + "Reads each StreamGrant's `required_fields` from the authorization result before requesting disclosure. Expects those fields in every returned record's `data` when the stored record version has a value for them, and does not infer the set from a declaration.", + specAnchor: "#conformance", + }, ]; /** Every Core Section 9 requirement, in spec order. The coverage denominator. */ diff --git a/packages/conformance-suite/src/requirements/matrix.ts b/packages/conformance-suite/src/requirements/matrix.ts index a07f39670..488862d5a 100644 --- a/packages/conformance-suite/src/requirements/matrix.ts +++ b/packages/conformance-suite/src/requirements/matrix.ts @@ -4,7 +4,7 @@ // The clause-level normative matrix for Core sections 4-8 and 10. // // WHY THIS EXISTS, given catalog.ts already exists. catalog.ts transcribes the -// 45 numbered items in Section 9. Those items are SUMMARIES: Section 9 AS item 2 +// 123 numbered items in Section 9. Those items are SUMMARIES: Section 9 AS item 2 // is one sentence standing in for six separate normative sentences spread across // sections 5 and 6. A report that says "23 of 36 applicable items" is therefore // not a statement about how much of the specification was exercised, because the @@ -22,14 +22,11 @@ // 2. `level` is the spec's own keyword. An RFC 2119 SHOULD is recorded as // `should`, never promoted to `must` because it sounds important. Promoting // one would report a conforming implementation as non-conformant. -// Eight entries are the exception and say so in their own `gapNote`: -// 5.6-2a, 6.6-1, 7.1-1, 7.2-0, 7.10-1, 8.1-1a, 8.9-16 and 8.14-1 quote -// subsections that carry NO RFC 2119 keyword, yet a Section 9 item states -// the same obligation as a MUST. Their level is the conformance list's, not -// the quoted text's, and the note records that provenance so a reader does -// not mistake it for a keyword this file invented. Without these entries -// seven Section 9 items — AS-11, AS-13, AS-17, RS-5, CL-4, CL-6, CL-8 — -// would trace to no spec text at all, which is itself the finding. +// 7 entries are the exception and say so in their own `gapNote`: +// 4.4-16, 5.8-3, 6.7-1, 7.1-1, 7.2-1, 7.24-1, 8.1-2 quote text that carries NO RFC 2119 keyword, yet a +// Section 9 item states the same obligation as a MUST. Their level is the +// conformance list's, not the quoted text's, and the note records that +// provenance so a reader does not mistake it for a keyword this file invented. // 3. `requirementIds` may be EMPTY. A clause that no Section 9 item summarizes // is a real finding about the spec's conformance section, and it is recorded // as such rather than forced into the nearest-looking item. @@ -81,7 +78,7 @@ export function specFileOf(clause: ClauseEntry): string { return clause.specFile ?? "spec-core.md"; } -// Clause inventory for Core section 2 (Terminology and Actors). +// Clause inventory for Core section 2 (2. Terminology and Actors). const SECTION_2: readonly ClauseEntry[] = [ { clauseId: "2.2-1", @@ -89,14 +86,14 @@ const SECTION_2: readonly ClauseEntry[] = [ requirementIds: ["RS-3", "AS-18"], level: "must", role: ["resource-server"], - text: "However, when the AS and RS are deployed separately, the AS-to-RS token-resolution contract is normative: the RS MUST authenticate to the RFC 7662 introspection endpoint and resolve the complete grant enforcement context from its response.", + text: "When the AS and RS are deployed separately, the AS-to-RS token-resolution contract is normative: the RS MUST authenticate to the RFC 7662 introspection endpoint and resolve the complete grant enforcement context from its response.", specAnchor: "#protocol-roles", applicability: "the deployment separates AS and RS", observable: "black-box-http", partial: true, caseIds: ["AS-18/introspection-requires-authentication"], gapNote: - "The authentication half is tested; that the RS resolves the COMPLETE enforcement context from the response has no dedicated case, and the same obligation at 8.2-2 is review-only.", + "The authentication half is tested; that the RS resolves the COMPLETE enforcement context from the response has no dedicated case, and the same obligation at 8.3-8 is review-only.", }, { clauseId: "2.2-2", @@ -110,7 +107,7 @@ const SECTION_2: readonly ClauseEntry[] = [ observable: "review-only", caseIds: [], gapNote: - "The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer, as at 8.2-2.", + "The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer, as at 8.3-8.", }, { clauseId: "2.2-3", @@ -127,9 +124,23 @@ const SECTION_2: readonly ClauseEntry[] = [ gapNote: "A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path.", }, + { + clauseId: "2.5-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server", "resource-server"], + text: "Core can make support for a Core-defined value depend on such a claim: an implementation that supports a processing permission MUST conform to the permission's defining profile (Section 6).", + specAnchor: "#requirements-language", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, ]; -// Clause inventory for Core section 3 (System Architecture), text before its first subsection. +// Clause inventory for Core section 3 (3. System Architecture). const SECTION_3: readonly ClauseEntry[] = [ { clauseId: "3.0-1", @@ -161,1536 +172,8831 @@ const SECTION_3: readonly ClauseEntry[] = [ }, ]; -// Clause inventory for Core sections 4-8 and 10. -// Every clause is verbatim text from spec-core.md at v0.1. +// Clause inventory for Core section 4 (4. Record Model). const SECTION_4: readonly ClauseEntry[] = [ { - clauseId: "4.3-1", + clauseId: "4.2-1", specVersion: "0.1", - requirementIds: ["RS-7"], - level: "may", + requirementIds: [], + level: "must", role: ["resource-server"], - text: "Resource servers MAY expire historical version data after a retention period.", - specAnchor: "#incremental-sync-for-mutable-streams", - applicability: "the target serves at least one `mutable_state` stream", - observable: "black-box-http", + text: "The resource server MUST leave the stored record, including its `emitted_at`, unchanged, and MUST NOT create a version or a change-history event.", + specAnchor: "#stream-semantics", + applicability: "always", + observable: "review-only", caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", }, { - clauseId: "4.3-2", + clauseId: "4.2-2", specVersion: "0.1", - requirementIds: ["RS-7"], + requirementIds: [], level: "must", role: ["resource-server"], - text: "If a client's cursor has expired, the resource server MUST return HTTP 410 Gone with error code `cursor_expired`.", - specAnchor: "#incremental-sync-for-mutable-streams", - applicability: "the target serves at least one `mutable_state` stream", - observable: "black-box-http", - caseIds: ["RS-7/expired-sync-cursor-reported-as-gone"], + text: "Differing `data` is a conflict: a profile-defined ingest or write interface that receives one MUST report the conflict to the producer and SHOULD make it visible to the owner.", + specAnchor: "#stream-semantics", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "4.3-3", + clauseId: "4.2-3", specVersion: "0.1", - requirementIds: ["CL-5"], + requirementIds: [], level: "must", - role: ["client"], - text: "The client MUST perform a full re-sync to re-establish its baseline.", - specAnchor: "#incremental-sync-for-mutable-streams", - applicability: "the client performs incremental sync", - observable: "client-capture", - caseIds: ["CL-5/full-resync-after-cursor-expired"], + role: ["authorization-server"], + text: "An authorization server MUST reject a declaration containing an unrecognized `streams[].semantics` value.", + specAnchor: "#stream-semantics", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "4.3-4", + clauseId: "4.2-4", specVersion: "0.1", - requirementIds: ["CL-3"], + requirementIds: [], level: "must", - role: ["client"], - text: "A client MUST NOT use a `next_cursor` value as a `changes_since` parameter; they are different token spaces and will produce a protocol error if confused.", - specAnchor: "#incremental-sync-for-mutable-streams", + role: ["resource-server"], + text: "A resource server MUST NOT interpret an unrecognized stream semantic as `append_only` or `mutable_state`.", + specAnchor: "#stream-semantics", applicability: "always", - observable: "client-capture", - caseIds: ["CL-3/changes-since-does-not-reuse-next-cursor"], + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "4.3-5", + clauseId: "4.3-1", specVersion: "0.1", - requirementIds: ["RS-8"], + requirementIds: ["CL-33"], level: "must", role: ["resource-server"], - text: "The terminal page of a `changes_since` result MUST include a `next_changes_since` field.", - specAnchor: "#incremental-sync-for-mutable-streams", - applicability: "the target supports `changes_since`", - observable: "black-box-http", - caseIds: ["RS-8/terminal-page-carries-next-changes-since", "RS-7/changes-since-accepted-on-multi-instance-grant"], + text: "For a client-token read, the RS MUST include those fields whenever it returns that record's `data` and the stored record version has a value for them.", + specAnchor: "#full-records-and-disclosed-projections", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", }, { - clauseId: "4.3-6", + clauseId: "4.3-2", specVersion: "0.1", - requirementIds: ["RS-7"], + requirementIds: ["RS-26"], level: "must", role: ["resource-server"], - text: "When a record is deleted from a `mutable_state` stream, the resource server MUST include a tombstone entry in incremental sync responses for clients whose cursor predates the deletion.", - specAnchor: "#incremental-sync-for-mutable-streams", - applicability: "the target serves at least one `mutable_state` stream", + text: "The RS MUST construct disclosed `data` from only the top-level members permitted by the grant and the request-time field selection.", + specAnchor: "#full-records-and-disclosed-projections", + applicability: "always", observable: "black-box-http", - caseIds: ["RS-7/deletion-surfaces-as-a-tombstone", "RS-7/tombstone-envelope-survives-sparse-fields"], + partial: true, + caseIds: ["RS-2/field-projection-not-exceeded"], + gapNote: + "The case reads a field-narrowed grant and checks that no returned record carries a member outside the grant's allowlist. It does not exercise a request-time field selection narrower than the grant.", }, { - clauseId: "4.3-7", + clauseId: "4.3-3", specVersion: "0.1", - requirementIds: ["RS-7"], - level: "should", + requirementIds: [], + level: "must", role: ["resource-server"], - text: "If the source system deletion time is unknown, the RS SHOULD use the `emitted_at` value of the delete directive as `deleted_at`.", - specAnchor: "#incremental-sync-for-mutable-streams", - applicability: "the target serves at least one `mutable_state` stream", - observable: "review-only", + text: "The RS MUST NOT insert nulls or fabricated values for withheld members.", + specAnchor: "#full-records-and-disclosed-projections", + applicability: "always", + observable: "black-box-http", caseIds: [], - gapNote: - "Not observable: whether the source deletion time was known is internal state, so a `deleted_at` value cannot be judged against this clause from outside.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "4.5-1", + clauseId: "4.3-4", specVersion: "0.1", - requirementIds: [], + requirementIds: ["CL-14"], level: "must", - role: ["source-declaration", "resource-server"], - text: "Each primary-key component MUST be serialized as a string in the canonical encoding. Non-string primary-key field values (e.g., integers, dates) MUST be converted to their string representation before encoding.", - specAnchor: "#the-record-envelope", + role: ["client"], + text: "A client MUST NOT validate a projected `data` object as though it were the full stream record.", + specAnchor: "#full-records-and-disclosed-projections", applicability: "always", - observable: "black-box-http", - caseIds: ["RS-1/compound-primary-key-canonically-encoded"], + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "4.5-2", + clauseId: "4.3-5", specVersion: "0.1", requirementIds: [], level: "must", - role: ["resource-server"], - text: "For any record, the values of the `data` fields named by the stream's `primary_key` MUST match the values in the `key` envelope field (in order). A resource server or profile-defined write interface MUST reject a record before storage when those values disagree.", - specAnchor: "#the-record-envelope", + role: ["authorization-server"], + text: "The AS MUST disclose this fact during owner review.", + specAnchor: "#full-records-and-disclosed-projections", applicability: "always", observable: "review-only", caseIds: [], gapNote: - "The rejection half binds a write interface the Collection Profile defines, which Core's read-only query surface cannot reach; the read half could be checked but no Section 9 item claims it.", + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", }, { - clauseId: "4.6-1", + clauseId: "4.3-6", specVersion: "0.1", requirementIds: [], - level: "should", - role: ["source-declaration"], - text: "Connector authors SHOULD use these names when the platform provides them, rather than inventing platform-specific names.", - specAnchor: "#timestamps", - applicability: "always", - observable: "declaration-static", + level: "must", + role: ["authorization-server"], + text: "If envelope disclosure is unacceptable, the AS MUST omit an optional stream or refuse issuance for a required stream.", + specAnchor: "#full-records-and-disclosed-projections", + applicability: "the condition the clause states holds", + observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "4.8-1", + clauseId: "4.3-7", specVersion: "0.1", - requirementIds: [], + requirementIds: ["RS-26"], level: "must", role: ["resource-server"], - text: "`mime_type` MUST be a valid IANA media type (see [IANA Media Types](https://www.iana.org/assignments/media-types/)).", - specAnchor: "#binary-data-blobref", - applicability: "a served record carries a `blob_ref`", + text: "An RS unable to serve a requested projection without changing the meaning of the disclosed data MUST refuse that read with HTTP 403 `disclosure_unavailable`.", + specAnchor: "#full-records-and-disclosed-projections", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["RS-1/blob-ref-mime-type-valid"], - gapNote: - "The value lives in a record's `data.blob_ref`, so the case reads served records for the blob fixture's grant. It judges media-type STRUCTURE only: a registered top-level type or a well-formed `x-` token, a `/`, and a subtype made of RFC 6838 restricted-name characters (parameters are not validated). It does not consult the live IANA registry, so a well-formed but unregistered subtype passes, and it observes only the records that fixture exposes. A `mime_type` that is present but not a string fails; a `blob_ref` with no `mime_type` skips. No Section 9 item covers it, so `requirementIds` stays empty; the case is filed under RS-1 because the blob fixture is its surface.", - partial: true, + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, -]; - -// --------------------------------------------------------------------------- -// Section 5: Source Declaration -// --------------------------------------------------------------------------- - -const SECTION_5: readonly ClauseEntry[] = [ { - clauseId: "5.2-1", + clauseId: "4.3-8", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The authorization server MUST treat `publisher.id` as authenticated only where an accepted channel or configured mapping binds that publisher to the declaration; absent that binding it MUST NOT support source acceptance, redirect policy, attribution, or any other trust decision.", - specAnchor: "#sourcedeclaration-fields", + role: ["resource-server"], + text: "It MUST NOT repair the projection by disclosing unauthorized fields.", + specAnchor: "#full-records-and-disclosed-projections", applicability: "always", - observable: "review-only", + observable: "black-box-http", caseIds: [], - gapNote: - "What the AS treated as authenticated is an internal trust decision; from outside, an accepted and a rejected declaration differ only by whether consent proceeded, which does not discriminate this clause.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.2-2", + clauseId: "4.4-1", specVersion: "0.1", requirementIds: [], level: "must", - role: ["source-declaration"], - text: "`primary_key` and `cursor_field` MUST reference fields declared here.", - specAnchor: "#sourcedeclaration-fields", + role: ["resource-server"], + text: "A `next_changes_since` token MUST NOT reveal a readable position or time.", + specAnchor: "#incremental-sync", applicability: "always", - observable: "declaration-static", - caseIds: ["AS-16/key-field-not-declared-in-schema-refused"], - gapNote: - "No Section 9 item covers declaration internal consistency, so `requirementIds` stays empty; the case is filed under AS-16 because declaration acceptance is the surface it observes.", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.2-3", + clauseId: "4.4-2", specVersion: "0.1", requirementIds: [], level: "must", - role: ["source-declaration"], - text: "MUST reference a field declared in the schema.", - specAnchor: "#sourcedeclaration-fields", - applicability: "the stream declares `consent_time_field`", - observable: "declaration-static", - caseIds: ["AS-16/consent-time-field-not-declared-in-schema-refused"], - gapNote: - "No Section 9 item covers `consent_time_field` declaration validity, so `requirementIds` stays empty; the case is filed under AS-16 because declaration acceptance is the surface it observes.", + role: ["resource-server"], + text: "Token equality or ordering MUST NOT signal a change that is not visible to the grant, or to the owner session's effective scope, including after a session that returns nothing.", + specAnchor: "#incremental-sync", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.2-4", + clauseId: "4.4-3", specVersion: "0.1", - requirementIds: ["AS-2"], + requirementIds: ["RS-21"], level: "must", - role: ["authorization-server"], - text: "The AS MUST reject grants that request `time_range` on a stream without a `consent_time_field`, or that request an unsupported selection parameter.", - specAnchor: "#sourcedeclaration-fields", + role: ["resource-server"], + text: "A page cursor MUST NOT reveal a readable position, time, or field value either.", + specAnchor: "#incremental-sync", applicability: "always", observable: "black-box-http", - caseIds: ["AS-2/time-range-without-consent-time-field-refused"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.2-5", + clauseId: "4.4-4", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: ["RS-7", "RS-20"], level: "must", - role: ["authorization-server", "source-declaration"], - text: "If `$schema` is present, it MUST equal `https://json-schema.org/draft/2020-12/schema`. This dialect choice does not by itself guarantee identical validator behavior. The AS MUST meta-validate each embedded stream schema before accepting the declaration. Embedded `$ref` and `$dynamicRef` values MUST be local fragment references. A declaration MUST NOT make consent interpretation depend on a mutable remote schema.", - specAnchor: "#sourcedeclaration-fields", - applicability: "always", - observable: "declaration-static", + role: ["resource-server"], + text: "The resource server MUST emit a tombstone for each record visible at h0 and not visible at h1.", + specAnchor: "#incremental-sync", + applicability: "the target serves at least one `mutable_state` stream", + observable: "black-box-http", partial: true, - caseIds: ["AS-16/embedded-schema-remote-reference-refused"], + caseIds: ["RS-7/deletion-surfaces-as-a-tombstone"], gapNote: - "Partially covered. The case exercises the local-reference obligation (`$ref`/`$dynamicRef` must be local fragments), which is the half that makes consent interpretation depend on a mutable remote schema. The `$schema` dialect equality and full metaschema meta-validation are implemented by the reference target but not separately asserted: a case for each would need its own defect to discriminate, and the remote-reference oracle already fails a target that skips embedded-schema validation altogether.", + "Only the emission of a tombstone for a deleted record is observed. That no tombstone is emitted for any other record, and that a record moved out of the query context also yields one, is not tested. RS-7/tombstone-envelope-survives-sparse-fields is no longer mapped here: it requires `deleted_at` and `emitted_at` on every tombstone, and the locked text gives a client-token tombstone neither.", }, { - clauseId: "5.3-1", + clauseId: "4.4-5", specVersion: "0.1", requirementIds: [], - level: "may", - role: ["source-declaration"], - text: "Streams MAY include a `display` object with human-readable metadata for the consent UI.", - specAnchor: "#stream-display", + level: "must", + role: ["resource-server"], + text: "It MUST NOT emit a tombstone for any other record.", + specAnchor: "#incremental-sync", applicability: "always", - observable: "declaration-static", + observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.3-2", + clauseId: "4.4-6", specVersion: "0.1", - requirementIds: ["AS-7"], - level: "should", - role: ["authorization-server"], - text: "If absent, the AS SHOULD display `streams[].description` or fall back to the stream name.", - specAnchor: "#stream-display", - applicability: "the declaration omits `display.label`", - observable: "review-only", + requirementIds: ["RS-20"], + level: "must", + role: ["resource-server"], + text: "The resource server MUST retain the state needed to determine visibility at every unexpired horizon.", + specAnchor: "#incremental-sync", + applicability: "always", + observable: "black-box-http", caseIds: [], - gapNote: - "Consent-surface rendering is not observable over HTTP; a case could only assert on a deployment's HTML, which tests the deployment rather than the protocol.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.3-3", + clauseId: "4.4-7", specVersion: "0.1", - requirementIds: ["AS-7"], - level: "may", - role: ["authorization-server"], - text: "If absent, the AS MAY generate a description from the stream schema, or display no detail.", - specAnchor: "#stream-display", - applicability: "the declaration omits `display.detail`", - observable: "review-only", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "If it cannot determine that state, it MUST fail the session with 410 `cursor_expired`.", + specAnchor: "#incremental-sync", + applicability: "the condition the clause states holds", + observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.3-4", + clauseId: "4.4-8", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: ["CL-10"], level: "must", role: ["client"], - text: "The requesting client MUST NOT override or supplement these descriptions in the selection request.", - specAnchor: "#stream-display", + text: "A client MUST treat a tombstone for a record it does not hold as a no-op.", + specAnchor: "#incremental-sync", applicability: "always", observable: "client-capture", - caseIds: ["AS-7/client-does-not-override-display-descriptions"], - }, - { - clauseId: "5.4-1", - specVersion: "0.1", - requirementIds: [], - level: "must", - role: ["source-declaration"], - text: "The `consent_time_field` may be the same field as `cursor_field`, but they serve different purposes and MUST be declared separately: [...] `cursor_field` governs incremental sync mechanics (which records to fetch since the last run). [...] `consent_time_field` governs consent-time filtering (which records fall within the authorized time window).", - specAnchor: "#consenttimefield", - applicability: "always", - observable: "declaration-static", caseIds: [], - gapNote: - "No Section 9 item covers it, so `requirementIds` stays empty. Core explicitly permits streams that cannot define a stable `consent_time_field` to omit it, so the former negative fixture (`time_range_capable` with no consent field) did not violate this clause. A valid black-box fixture would need to observe the AS deriving a consent boundary from `cursor_field`; the declaration submission surface exposes neither that derived state nor a normative capability member.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.4-2", + clauseId: "4.4-9", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The `consent_time_field` MUST be rendered in human-readable consent UX.", - specAnchor: "#consenttimefield", - applicability: "the grant carries a `time_constraint`", - observable: "review-only", + role: ["client"], + text: "After a completed `changes_since=beginning` session, the client MUST replace its copy of the stream synchronized under that grant and query context with that session's result.", + specAnchor: "#incremental-sync", + applicability: "the condition the clause states holds", + observable: "client-capture", caseIds: [], - gapNote: - "A rendering obligation on the consent surface, which black-box HTTP cannot observe; the suite can see what the AS bound, not what it showed.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.6-1", + clauseId: "4.4-10", specVersion: "0.1", - requirementIds: [], + requirementIds: ["RS-7"], level: "may", - role: ["source-declaration"], - text: "Declaration publishers MAY suggest views.", - specAnchor: "#views", - applicability: "always", - observable: "declaration-static", + role: ["resource-server"], + text: "Resource servers MAY expire historical change data after a retention period.", + specAnchor: "#incremental-sync", + applicability: "the target serves at least one `mutable_state` stream", + observable: "black-box-http", caseIds: [], }, { - clauseId: "5.6-2", + clauseId: "4.4-11", specVersion: "0.1", - requirementIds: ["AS-12"], + requirementIds: ["RS-7"], level: "must", - role: ["authorization-server"], - text: "The AS MUST NOT define a view that includes fields absent from the retained SourceDeclaration schema for the relevant stream.", - specAnchor: "#views", - applicability: "the AS supports views", + role: ["resource-server"], + text: "If a client's token has expired, the resource server MUST return HTTP 410 Gone with error code `cursor_expired`.", + specAnchor: "#incremental-sync", + applicability: "the target serves at least one `mutable_state` stream", observable: "black-box-http", - caseIds: ["AS-12/view-within-declared-schema"], + caseIds: ["RS-7/expired-sync-cursor-reported-as-gone"], }, { - clauseId: "5.6-2a", + clauseId: "4.4-12", specVersion: "0.1", - requirementIds: ["AS-13"], + requirementIds: ["CL-5"], level: "must", - role: ["authorization-server"], - text: "Grants are bound to the resolved field set at issuance time: `fields` in the `StreamGrant` is authoritative, not the view name. View evolution (adding new fields to a view) never silently widens existing grants. Re-consent is required before a client can access new fields, even if those fields are subsequently added to a named view the client already has a grant for.", - specAnchor: "#views", - applicability: "the AS supports views", - observable: "black-box-http", - partial: true, - caseIds: ["AS-13/view-evolution-does-not-widen-an-issued-grant"], - gapNote: - "Recorded as MUST-equivalent because Section 9 AS item 13 states it as one; the spec's own sentence here carries no RFC 2119 keyword, so the binding level is the conformance list's rather than this subsection's. The covering case exercises the field-widening half against a target whose view evolves after issuance; the re-consent half binds a consent flow the suite cannot drive.", + role: ["client"], + text: "The client MUST start again from `beginning`.", + specAnchor: "#incremental-sync", + applicability: "the client performs incremental sync", + observable: "client-capture", + caseIds: ["CL-5/full-resync-after-cursor-expired"], }, { - clauseId: "5.6-3", + clauseId: "4.4-13", specVersion: "0.1", - requirementIds: [], + requirementIds: ["CL-3"], level: "must", - role: ["authorization-server", "resource-server", "client"], - text: "Implementations MUST treat unrecognized view URIs as opaque identifiers.", - specAnchor: "#views", - applicability: "the AS supports views", + role: ["client"], + text: "A client MUST NOT use a `next_cursor` value as a `changes_since` parameter; they are different token spaces and will produce a protocol error if confused.", + specAnchor: "#incremental-sync", + applicability: "always", + observable: "client-capture", + caseIds: ["CL-3/changes-since-does-not-reuse-next-cursor"], + }, + { + clauseId: "4.4-14", + specVersion: "0.1", + requirementIds: ["RS-8"], + level: "must", + role: ["resource-server"], + text: "The terminal page of a `changes_since` result MUST include a `next_changes_since` field.", + specAnchor: "#incremental-sync", + applicability: "the target supports `changes_since`", observable: "black-box-http", - caseIds: ["AS-13/unrecognized-view-uri-treated-as-opaque"], + caseIds: ["RS-8/terminal-page-carries-next-changes-since", "RS-7/changes-since-accepted-on-multi-instance-grant"], }, { - clauseId: "5.8-1", + clauseId: "4.4-15", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: [], level: "must", - role: ["client"], - text: "A client MUST NOT introduce a new source authority or declaration URI during authorization.", - specAnchor: "#declaration-acceptance", + role: ["resource-server"], + text: "A non-terminal page MUST NOT include it, because the resource server never issues a token for pages it has not delivered.", + specAnchor: "#incremental-sync", applicability: "always", observable: "black-box-http", caseIds: [], - gapNote: - "The former case submitted a non-normative top-level `authority` member to an operator declaration-onboarding hook. Core defines the accepted authority as AS-side acceptance metadata and gives `SourceDeclaration` no such document member; this surface cannot observe a client introducing authority during authorization. A client-authorization fixture with an overridable source identifier is required.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.8-2", + clauseId: "4.4-16", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: ["CL-4"], level: "must", - role: ["authorization-server"], - text: "For a `provider_native` source, `source.id` MUST be identical to the protected-resource identifier the authorization server has already accepted for that resource. The authorization server MUST reject any mismatch before consent or grant issuance.", - specAnchor: "#declaration-acceptance", - applicability: "the source kind is `provider_native`", - observable: "black-box-http", - caseIds: ["AS-16/provider-native-source-id-mismatch-refused"], + role: ["client"], + text: "then store `next_changes_since` from the terminal page for the next session.", + specAnchor: "#incremental-sync", + applicability: "the client performs incremental sync", + observable: "client-capture", + caseIds: ["CL-4/stores-terminal-next-changes-since"], + gapNote: "The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's.", }, { - clauseId: "5.8-3", + clauseId: "4.6-1", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "Without such a binding, the authorization server MUST NOT rely on `publisher.id` for source acceptance, attribution, redirect policy, or any other trust decision.", - specAnchor: "#declaration-acceptance", + role: ["resource-server"], + text: "For `op: delete`, `data` MUST contain every primary-key field, matching `key`; other schema-required fields MAY be absent.", + specAnchor: "#the-record-envelope", applicability: "always", - observable: "review-only", + observable: "black-box-http", caseIds: [], - gapNote: - "Restates 5.2-1 as an acceptance obligation; what the AS relied on internally is not observable from a request outcome.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.8-4", + clauseId: "4.6-2", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "Different parsed content under an accepted key is equivocation: the authorization server MUST reject it and retain the previously accepted content, and MUST NOT infer ordering or freshness from `declaration_version`.", - specAnchor: "#declaration-acceptance", + role: ["source-declaration", "resource-server"], + text: "Each primary-key component MUST be serialized as a string in the canonical encoding.", + specAnchor: "#the-record-envelope", applicability: "always", observable: "black-box-http", - caseIds: ["AS-16/declaration-equivocation-refused-and-prior-content-retained"], + caseIds: ["RS-1/compound-primary-key-canonically-encoded"], }, { - clauseId: "5.8-5", + clauseId: "4.6-3", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "When retrieving a declaration, the authorization server MUST use HTTPS without ambient credentials. It MUST validate every redirect target and the final URL against its accepted declaration pointer and network policy. It MUST reject a declaration that requires automatic retrieval of a remote schema, and MUST fail closed when any check fails.", - specAnchor: "#declaration-acceptance", - applicability: "the AS retrieves declarations over the network", + role: ["resource-server"], + text: 'Each primary-key field\'s schema type MUST be exactly `string`: not a type array such as `["string","null"]`, and not any other type.', + specAnchor: "#the-record-envelope", + applicability: "always", observable: "black-box-http", caseIds: [], - gapNote: - "Missing fixture: an inbound HTTP endpoint serving a candidate declaration that logs each request the AS makes (credentials sent, redirect targets followed, remote schema retrieval). The harness already hosts one for client identity documents (src/tests/client-identity.ts serveDocument) but has no hook to offer an AS a declaration pointer to retrieve. The HTTPS requirement needs a TLS fixture, and the connection-bound address check is internal to the AS, so those parts stay unobservable even with the fixture.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.8-6", + clauseId: "4.6-4", specVersion: "0.1", - requirementIds: ["AS-16"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The authorization server MUST validate the destination address against its network policy immediately before each connection attempt, including each redirect hop. It MUST connect only to an address from that validated result.", - specAnchor: "#declaration-acceptance", - applicability: "the AS retrieves declarations over the network", - observable: "review-only", + role: ["resource-server"], + text: "The stream schema MUST list the field in `required`.", + specAnchor: "#the-record-envelope", + applicability: "always", + observable: "black-box-http", caseIds: [], - gapNote: - "A DNS-rebinding defence stated as a connection-time ordering obligation; distinguishing it from a URL-time check needs control of the AS's resolver, not an HTTP client.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "5.8-7", + clauseId: "4.6-5", specVersion: "0.1", - requirementIds: ["AS-7", "AS-16"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "An authorization server MUST render declaration display values safely for the output context, by context-appropriate escaping, by sanitization, or by any construction that guarantees the value cannot be interpreted as markup, script, or a control sequence in that context.", - specAnchor: "#declaration-acceptance", + role: ["resource-server"], + text: "Its value MUST be a non-empty string, used exactly as stored, with no Unicode or case normalization.", + specAnchor: "#the-record-envelope", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.6-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "A resource server or profile-defined write interface MUST reject a record whose primary-key field value is not a non-empty string.", + specAnchor: "#the-record-envelope", applicability: "always", observable: "review-only", caseIds: [], gapNote: - "The obligation is on rendered output in an unspecified output context; asserting on it means asserting on a deployment's HTML, which is outside what this suite judges.", + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "5.8-8", + clauseId: "4.6-7", specVersion: "0.1", - requirementIds: ["AS-16", "RS-15"], + requirementIds: [], level: "must", - role: ["authorization-server", "resource-server"], - text: "Current declaration capabilities MUST NOT widen an issued grant.", - specAnchor: "#declaration-acceptance", + role: ["resource-server"], + text: "Within JSON, `resources[]` entries and response record `id` values carry the canonical key string without percent-encoding, and the AS and RS MUST NOT percent-decode them.", + specAnchor: "#the-record-envelope", applicability: "always", observable: "black-box-http", - caseIds: ["RS-15/client-metadata-projection-closed"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, -]; - -// --------------------------------------------------------------------------- -// Section 6: Selection Request -// --------------------------------------------------------------------------- - -const SECTION_6: readonly ClauseEntry[] = [ { - clauseId: "6.1-1", + clauseId: "4.6-8", specVersion: "0.1", - requirementIds: ["AS-7"], - level: "may", + requirementIds: [], + level: "must", role: ["authorization-server"], - text: "The AS MAY replace or augment inline values with locally registered metadata, validated binding metadata, validated software-statement metadata, or trust-registry metadata.", - specAnchor: "#client-display", + text: "The AS MUST reject a `resources[]` entry that is not in canonical form.", + specAnchor: "#the-record-envelope", applicability: "always", - observable: "review-only", + observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.1-2", + clauseId: "4.6-9", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "a conforming authorization server MUST NOT reject a valid client ID metadata document solely because the client is not preregistered.", - specAnchor: "#client-display", + role: ["resource-server"], + text: "For any record, the values of the `data` fields named by the stream's `primary_key` MUST equal the values in the `key` envelope field, as strings and in order. A resource server or profile-defined write interface MUST reject a record before storage when they differ.", + specAnchor: "#the-record-envelope", applicability: "always", - observable: "black-box-http", - caseIds: ["AS-7/valid-url-hosted-client-identity-accepted", "AS-7/malformed-url-hosted-client-identity-refused"], + observable: "review-only", + caseIds: [], + gapNote: + "The rejection half binds a write interface the Collection Profile defines, which Core's read-only query surface cannot reach; the read half could be checked but no Section 9 item claims it.", }, { - clauseId: "6.1-3", + clauseId: "4.6-10", specVersion: "0.1", requirementIds: [], - level: "may", - role: ["authorization-server"], - text: "the server MAY still deny authorization, rate-limit the client, or require a registry-derived trust or admission result, under local policy and for any reason other than the absence of preregistration.", - specAnchor: "#client-display", + level: "must", + role: ["resource-server"], + text: "A client-token response MUST NOT include `emitted_at`; an owner-token response includes it.", + specAnchor: "#the-record-envelope", applicability: "always", observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.1-4", + clauseId: "4.6-11", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: ["CL-10"], level: "must", - role: ["authorization-server"], - text: "For PDPP Core v0.1 interoperability, a conforming authorization server MUST accept a valid URL-hosted client identity unless local policy denies authorization.", - specAnchor: "#client-display", + role: ["client"], + text: "A client MUST identify a record within a stream by its `instance_id` and `id` together.", + specAnchor: "#the-record-envelope", applicability: "always", - observable: "black-box-http", - caseIds: ["AS-7/valid-url-hosted-client-identity-accepted", "AS-7/malformed-url-hosted-client-identity-refused"], + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.1-5", + clauseId: "4.6-12", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The AS MUST resolve requester identity metadata from the best available source. Source precedence is local registration or trust-registry metadata, then validated software-statement metadata if supported, then validated binding metadata, then inline `client_display`, then `client_id` fallback.", - specAnchor: "#client-display", + role: ["resource-server"], + text: "Applying an instruction MUST atomically produce a record that satisfies the stream schema and the record identity rule above.", + specAnchor: "#the-record-envelope", applicability: "always", observable: "review-only", caseIds: [], gapNote: - "Which source the AS resolved from is visible only on the consent surface it renders, which is not black-box observable.", + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", }, { - clauseId: "6.1-6", + clauseId: "4.6-13", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "If the resolved metadata contains a display name, the AS MUST display it to the user during consent. If no display name is available, the AS MUST display `client_id` as the requester identity.", - specAnchor: "#client-display", + role: ["resource-server"], + text: "A profile MUST NOT use a partial-update instruction to modify an existing `append_only` record.", + specAnchor: "#the-record-envelope", applicability: "always", observable: "review-only", caseIds: [], gapNote: - "A display obligation on the consent surface; a case can see what the AS bound into the grant, not what it rendered.", + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "6.1-7", + clauseId: "4.7-1", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "may", - role: ["authorization-server"], - text: "If the resolved metadata contains `policy_uri` or `tos_uri`, the AS MAY display them as secondary links or disclosures.", - specAnchor: "#client-display", - applicability: "always", - observable: "review-only", + role: ["resource-server"], + text: "A record that the resource server serves (Section 8) MAY carry an `evidence` member.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", + observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.1-8", + clauseId: "4.7-2", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: 'If the server has a positive trust signal for the client (e.g., verified domain control, trust registry membership), it MUST render that status distinctly (e.g., a "verified" badge). If it has no positive trust signal, it MUST treat the client as unverified and SHOULD display an "unverified app" indicator.', - specAnchor: "#client-display", + role: ["resource-server"], + text: "MUST NOT be an absolute URI.", + specAnchor: "#record-evidence", applicability: "always", - observable: "review-only", + observable: "black-box-http", caseIds: [], - gapNote: - "Trust-signal rendering is a consent-surface obligation; not observable over the protocol surface this suite drives.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.1-9", + clauseId: "4.7-3", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: 'The AS MAY treat verified domain control as a positive trust signal under obligation 4, and when it does it MUST name the verified domain rather than assert an unqualified verification (for example "Verified domain: example.com", not "Verified app"). [...] the AS MUST NOT present it as one.', - specAnchor: "#client-display", - applicability: "the AS treats verified domain control as a trust signal", + role: ["resource-server"], + text: "The proof MUST cover `refs`.", + specAnchor: "#record-evidence", + applicability: "always", observable: "review-only", caseIds: [], - gapNote: "Consent-surface wording obligation; not observable without asserting on a deployment's rendered text.", + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "6.1-10", + clauseId: "4.7-4", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The AS MUST treat `logo_uri` as untrusted content until it has been accepted under local policy. It MUST NOT fetch and render a client-supplied remote logo in the consent UI unless the client is verified or the asset has been proxied, cached, and approved under local policy. For unverified clients, the AS SHOULD generate a monogram from the resolved display name.", - specAnchor: "#client-display", + role: ["resource-server"], + text: "A resource server MUST NOT add a member to an item.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", caseIds: [], - gapNote: - "Missing fixture: an inbound HTTP endpoint named as an unverified client's `logo_uri` that logs requests, plus a way to open the consent surface so a fetch triggered by rendering it is seen. The harness hosts a request-logging fixture for client identity documents (src/tests/client-identity.ts serveDocument) but has no consent-render hook. A fetch made by the owner's browser, rather than the AS, would be seen only if the fixture logs it; the verified-client and proxied-asset exceptions need local policy knowledge the suite lacks.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.1-11", + clauseId: "4.7-5", specVersion: "0.1", - requirementIds: ["AS-7"], - level: "should", - role: ["authorization-server"], - text: "If neither resolved metadata nor inline `client_display` provides a display name, the consent UI SHOULD clearly indicate that the client has not provided display metadata.", - specAnchor: "#client-display", - applicability: "the client provides no display name", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "Record binding.** The verification procedure that a profile defines MUST bind the claim to the record context as served to this caller.", + specAnchor: "#record-evidence", + applicability: "always", observable: "review-only", caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "6.2-1", + clauseId: "4.7-6", specVersion: "0.1", requirementIds: [], - level: "may", - role: ["authorization-server"], - text: "An authorization server MAY support clients that are public and pre-registered by the deployment rather than dynamically registered.", - specAnchor: "#pre-registered-public-clients", + level: "must", + role: ["resource-server"], + text: "The verification procedure MUST distinguish owner-token and client-token contexts.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.2-2", + clauseId: "4.7-7", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "the field MUST NOT contain secrets, access tokens, owner-scoped clients, dynamically registered clients, or private registration state.", - specAnchor: "#pre-registered-public-clients", - applicability: "the AS advertises `pre_registered_public` in `pdpp_registration_modes_supported`", + role: ["resource-server"], + text: "It MUST authenticate each identifier together with its namespace.", + specAnchor: "#record-evidence", + applicability: "always", observable: "black-box-http", - caseIds: ["AS-7/pre-registered-public-clients-carry-no-private-state"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.3-1", + clauseId: "4.7-8", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: 'The AS MUST render `client_claims` content separately from protocol-enforced grant terms and MUST attribute it to the client (e.g., "[client name] says:"). The AS MUST NOT render client claims in the same visual register as protocol-enforced grant terms, structured policy declarations, or declaration-authored data descriptions.', - specAnchor: "#client-claims", - applicability: "the selection request carries `client_claims`", + role: ["resource-server"], + text: "The mapping item's `refs` MUST name the attester's item, and its proof MUST authenticate the complete referenced item and the attester's record identifier.", + specAnchor: "#record-evidence", + applicability: "always", observable: "review-only", caseIds: [], gapNote: - "A visual-register obligation on the consent surface. This is the clause AS-7 chiefly summarizes, and it is the clearest example of a MUST that is not black-box observable in principle.", + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "6.3-2", + clauseId: "4.7-9", specVersion: "0.1", - requirementIds: ["AS-15"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "If rendered on the final owner review surface, `client_claims` MUST be normalized and bound exactly, with client attribution, into the immutable final approval artifact and review revision. Retained consent evidence MUST preserve that binding.", - specAnchor: "#client-claims", - applicability: "`client_claims` are rendered on the final review surface", - observable: "black-box-http", - caseIds: ["AS-15/rendered-client-claims-bound-with-attribution"], - }, - { - clauseId: "6.3-3", - specVersion: "0.1", - requirementIds: ["AS-3"], - level: "should", - role: ["authorization-server", "client"], - text: 'Structured grant fields (e.g., `retention.max_duration`, `access_mode`) SHOULD be rendered by the AS as server-generated display text (e.g., "Deleted within 90 days", "Ongoing access until you revoke it"). Clients SHOULD NOT duplicate machine-readable constraints as free-text commitments.', - specAnchor: "#client-claims", - applicability: "always", - observable: "review-only", + role: ["client"], + text: "If a client cannot verify a binding, directly or through a composed chain, the item attests only detached field values, and the client MUST NOT infer from it where the record came from.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", + observable: "client-capture", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.4-1", + clauseId: "4.7-10", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "A conformant AS MUST preserve the distinction between protocol-enforced terms, structured policy declarations, declaration-authored data descriptions, and client-authored claims. It MUST NOT flatten these categories into a single undifferentiated consent surface.", - specAnchor: "#semantic-classes-and-consent-surface-rendering", + role: ["client"], + text: "Core defines no member that states that an item is verified, and a client MUST NOT treat delivery as verification.", + specAnchor: "#record-evidence", applicability: "always", - observable: "review-only", + observable: "client-capture", caseIds: [], - gapNote: - "The normative core of AS-7, and not observable over HTTP: a case can see what the server bound, not what it rendered.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.5-1", + clauseId: "4.7-11", specVersion: "0.1", - requirementIds: ["AS-6"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The AS MUST accept any syntactically valid absolute-URI purpose code. For unrecognized codes, the AS MUST display `purpose_description` if present, or the raw URI if not, and MUST NOT reject the request solely because the purpose code is unrecognized.", - specAnchor: "#request-level-parameters", + role: ["resource-server"], + text: "References.** A resource server MUST NOT deliver an item whose local `refs` entries do not all name items delivered in the same array.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - caseIds: ["AS-6/unregistered-purpose-not-rejected"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.5-2", + clauseId: "4.7-12", specVersion: "0.1", - requirementIds: ["AS-7"], + requirementIds: [], level: "must", - role: ["authorization-server", "client"], - text: "Clients SHOULD provide this field. When present, the AS MUST display it.", - specAnchor: "#request-level-parameters", + role: ["client"], + text: "A client MUST NOT retrieve the URI unless the profile defines retrieval, access control, and integrity binding for it.", + specAnchor: "#record-evidence", applicability: "always", observable: "review-only", caseIds: [], gapNote: - "The AS half is a display obligation on the consent surface; the client half is a SHOULD needing client capture. Neither is reachable from this suite's seam.", + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "6.6-1", + clauseId: "4.7-13", specVersion: "0.1", - requirementIds: ["CL-8"], + requirementIds: ["RS-30"], level: "must", - role: ["client", "authorization-server"], - text: "A selection request does not carry `source.kind`. The authorization server derives the provenance class from the declaration it accepted for `source.id`, and records it in consent evidence and any issued grant, where a client reads it back through introspection. A client whose policy depends on provenance therefore reads it from the issued grant rather than asserting an expectation in the request; Section 9 states that as a client requirement.", - specAnchor: "#source-kinds", - applicability: "the client has provenance-dependent local policy", - observable: "client-capture", - caseIds: ["CL-8/reads-source-kind-from-grant"], + role: ["resource-server"], + text: "The RS MUST include R in every client record projection.", + specAnchor: "#record-evidence", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.7-1", + clauseId: "4.7-14", specVersion: "0.1", - requirementIds: ["AS-14"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The AS MUST obtain explicit affirmative user consent before issuing any grant with `purpose_code` value `https://pdpp.dev/purpose/ai_training`.", - specAnchor: "#ai-training-consent", - applicability: "the AS issues grants carrying the ai_training purpose code", - observable: "review-only", - caseIds: ["AS-14/explicit-consent-required-for-ai-training"], + role: ["resource-server"], + text: "For an explicit request field set Q, the RS MUST validate request syntax, resolve the active grant, and authorize the stream before comparing Q with G.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.8-1", + clauseId: "4.7-15", specVersion: "0.1", - requirementIds: ["AS-2", "AS-5"], + requirementIds: ["RS-31", "RS-32"], level: "must", - role: ["authorization-server"], - text: "Mutually exclusive with `fields` in a request; both MUST NOT be present simultaneously. AS returns 400 `invalid_request` if both are present.", - specAnchor: "#stream-selection-parameters", - applicability: "the AS supports views", + role: ["resource-server"], + text: "If Q is not a subset of G, the RS MUST reject the whole read with HTTP 403 `field_not_granted` before any source-schema or record-data lookup.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["AS-2/view-and-fields-mutually-exclusive"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.8-2", + clauseId: "4.7-16", specVersion: "0.1", - requirementIds: ["AS-2"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The authorization server MUST reject selection requests that specify `time_range` on a stream without that field.", - specAnchor: "#stream-selection-parameters", + role: ["resource-server"], + text: "It MUST return no records.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - caseIds: ["AS-2/time-range-without-consent-time-field-refused"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.8-3", + clauseId: "4.7-17", specVersion: "0.1", - requirementIds: ["AS-2", "AS-4"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "A wildcard entry MUST be the only entry in `streams`. Otherwise stream names MUST be unique within the request.", - specAnchor: "#stream-selection-parameters", + role: ["resource-server"], + text: "The error's `param` MUST be `fields`, and its message MUST name exactly the field names in Q that are absent from G.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - caseIds: ["AS-2/wildcard-beside-named-stream-refused", "AS-2/duplicate-stream-name-refused"], - }, - { - clauseId: "6.8-4", - specVersion: "0.1", - requirementIds: ["CL-1"], - level: "should", - role: ["client"], - text: "Clients SHOULD request only the data they need (see [Section 11, Data Minimization](#data-minimization)).", - specAnchor: "#stream-selection-parameters", - applicability: "always", - observable: "client-capture", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "6.9-1", + clauseId: "4.7-18", specVersion: "0.1", - requirementIds: ["AS-2"], + requirementIds: ["RS-32"], level: "must", - role: ["authorization-server"], - text: "Each selection preset MUST NOT contain the same stream name more than once.", - specAnchor: "#selection-presets", - applicability: "the AS supports selection presets", + role: ["resource-server"], + text: "For the same well-formed request and grant, the response MUST be identical whether one of those names exists in the current source schema or not.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["AS-16/duplicate-stream-in-a-selection-preset-refused"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, -]; - -// --------------------------------------------------------------------------- -// Section 7: Grant -// --------------------------------------------------------------------------- - -const SECTION_7: readonly ClauseEntry[] = [ { - clauseId: "7.1-1", + clauseId: "4.7-19", specVersion: "0.1", - requirementIds: ["AS-3", "AS-11"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "**The following field table is normative.** [Section 13](#typescript-types)'s TypeScript types are a non-normative convenience mirror; on conflict, this table wins.", - specAnchor: "#grant-fields", + role: ["resource-server"], + text: "The RS MUST apply this projection to record `data` and evidence.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - caseIds: [ - "AS-3/issued-grant-artifact-matches-section-7-schema", - "AS-3/resolved-grant-matches-observable-schema-fields", - ], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.2-0", + clauseId: "4.7-20", specVersion: "0.1", - requirementIds: ["AS-11"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "Unique non-empty resolved field allowlist, authoritative for RS enforcement. Top-level field names only.", - specAnchor: "#streamgrant-fields", - applicability: "always", + role: ["resource-server"], + text: "For a client-token read, the RS MUST NOT add a field outside the granted field set.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", observable: "black-box-http", partial: true, - caseIds: ["AS-2/dotted-nested-field-refused"], + caseIds: ["RS-2/field-projection-not-exceeded"], gapNote: - "The `fields` row of the normative StreamGrant table; carries no RFC 2119 keyword, so its MUST level is Section 9 AS item 11's. The top-level-only constraint is tested; uniqueness and non-emptiness of the issued allowlist are not.", + "The case reads a field-narrowed grant and checks that no returned record carries a field outside the grant's allowlist. It does not exercise a request-time `fields` selection or an `expand`ed relation.", }, { - clauseId: "7.2-1", + clauseId: "4.7-21", specVersion: "0.1", - requirementIds: ["AS-15", "AS-4"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "Before the final approval surface is shown, the AS MUST resolve omitted `instance_ids` to exact eligible instance handles or require an explicit owner choice.", - specAnchor: "#streamgrant-fields", + role: ["resource-server"], + text: "The RS MUST NOT fabricate its value.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - caseIds: ["AS-4/omitted-fields-expanded-before-issuance"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.2-2", + clauseId: "4.7-22", specVersion: "0.1", - requirementIds: ["AS-15"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The final approval artifact MUST include the exact resolved `instance_ids`, stream names, fields, resources, temporal field, `since`, `until`, purpose, retention, client identity, and grant expiry.", - specAnchor: "#streamgrant-fields", + role: ["resource-server"], + text: "The RS MUST continue to determine client-token eligibility using the grant-authorized projection defined in [Incremental sync](#incremental-sync).", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - caseIds: ["AS-15/final-approval-artifact-carries-resolved-terms"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.2-3", + clauseId: "4.7-23", specVersion: "0.1", - requirementIds: ["AS-15"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "The approval mutation MUST bind to an immutable review revision or digest over the authorization decision fields.", - specAnchor: "#streamgrant-fields", + role: ["resource-server"], + text: "The RS MUST preserve the grant and request-field bindings of page cursors and sync tokens.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - caseIds: ["AS-15/approval-requires-the-issued-revision"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.2-4", + clauseId: "4.7-24", specVersion: "0.1", - requirementIds: ["AS-3", "AS-15"], + requirementIds: [], level: "must", - role: ["authorization-server", "resource-server"], - text: "`client_claims` MUST remain outside the resolved grant and RS enforcement.", - specAnchor: "#streamgrant-fields", - applicability: "the selection request carries `client_claims`", + role: ["resource-server"], + text: "No widening.** Evidence MUST NOT reveal anything outside the effective projection.", + specAnchor: "#record-evidence", + applicability: "always", observable: "black-box-http", - caseIds: ["AS-3/client-claims-stay-outside-the-resolved-grant"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.2-5", + clauseId: "4.7-25", specVersion: "0.1", - requirementIds: ["AS-15"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "If instance eligibility or the reviewed revision becomes stale before approval, the AS MUST reject approval and require a new review.", - specAnchor: "#streamgrant-fields", + role: ["resource-server"], + text: "It MUST NOT reveal a value, field name, or subject identifier other than one already disclosed to this caller.", + specAnchor: "#record-evidence", applicability: "always", observable: "black-box-http", - partial: true, - caseIds: ["AS-15/approval-requires-the-issued-revision"], - gapNote: - "The case rejects approval of a revision the AS never issued. It never makes a previously valid revision stale, nor changes instance eligibility after review, so the stale-after-change obligation is not exercised. Missing case: approve a revision after eligibility changes.", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.2-6", + clauseId: "4.7-26", specVersion: "0.1", - requirementIds: ["AS-7", "AS-15"], + requirementIds: [], level: "must", - role: ["authorization-server"], - text: "If `client_claims` are rendered during final review, the final approval artifact and review revision MUST also bind the normalized exact claims with client attribution.", - specAnchor: "#streamgrant-fields", - applicability: "`client_claims` are rendered on the final review surface", + role: ["resource-server"], + text: "It MUST NOT reveal any fact about hidden data, such as whether a hidden value changed or how many hidden fields or records exist.", + specAnchor: "#record-evidence", + applicability: "always", observable: "black-box-http", - caseIds: ["AS-15/rendered-client-claims-bound-with-attribution"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.4-1", + clauseId: "4.7-27", specVersion: "0.1", - requirementIds: [], + requirementIds: ["RS-28"], + level: "may", + role: ["resource-server"], + text: "A resource server MAY decide per record whether to deliver an item only when that decision is independent of hidden data.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.7-28", + specVersion: "0.1", + requirementIds: ["RS-28"], level: "must", - role: ["authorization-server", "resource-server", "client"], - text: "Three independent version axes exist in PDPP. They MUST NOT be conflated:", - specAnchor: "#version-layering", + role: ["resource-server"], + text: "Delivery.** A resource server MUST NOT deliver an evidence item unless all of these conditions are true: [...] The resource server implements the item's type, including the verification and disclosure rules of its profile. [...] The resource server has checked that the proof is valid over the served values of the covered fields, in the canonical form that the profile defines. It has also checked that the proof authenticates the item's `type`, the `attester` identity and role, and each `covers` pointer together with the served value it names. A profile's verification procedure MUST authenticate all of these, and clients MUST check them. [...] The item's record binding verifies for this caller, directly or through a composed chain. [...] The resource server has checked that the item satisfies the no-widening rule for this response.", + specAnchor: "#record-evidence", applicability: "always", observable: "review-only", caseIds: [], gapNote: - "A design constraint on implementations rather than a checkable wire behaviour; its observable consequences are the three axis rows, recorded separately as 7.4-2, AS-17, and RS-11.", + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "7.4-2", + clauseId: "4.7-29", specVersion: "0.1", - requirementIds: ["RS-11"], + requirementIds: [], level: "must", role: ["resource-server"], - text: "RS MUST reject grants with unsupported major versions, returning 400 `unsupported_version`.", - specAnchor: "#version-layering", - applicability: "always", + text: "A resource server MUST NOT pass through an item that it cannot check, and MUST NOT mark an item as verified or unverified.", + specAnchor: "#record-evidence", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["RS-11/unsupported-grant-schema-version-rejected"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "7.4-3", + clauseId: "4.7-30", specVersion: "0.1", - requirementIds: ["AS-21"], + requirementIds: [], level: "must", - role: ["authorization-server", "resource-server"], - text: "The current persisted-authorization-state reader MUST reject any persisted authorization state whose version or shape it cannot validate against a supported contract before its caller continues introspection or route handling. The reader MUST NOT reconstruct missing authorization or binding facts from current configuration. A deployment that cannot support or explicitly migrate such state MUST require fresh consent.", - specAnchor: "#version-layering", + role: ["resource-server"], + text: "Profiles.** An evidence profile MUST define: [...] the canonical form that its proof covers; [...] how it meets the no-widening rule across reads, for example by reusing an item while the canonical covered values and the binding context are unchanged, and by replacing or omitting the item when validity, expiry, or revocation requires it; [...] any structure shared across records, such as a signed tree head, and how that structure meets the no-widening rule; [...] how it prevents clients from linking the same subject or record beyond what the served data already reveals; [...] whether and how an item discloses raw inputs of a derived field without widening the disclosure; [...] whether it can cover a `blob_ref` or `resource_ref` field, which the resource server rewrites at read time; [...] its holder binding and validity semantics, such as expiry and revocation; [...] whether it supports owner reads.", + specAnchor: "#record-evidence", applicability: "always", observable: "review-only", caseIds: [], gapNote: - "An ordering obligation over internal persisted state: the suite would need to write a legacy-shaped record into the target's store, which the adapter deliberately cannot do, and even then could not observe that the refusal happened *before* route handling.", + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "7.5-1", + clauseId: "4.7-31", specVersion: "0.1", - requirementIds: ["AS-10"], + requirementIds: ["CL-16"], level: "must", - role: ["authorization-server"], - text: "The AS MUST reject subsequent attempts to issue new client access tokens against the same consumed grant.", - specAnchor: "#access-modes", - applicability: "the AS supports `single_use` grants", - observable: "black-box-http", - caseIds: ["AS-10/single-use-grant-consumed"], - }, - { - clauseId: "7.5-2", - specVersion: "0.1", - requirementIds: ["AS-10"], - level: "may", role: ["client"], - text: "The client MAY retry or resume pagination using the same access token.", - specAnchor: "#access-modes", - applicability: "the AS supports `single_use` grants", - observable: "black-box-http", + text: "Unknown types and roles.** A client MUST ignore an item whose `type` it does not implement or whose `attester.role` it does not recognize.", + specAnchor: "#record-evidence", + applicability: "always", + observable: "review-only", caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "7.8-1", + clauseId: "4.8-1", specVersion: "0.1", requirementIds: [], level: "should", + role: ["source-declaration"], + text: "Connector authors SHOULD use these names when the platform provides them, rather than inventing platform-specific names.", + specAnchor: "#timestamps", + applicability: "always", + observable: "declaration-static", + caseIds: [], + }, + { + clauseId: "4.10-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "`mime_type` MUST be a valid IANA media type (see [IANA Media Types](https://www.iana.org/assignments/media-types/)).", + specAnchor: "#binary-data-blobref", + applicability: "a served record carries a `blob_ref`", + observable: "black-box-http", + partial: true, + caseIds: ["RS-1/blob-ref-mime-type-valid"], + gapNote: + "The value lives in a record's `data.blob_ref`, so the case reads served records for the blob fixture's grant. It judges media-type STRUCTURE only: a registered top-level type or a well-formed `x-` token, a `/`, and a subtype made of RFC 6838 restricted-name characters (parameters are not validated). It does not consult the live IANA registry, so a well-formed but unregistered subtype passes, and it observes only the records that fixture exposes. A `mime_type` that is present but not a string fails; a `blob_ref` with no `mime_type` skips. No Section 9 item covers it, so `requirementIds` stays empty; the case is filed under RS-1 because the blob fixture is its surface.", + }, + { + clauseId: "4.11-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "For a client token, the target MUST pass every constraint of the same grant for the target stream: instance, `resources`, time constraint, and field access to the target's key.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.11-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "For an owner token, the target MUST fall within its subject, source, connection, and operation scope; no client grant is required or synthesized.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.11-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client MUST accept a redacted form wherever a `resource_ref` can appear, and MUST treat it as a reference that it cannot follow.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.11-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST evaluate each reference and its target at each horizon where the referring record is visible.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.11-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "It MUST return the referring record when this projection changes, even if the referring record's stored fields do not.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "4.11-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "If a target is deleted after the first page, later pages MUST redact references to it.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.11-7", + specVersion: "0.1", + requirementIds: ["RS-23"], + level: "must", + role: ["resource-server"], + text: "The RS MUST record each referring record that the session serves with a reference redacted for this reason.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.11-8", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The next session under the same grant and query context MUST return each such record that is visible at its horizon, projected as of that horizon, even if its projection is the same at both horizons.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "4.11-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST retain the target, dependency, and redaction history needed for this evaluation, or return 410 `cursor_expired`.", + specAnchor: "#cross-stream-references-resourceref", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, +]; + +// Clause inventory for Core section 5 (5. Source Declaration). +const SECTION_5: readonly ClauseEntry[] = [ + { + clauseId: "5.3-1", + specVersion: "0.1", + requirementIds: ["AS-16"], + level: "must", role: ["authorization-server"], - text: "Authorization server UIs SHOULD model this flow as revocation followed by a new grant request.", - specAnchor: "#grant-narrowing", + text: "The authorization server MUST treat `publisher.id` as authenticated only where an accepted channel or configured mapping binds that publisher to the declaration; absent that binding it MUST NOT support source acceptance, redirect policy, attribution, or any other trust decision.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "What the AS treated as authenticated is an internal trust decision; from outside, an accepted and a rejected declaration differ only by whether consent proceeded, which does not discriminate this clause.", + }, + { + clauseId: "5.3-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "It is REQUIRED when any display value is a language map.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.3-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "`primary_key` and `cursor_field` MUST reference fields declared here.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "declaration-static", + caseIds: ["AS-16/key-field-not-declared-in-schema-refused"], + gapNote: + "No Section 9 item covers declaration internal consistency, so `requirementIds` stays empty; the case is filed under AS-16 because declaration acceptance is the surface it observes.", + }, + { + clauseId: "5.3-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "For client reads, the AS MUST freeze it as `sort_field` when its schema format is `date` or `date-time` and the grant includes it.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.3-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "MUST reference a top-level field declared in the schema with format `date` or `date-time` (see [consent_time_field](#consent-time-field)).", + specAnchor: "#sourcedeclaration-fields", + applicability: "the stream declares `consent_time_field`", + observable: "declaration-static", + partial: true, + caseIds: ["AS-16/consent-time-field-not-declared-in-schema-refused"], + gapNote: + "The locked text adds that the field's schema format is `date` or `date-time`; the case checks only that the field is declared in the schema. No Section 9 item covers `consent_time_field` declaration validity beyond AS-27, so the case stays filed under AS-16, the surface it observes.", + }, + { + clauseId: "5.3-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a non-boolean value.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.3-7", + specVersion: "0.1", + requirementIds: ["AS-2"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject grants that request `time_range` on a stream without a `consent_time_field`, or that request an unsupported selection parameter.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-2/time-range-without-consent-time-field-refused"], + }, + { + clauseId: "5.3-8", + specVersion: "0.1", + requirementIds: ["AS-16"], + level: "must", + role: ["authorization-server", "source-declaration"], + text: "If `$schema` is present, it MUST equal `https://json-schema.org/draft/2020-12/schema`. This dialect choice does not by itself guarantee identical validator behavior. The AS MUST meta-validate each embedded stream schema before accepting the declaration. Embedded `$ref` and `$dynamicRef` values MUST be local fragment references. A declaration MUST NOT make consent interpretation depend on a mutable remote schema.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "declaration-static", + partial: true, + caseIds: ["AS-16/embedded-schema-remote-reference-refused"], + gapNote: + "Partially covered. The case exercises the local-reference obligation (`$ref`/`$dynamicRef` must be local fragments), which is the half that makes consent interpretation depend on a mutable remote schema. The `$schema` dialect equality and full metaschema meta-validation are implemented by the reference target but not separately asserted: a case for each would need its own defect to discriminate, and the remote-reference oracle already fails a target that skips embedded-schema validation altogether.", + }, + { + clauseId: "5.3-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a declaration whose root `required` array names a field not declared in the root `properties` object.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.3-10", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "A publisher MUST list a top-level field in the root `required` array if the field must be included in every disclosed record.", + specAnchor: "#sourcedeclaration-fields", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "5.4-1", + specVersion: "0.1", + requirementIds: [], + level: "may", + role: ["source-declaration"], + text: "Streams MAY include a `display` object with human-readable metadata for the consent UI.", + specAnchor: "#stream-display", + applicability: "always", + observable: "declaration-static", + caseIds: [], + }, + { + clauseId: "5.4-2", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "should", + role: ["authorization-server"], + text: "If absent, the AS SHOULD display `streams[].description` or fall back to the stream name.", + specAnchor: "#stream-display", + applicability: "the declaration omits `display.label`", + observable: "review-only", + caseIds: [], + gapNote: + "Consent-surface rendering is not observable over HTTP; a case could only assert on a deployment's HTML, which tests the deployment rather than the protocol.", + }, + { + clauseId: "5.4-3", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "may", + role: ["authorization-server"], + text: "If absent, the AS MAY generate a description from the stream schema, or display no detail.", + specAnchor: "#stream-display", + applicability: "the declaration omits `display.detail`", + observable: "review-only", + caseIds: [], + }, + { + clauseId: "5.4-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "Each key MUST name a top-level field declared in `streams[].schema`, and the AS MUST reject a declaration with any other key.", + specAnchor: "#stream-display", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.4-5", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["client"], + text: "The requesting client MUST NOT override or supplement these descriptions in the selection request.", + specAnchor: "#stream-display", + applicability: "always", + observable: "client-capture", + caseIds: ["AS-7/client-does-not-override-display-descriptions"], + }, + { + clauseId: "5.5-1", + specVersion: "0.1", + requirementIds: ["AS-22"], + level: "must", + role: ["source-declaration"], + text: "Each key of a language map MUST be a well-formed BCP 47 language tag.", + specAnchor: "#display-text-languages", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.5-2", + specVersion: "0.1", + requirementIds: ["AS-22"], + level: "must", + role: ["source-declaration"], + text: "Keys MUST be unique when compared without regard to case.", + specAnchor: "#display-text-languages", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.5-3", + specVersion: "0.1", + requirementIds: ["AS-22"], + level: "must", + role: ["source-declaration"], + text: "Each value MUST be a non-empty string, and a map MUST have at least one entry.", + specAnchor: "#display-text-languages", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.5-4", + specVersion: "0.1", + requirementIds: ["AS-22"], + level: "must", + role: ["source-declaration"], + text: "The publisher MUST make every version of one value state the same meaning: a language map carries translations, not different descriptions.", + specAnchor: "#display-text-languages", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "5.5-5", + specVersion: "0.1", + requirementIds: ["AS-22"], + level: "must", + role: ["authorization-server"], + text: "Otherwise the declaration does not state its language, and the AS MUST NOT assume one.", + specAnchor: "#display-text-languages", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.5-6", + specVersion: "0.1", + requirementIds: ["AS-22"], + level: "must", + role: ["source-declaration"], + text: "When any display value is a language map, `display.default_language` MUST be present, and every language map MUST have a key that matches it without regard to case.", + specAnchor: "#display-text-languages", + applicability: "the condition the clause states holds", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.5-7", + specVersion: "0.1", + requirementIds: ["AS-22"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a declaration that breaks a structural or language-tag rule in this section.", + specAnchor: "#display-text-languages", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-1", + specVersion: "0.1", + requirementIds: ["RS-33"], + level: "must", + role: ["source-declaration"], + text: "The `consent_time_field` may be the same field as `cursor_field`, but they serve different purposes and MUST be declared separately: [...] `cursor_field` selects ordinary list order; a client grant uses it only when frozen as `sort_field`. `changes_since` uses a separate RS change-order position. [...] `consent_time_field` governs consent-time filtering (which records fall within the authorized time window).", + specAnchor: "#consent-time-field", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: + "No Section 9 item covers it, so `requirementIds` stays empty. Core explicitly permits streams that cannot define a stable `consent_time_field` to omit it, so the former negative fixture (`time_range_capable` with no consent field) did not violate this clause. A valid black-box fixture would need to observe the AS deriving a consent boundary from `cursor_field`; the declaration submission surface exposes neither that derived state nor a normative capability member.", + }, + { + clauseId: "5.6-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "A `consent_time_field` MUST be a top-level field whose schema declares format `date` (RFC 3339 `full-date`) or `date-time` (RFC 3339 `date-time`, which always has an offset).", + specAnchor: "#consent-time-field", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a declaration that names an ineligible `consent_time_field`; a publisher may omit the field to offer the stream without time-range selection.", + specAnchor: "#consent-time-field", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "5.6-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "A stream whose records can do this MUST declare `consent_time_content_may_exceed: true`.", + specAnchor: "#consent-time-field", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-5", + specVersion: "0.1", + requirementIds: ["AS-28"], + level: "must", + role: ["authorization-server"], + text: "For a time-bounded grant on such a stream, the AS MUST tell the owner that matched records can contain data from outside the bounds, and say what the matched value is.", + specAnchor: "#consent-time-field", + applicability: "the condition the clause states holds", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT present such a grant as limited to data inside the bounds.", + specAnchor: "#consent-time-field", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-7", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: "The `consent_time_field` MUST be rendered in human-readable consent UX, with the declared meaning of the field.", + specAnchor: "#consent-time-field", + applicability: "the grant carries a `time_constraint`", + observable: "review-only", + caseIds: [], + gapNote: + "A rendering obligation on the consent surface, which black-box HTTP cannot observe; the suite can see what the AS bound, not what it showed.", + }, + { + clauseId: "5.6-8", + specVersion: "0.1", + requirementIds: ["AS-28"], + level: "may", + role: ["authorization-server"], + text: 'The AS MAY show a relative label, such as "last 90 days", only if the AS has verified that the label is true for that stream\'s exact bounds.', + specAnchor: "#consent-time-field", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: 'For a `date` field, the consent text MUST state whose day the value is, as the field\'s declared meaning describes it: for example "the day Oura assigns, in your local time" or "the budget date you entered in YNAB".', + specAnchor: "#consent-time-field", + applicability: "the condition the clause states holds", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-10", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "A `continuous` grant with `since` and no `until` MUST be described as covering records since that bound, including new records as they arrive.", + specAnchor: "#consent-time-field", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-11", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["source-declaration"], + text: "It MUST NOT be described as a rolling window.", + specAnchor: "#consent-time-field", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-12", + specVersion: "0.1", + requirementIds: ["AS-28"], + level: "must", + role: ["authorization-server"], + text: "For a `continuous` grant, the AS MUST tell the owner that the client can see records leave the window when their time values change (see [Time constraint semantics](#time-constraint-semantics)).", + specAnchor: "#consent-time-field", + applicability: "the condition the clause states holds", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.6-13", + specVersion: "0.1", + requirementIds: [], + level: "should", + role: ["authorization-server"], + text: "If the field's schema allows null, the AS SHOULD tell the owner that records without a value are not included.", + specAnchor: "#consent-time-field", + applicability: "the condition the clause states holds", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.8-1", + specVersion: "0.1", + requirementIds: [], + level: "may", + role: ["source-declaration"], + text: "Declaration publishers MAY suggest views.", + specAnchor: "#views", + applicability: "always", + observable: "declaration-static", + caseIds: [], + }, + { + clauseId: "5.8-2", + specVersion: "0.1", + requirementIds: ["AS-12"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT define a view that includes fields absent from the retained SourceDeclaration schema for the relevant stream.", + specAnchor: "#views", + applicability: "the AS supports views", + observable: "black-box-http", + caseIds: ["AS-12/view-within-declared-schema"], + }, + { + clauseId: "5.8-3", + specVersion: "0.1", + requirementIds: ["AS-13"], + level: "must", + role: ["authorization-server"], + text: "Grants are bound to the resolved field set at issuance time: `fields` in the `StreamGrant` is authoritative, not the view name. View evolution (adding new fields to a view) never silently widens existing grants. Re-consent is required before a client can access new fields, even if those fields are subsequently added to a named view the client already has a grant for.", + specAnchor: "#views", + applicability: "the AS supports views", + observable: "black-box-http", + partial: true, + caseIds: ["AS-13/view-evolution-does-not-widen-an-issued-grant"], + gapNote: + "Recorded as MUST-equivalent because Section 9 AS item 13 states it as one; the spec's own sentence here carries no RFC 2119 keyword, so the binding level is the conformance list's rather than this subsection's. The covering case exercises the field-widening half against a target whose view evolves after issuance; the re-consent half binds a consent flow the suite cannot drive.", + }, + { + clauseId: "5.8-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server", "resource-server", "client"], + text: "Implementations MUST treat unrecognized view URIs as opaque identifiers.", + specAnchor: "#views", + applicability: "the AS supports views", + observable: "black-box-http", + caseIds: ["AS-13/unrecognized-view-uri-treated-as-opaque"], + }, + { + clauseId: "5.10-1", + specVersion: "0.1", + requirementIds: ["AS-16"], + level: "must", + role: ["client"], + text: "A client MUST NOT introduce a new source authority or declaration URI during authorization.", + specAnchor: "#declaration-acceptance", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: + "The former case submitted a non-normative top-level `authority` member to an operator declaration-onboarding hook. Core defines the accepted authority as AS-side acceptance metadata and gives `SourceDeclaration` no such document member; this surface cannot observe a client introducing authority during authorization. A client-authorization fixture with an overridable source identifier is required.", + }, + { + clauseId: "5.10-2", + specVersion: "0.1", + requirementIds: ["AS-16"], + level: "must", + role: ["authorization-server"], + text: "For a `provider_native` source, `source.id` MUST be identical to the protected-resource identifier the authorization server has already accepted for that resource. The authorization server MUST reject any mismatch before consent or grant issuance.", + specAnchor: "#declaration-acceptance", + applicability: "the source kind is `provider_native`", + observable: "black-box-http", + caseIds: ["AS-16/provider-native-source-id-mismatch-refused"], + }, + { + clauseId: "5.10-3", + specVersion: "0.1", + requirementIds: ["AS-16"], + level: "must", + role: ["authorization-server"], + text: "Without such a binding, the authorization server MUST NOT rely on `publisher.id` for source acceptance, attribution, redirect policy, or any other trust decision.", + specAnchor: "#declaration-acceptance", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "Restates 5.3-1 as an acceptance obligation; what the AS relied on internally is not observable from a request outcome.", + }, + { + clauseId: "5.10-4", + specVersion: "0.1", + requirementIds: ["AS-16"], + level: "must", + role: ["authorization-server"], + text: "Different parsed content under an accepted key is equivocation: the authorization server MUST reject it and retain the previously accepted content, and MUST NOT infer ordering or freshness from `declaration_version`.", + specAnchor: "#declaration-acceptance", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-16/declaration-equivocation-refused-and-prior-content-retained"], + }, + { + clauseId: "5.10-5", + specVersion: "0.1", + requirementIds: ["AS-16", "AS-48"], + level: "must", + role: ["authorization-server"], + text: "When retrieving a declaration, the authorization server MUST use HTTPS without ambient credentials. It MUST validate every redirect target and the final URL against its accepted declaration pointer and network policy. It MUST reject a declaration that requires automatic retrieval of a remote schema, and MUST fail closed when any check fails.", + specAnchor: "#declaration-acceptance", + applicability: "the AS retrieves declarations over the network", + observable: "black-box-http", + caseIds: [], + gapNote: + "Missing fixture: an inbound HTTP endpoint serving a candidate declaration that logs each request the AS makes (credentials sent, redirect targets followed, remote schema retrieval). The harness already hosts one for client identity documents (src/tests/client-identity.ts serveDocument) but has no hook to offer an AS a declaration pointer to retrieve. The HTTPS requirement needs a TLS fixture, and the connection-bound address check is internal to the AS, so those parts stay unobservable even with the fixture.", + }, + { + clauseId: "5.10-6", + specVersion: "0.1", + requirementIds: ["AS-16", "AS-48"], + level: "must", + role: ["authorization-server"], + text: "The authorization server MUST validate the destination address against its network policy immediately before each connection attempt, including each redirect hop. It MUST connect only to an address from that validated result.", + specAnchor: "#declaration-acceptance", + applicability: "the AS retrieves declarations over the network", + observable: "review-only", + caseIds: [], + gapNote: + "A DNS-rebinding defence stated as a connection-time ordering obligation; distinguishing it from a URL-time check needs control of the AS's resolver, not an HTTP client.", + }, + { + clauseId: "5.10-7", + specVersion: "0.1", + requirementIds: ["AS-7", "AS-16", "AS-49"], + level: "must", + role: ["authorization-server"], + text: "An authorization server MUST render declaration display values safely for the output context, by context-appropriate escaping, by sanitization, or by any construction that guarantees the value cannot be interpreted as markup, script, or a control sequence in that context.", + specAnchor: "#declaration-acceptance", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation is on rendered output in an unspecified output context; asserting on it means asserting on a deployment's HTML, which is outside what this suite judges.", + }, + { + clauseId: "5.10-8", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST render them safely under the same requirement.", + specAnchor: "#declaration-acceptance", + applicability: "always", + observable: "declaration-static", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "5.10-9", + specVersion: "0.1", + requirementIds: ["AS-16", "RS-15"], + level: "must", + role: ["authorization-server", "resource-server"], + text: "Current declaration capabilities MUST NOT widen an issued grant.", + specAnchor: "#declaration-acceptance", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-15/client-metadata-projection-closed"], + }, +]; + +// Clause inventory for Core section 6 (6. Selection Request). +const SECTION_6: readonly ClauseEntry[] = [ + { + clauseId: "6.0-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "As RFC 9396 Section 5 requires, the AS MUST reject an authorization request with `invalid_authorization_details` when any object has a type the AS does not support or is invalid under its type's definition.", + specAnchor: "#selection-request", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.1-1", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "may", + role: ["authorization-server"], + text: "The AS MAY replace or augment inline values with locally registered metadata, validated binding metadata, validated software-statement metadata, or trust-registry metadata.", + specAnchor: "#client-display", + applicability: "always", + observable: "review-only", + caseIds: [], + }, + { + clauseId: "6.1-2", + specVersion: "0.1", + requirementIds: ["AS-7", "AS-50"], + level: "must", + role: ["authorization-server"], + text: "a conforming authorization server MUST NOT reject a valid client ID metadata document solely because the client is not preregistered.", + specAnchor: "#client-display", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-7/valid-url-hosted-client-identity-accepted", "AS-7/malformed-url-hosted-client-identity-refused"], + }, + { + clauseId: "6.1-3", + specVersion: "0.1", + requirementIds: [], + level: "may", + role: ["authorization-server"], + text: "the server MAY still deny authorization, rate-limit the client, or require a registry-derived trust or admission result, under local policy and for any reason other than the absence of preregistration.", + specAnchor: "#client-display", + applicability: "always", + observable: "black-box-http", + caseIds: [], + }, + { + clauseId: "6.1-4", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: "For PDPP Core v0.1 interoperability, a conforming authorization server MUST accept a valid URL-hosted client identity unless local policy denies authorization.", + specAnchor: "#client-display", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-7/valid-url-hosted-client-identity-accepted", "AS-7/malformed-url-hosted-client-identity-refused"], + }, + { + clauseId: "6.1-5", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST resolve requester identity metadata from the best available source. Source precedence is local registration or trust-registry metadata, then validated software-statement metadata if supported, then validated binding metadata, then inline `client_display`, then `client_id` fallback.", + specAnchor: "#client-display", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "Which source the AS resolved from is visible only on the consent surface it renders, which is not black-box observable.", + }, + { + clauseId: "6.1-6", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: "If the resolved metadata contains a display name, the AS MUST display it to the user during consent. If no display name is available, the AS MUST display `client_id` as the requester identity.", + specAnchor: "#client-display", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "A display obligation on the consent surface; a case can see what the AS bound into the grant, not what it rendered.", + }, + { + clauseId: "6.1-7", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "should", + role: ["authorization-server"], + text: "If the resolved metadata contains `policy_uri` or `tos_uri`, the AS SHOULD present each during consent.", + specAnchor: "#client-display", + applicability: "always", + observable: "review-only", + caseIds: [], + }, + { + clauseId: "6.1-8", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "If it issues a grant, the corresponding field in `client.client_display` MUST contain the exact URI presented as text or as a link target; a URI not presented MUST be omitted from that field.", + specAnchor: "#client-display", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.1-9", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: 'If the server has a positive trust signal for the client (e.g., verified domain control, trust registry membership), it MUST render that status distinctly (e.g., a "verified" badge). If it has no positive trust signal, it MUST treat the client as unverified and SHOULD display an "unverified app" indicator.', + specAnchor: "#client-display", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "Trust-signal rendering is a consent-surface obligation; not observable over the protocol surface this suite drives.", + }, + { + clauseId: "6.1-10", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: 'The AS MAY treat verified domain control as a positive trust signal under obligation 4, and when it does it MUST name the verified domain rather than assert an unqualified verification (for example "Verified domain: example.com", not "Verified app"). [...] the AS MUST NOT present it as one.', + specAnchor: "#client-display", + applicability: "the AS treats verified domain control as a trust signal", + observable: "review-only", + caseIds: [], + gapNote: "Consent-surface wording obligation; not observable without asserting on a deployment's rendered text.", + }, + { + clauseId: "6.1-11", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST treat `logo_uri` as untrusted content until it has been accepted under local policy. It MUST NOT fetch and render a client-supplied remote logo in the consent UI unless the client is verified or the asset has been proxied, cached, and approved under local policy. For unverified clients, the AS SHOULD generate a monogram from the resolved display name.", + specAnchor: "#client-display", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: + "Missing fixture: an inbound HTTP endpoint named as an unverified client's `logo_uri` that logs requests, plus a way to open the consent surface so a fetch triggered by rendering it is seen. The harness hosts a request-logging fixture for client identity documents (src/tests/client-identity.ts serveDocument) but has no consent-render hook. A fetch made by the owner's browser, rather than the AS, would be seen only if the fixture logs it; the verified-client and proxied-asset exceptions need local policy knowledge the suite lacks.", + }, + { + clauseId: "6.1-12", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "should", + role: ["authorization-server"], + text: "If neither resolved metadata nor inline `client_display` provides a display name, the consent UI SHOULD clearly indicate that the client has not provided display metadata.", + specAnchor: "#client-display", + applicability: "the client provides no display name", + observable: "review-only", + caseIds: [], + }, + { + clauseId: "6.2-1", + specVersion: "0.1", + requirementIds: [], + level: "may", + role: ["authorization-server"], + text: "An authorization server MAY support clients that are public and pre-registered by the deployment rather than dynamically registered.", + specAnchor: "#pre-registered-public-clients", + applicability: "always", + observable: "black-box-http", + caseIds: [], + }, + { + clauseId: "6.2-2", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: "the field MUST NOT contain secrets, access tokens, owner-scoped clients, dynamically registered clients, or private registration state.", + specAnchor: "#pre-registered-public-clients", + applicability: "the AS advertises `pre_registered_public` in `pdpp_registration_modes_supported`", + observable: "black-box-http", + caseIds: ["AS-7/pre-registered-public-clients-carry-no-private-state"], + }, + { + clauseId: "6.3-1", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: 'The AS MUST render `client_claims` content separately from protocol-enforced grant terms and MUST attribute it to the client (e.g., "[client name] says:"). The AS MUST NOT render client claims in the same visual register as protocol-enforced grant terms, structured policy declarations, or declaration-authored data descriptions.', + specAnchor: "#client-claims", + applicability: "the selection request carries `client_claims`", + observable: "review-only", + caseIds: [], + gapNote: + "A visual-register obligation on the consent surface. This is the clause AS-7 chiefly summarizes, and it is the clearest example of a MUST that is not black-box observable in principle.", + }, + { + clauseId: "6.3-2", + specVersion: "0.1", + requirementIds: ["AS-15"], + level: "must", + role: ["authorization-server"], + text: "If rendered on the final owner review surface, `client_claims` MUST be normalized and bound exactly, with client attribution, into the immutable final approval artifact and review revision. Retained consent evidence MUST preserve that binding.", + specAnchor: "#client-claims", + applicability: "`client_claims` are rendered on the final review surface", + observable: "black-box-http", + caseIds: ["AS-15/rendered-client-claims-bound-with-attribution"], + }, + { + clauseId: "6.3-3", + specVersion: "0.1", + requirementIds: ["AS-3"], + level: "should", + role: ["authorization-server", "client"], + text: 'Structured grant fields (e.g., `retention.max_duration`, `access_mode`) SHOULD be rendered by the AS as server-generated display text (e.g., "Deleted within 90 days", "Ongoing access until you revoke it"). Clients SHOULD NOT duplicate machine-readable constraints as free-text commitments.', + specAnchor: "#client-claims", + applicability: "always", + observable: "review-only", + caseIds: [], + }, + { + clauseId: "6.4-1", + specVersion: "0.1", + requirementIds: ["AS-7"], + level: "must", + role: ["authorization-server"], + text: "A conformant AS MUST preserve the distinction between protocol-enforced terms, structured policy declarations, declaration-authored data descriptions, and client-authored claims. It MUST NOT flatten these categories into a single undifferentiated consent surface.", + specAnchor: "#semantic-classes-and-consent-surface-rendering", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The normative core of AS-7, and not observable over HTTP: a case can see what the server bound, not what it rendered.", + }, + { + clauseId: "6.5-1", + specVersion: "0.1", + requirementIds: ["AS-34"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT reject a request solely because a requested language is unsupported.", + specAnchor: "#display-language", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.5-2", + specVersion: "0.1", + requirementIds: ["AS-34"], + level: "should", + role: ["authorization-server"], + text: "An AS that supports `ui_locales` SHOULD publish `ui_locales_supported` in its RFC 8414 metadata, listing the languages of its user interface.", + specAnchor: "#display-language", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.5-3", + specVersion: "0.1", + requirementIds: ["AS-34"], + level: "must", + role: ["authorization-server"], + text: "For each language map it renders (see [Display text languages](#display-text-languages)), the AS MUST choose one version: [...] Form a language priority list. A language preference the owner set at the AS comes first, when one exists. The parseable `ui_locales` tags come next, in request order. The list contains nothing else. [...] Apply the RFC 4647 Section 3.4 Lookup scheme to that list over the map's keys. Lookup shortens each tag from the end until a key matches, so `es-MX` matches an `es` key. [...] If no key matches, or the list is empty, use the version whose key matches `display.default_language` without regard to case.", + specAnchor: "#display-language", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.5-4", + specVersion: "0.1", + requirementIds: ["AS-34"], + level: "should", + role: ["authorization-server"], + text: "The AS SHOULD use the same priority list to choose the language of its own consent-surface text.", + specAnchor: "#display-language", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.5-5", + specVersion: "0.1", + requirementIds: ["AS-34"], + level: "should", + role: ["authorization-server"], + text: 'The AS SHOULD isolate the direction of each display value and each client-authored value it renders, for example with `dir="auto"` in HTML, or between U+2068 FIRST STRONG ISOLATE and U+2069 POP DIRECTIONAL ISOLATE in plain text.', + specAnchor: "#display-language", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.6-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Otherwise it MAY use any absolute URI, for example a DPV purpose, and MUST then send a `purpose_description` that the owner can understand.", + specAnchor: "#request-level-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.6-2", + specVersion: "0.1", + requirementIds: ["AS-6"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST support syntactically valid absolute-URI purpose codes, including codes outside Appendix A, subject to the detail-validation requirements below and local policy. The AS MUST reject a detail whose code is outside Appendix A and that lacks a non-empty `purpose_description`, with `invalid_authorization_details` in the OAuth/RAR binding, and MUST display that description. It MUST NOT reject a complete detail solely because its purpose code is unrecognized.", + specAnchor: "#request-level-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: + "AS-6/unregistered-purpose-not-rejected submits a detail with no `purpose_description`. The locked text requires the AS to reject such a detail for a code outside Appendix A, so the case does not probe the MUST NOT, which concerns a complete detail. No case submits a complete detail with an unrecognized purpose code.", + }, + { + clauseId: "6.6-3", + specVersion: "0.1", + requirementIds: ["AS-7", "CL-27"], + level: "must", + role: ["authorization-server", "client"], + text: "Clients SHOULD provide this field for Appendix A codes, and MUST provide a non-empty description that the owner can understand for any other code. When present, the AS MUST display it.", + specAnchor: "#request-level-parameters", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The AS half is a display obligation on the consent surface; the client half is a SHOULD needing client capture. Neither is reachable from this suite's seam.", + }, + { + clauseId: "6.6-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "A detail that carries the AI training permission MUST include it ([AI training consent](#ai-training-consent)).", + specAnchor: "#request-level-parameters", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.6-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Unknown members.** As RFC 9396 Section 5 requires, the AS MUST reject a detail that carries a member that neither Core nor a profile the AS supports defines for that detail type.", + specAnchor: "#request-level-parameters", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "6.6-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT ignore the member.", + specAnchor: "#request-level-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.7-1", + specVersion: "0.1", + requirementIds: ["CL-8"], + level: "must", + role: ["client", "authorization-server"], + text: "A selection request does not carry `source.kind`. The authorization server derives the provenance class from the declaration it accepted for `source.id`, and records it in consent evidence and any issued grant, where a client reads it back through introspection. A client whose policy depends on provenance therefore reads it from the issued grant rather than asserting an expectation in the request; Section 9 states that as a client requirement.", + specAnchor: "#source-kinds", + applicability: "the client has provenance-dependent local policy", + observable: "client-capture", + caseIds: ["CL-8/reads-source-kind-from-grant"], + gapNote: "The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's.", + }, + { + clauseId: "6.7-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "An authorization server MUST reject a declaration containing an unrecognized `source.kind`.", + specAnchor: "#source-kinds", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.7-3", + specVersion: "0.1", + requirementIds: ["RS-17"], + level: "must", + role: ["resource-server"], + text: "A resource server MUST NOT interpret an unrecognized grant `source.kind` as a known provenance class.", + specAnchor: "#source-kinds", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.7-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client that reads an unrecognized `source.kind` in a grant MUST NOT treat it as either known value; it MAY reject the grant as unsupported.", + specAnchor: "#source-kinds", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.7-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Any other profile-defined class MUST be an absolute URI that the profile's publisher controls, so that classes from different profiles do not collide.", + specAnchor: "#source-kinds", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "6.8-1", + specVersion: "0.1", + requirementIds: ["AS-14"], + level: "must", + role: ["authorization-server"], + text: "Closed issuance.** The AS MUST reject a detail whose `processing_permissions` is not an array of absolute URIs, or contains a duplicate, unknown, or unsupported value.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT ignore, substitute, or narrow such a value, and MUST NOT issue an ordinary grant in place of the detail.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-3", + specVersion: "0.1", + requirementIds: ["AS-14"], + level: "must", + role: ["authorization-server"], + text: "An AS MUST NOT advertise or grant a permission unless it implements the permission's complete defining contract.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client MUST NOT request a permission that the AS does not advertise.", + specAnchor: "#processing-permissions", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Registry maintainers MUST NOT replace that reference with a different profile or version.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "6.8-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "A profile revision MUST NOT widen that authority or reduce those protections.", + specAnchor: "#processing-permissions", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "6.8-7", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Publishing a revision MUST NOT retroactively invalidate previously issued processing authority or make conduct permitted by the bound version non-conformant.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.8-8", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Registration test.** A registration MUST identify every result of the permitted operation that survives the [erasure](#erasure) duty.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.8-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Where such a result may retain or reveal an owner's data, the defining profile MUST bound cessation of the operation to at most 1 hour.", + specAnchor: "#processing-permissions", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "6.8-10", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST hold without delivery of any message to the client, and over the whole execution interval of the operation.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-11", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The defining profile MUST also make every issuance of processing authority depend on the continued eligibility of each acquisition copy the authority covers: no effective erasure instruction covers the copy, and the client may still hold and process it.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.8-12", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "To make this check possible, the defining profile MUST make each processing credential name the acquisition grants whose held data it covers, and MUST make the processor refuse an input acquired under any grant the credential does not name.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.8-13", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "For a permission that covers independent-recipient disclosure, the defining profile MUST specify the recipients or recipient classes the owner approves.", + specAnchor: "#processing-permissions", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.8-14", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST specify the data and disclosure operations that approval covers.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-15", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST define the receiving parties' obligations and the evidence of their acceptance.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-16", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST define which effects of withdrawal and erasure reach those parties.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.8-17", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST identify any copy or result that remains usable after those effects.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-18", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST require the AS to show the owner those limits before approval.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.8-19", + specVersion: "0.1", + requirementIds: ["CL-18"], + level: "must", + role: ["client"], + text: "Consent and evidence.** A client MUST request a processing permission in its own detail, separate from any detail for ordinary access, even for the same source and purpose.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.8-20", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The owner MUST select that detail by a distinct affirmative action, even when it is the only detail.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-21", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT present it as selected by default.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-22", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The owner MUST be able to approve every other detail while declining it, and the AS MUST NOT make approval of another detail depend on it.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-23", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST render each permission as a distinct, labeled term, not as part of the purpose.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-24", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT issue its grant without its permissions, and MUST NOT add a permission to a detail that did not request it.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.8-25", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The final approval artifact, review revision, retained consent evidence, issued grant, token result, and introspection result MUST carry the exact permission set.", + specAnchor: "#processing-permissions", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.9-1", + specVersion: "0.1", + requirementIds: ["CL-17"], + level: "must", + role: ["client"], + text: "A client MUST NOT use a PDPP input for AI training unless it has valid processing authority under the permission's defining profile for every contributing PDPP input.", + specAnchor: "#ai-training", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.9-2", + specVersion: "0.1", + requirementIds: ["CL-17"], + level: "must", + role: ["authorization-server"], + text: "For data obtained through PDPP, a grant carrying `https://pdpp.dev/processing/ai_training` MUST cover the originating subject, source, stream, instance, fields, resources, and time constraint.", + specAnchor: "#ai-training", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.9-3", + specVersion: "0.1", + requirementIds: ["CL-17"], + level: "must", + role: ["client"], + text: "The client MUST establish that coverage using authenticated issuer, subject, and client bindings; matching identifier strings alone is insufficient.", + specAnchor: "#ai-training", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.9-4", + specVersion: "0.1", + requirementIds: ["CL-17"], + level: "must", + role: ["client"], + text: "If the client cannot establish coverage and valid authority for every contributing PDPP input, it MUST NOT use the input for AI training.", + specAnchor: "#ai-training", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.9-5", + specVersion: "0.1", + requirementIds: ["CL-17"], + level: "must", + role: ["client"], + text: "Every recipient or subprocessor to which a client passes PDPP inputs MUST observe the same restriction, and the client MUST impose it on them.", + specAnchor: "#ai-training", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.9-6", + specVersion: "0.1", + requirementIds: ["CL-17", "AS-38", "RS-29"], + level: "must", + role: ["resource-server"], + text: "An operator of an AS or RS MUST NOT use owner data that it holds in that role for AI training, except under a training grant issued to it as a client.", + specAnchor: "#ai-training", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.9-7", + specVersion: "0.1", + requirementIds: ["CL-17"], + level: "must", + role: ["authorization-server"], + text: "Every AS MUST treat it as unsupported: it MUST NOT advertise it and MUST reject a request that carries it.", + specAnchor: "#ai-training", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.10-1", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "A detail that carries the AI training permission MUST include a non-empty `purpose_description` that identifies the type of model, its functions, and the service or recipient it serves.", + specAnchor: "#ai-training-consent", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.10-2", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject such a detail without one, with `invalid_authorization_details` in the OAuth/RAR binding.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.10-3", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST label the detail as AI training on the approval surface.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.10-4", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "An AS MUST include `expires_at` in every grant carrying the AI training permission.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.10-5", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "The expiry MUST be later than `issued_at`.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.10-6", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST include that expiry in final owner review and bind it into the approval artifact and retained consent evidence.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.10-7", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "The approval surface for an AI training detail MUST state that learned parameters produced under valid processing authority may be kept and used for the reviewed purpose after the grant ends or the owner requests erasure.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.10-8", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "It MUST also state that stored examples remain subject to erasure and retention duties.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.10-9", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT claim that keeping learned parameters is exempt from applicable law or from an order to restrict, retrain, or destroy the model.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.10-10", + specVersion: "0.1", + requirementIds: ["AS-36"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT claim that the model is anonymous, that withdrawal or erasure removes the owner's data from learned parameters, or that erasing the training data removes all personal information from the model.", + specAnchor: "#ai-training-consent", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.11-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST omit a declined optional stream from the grant and MUST retain a `required` stream or refuse issuance.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "If no stream remains, the AS MUST refuse issuance with `access_denied`.", + specAnchor: "#stream-selection-parameters", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-3", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-5"], + level: "must", + role: ["authorization-server"], + text: "Mutually exclusive with `fields` in a request; both MUST NOT be present simultaneously. The OAuth/RAR binding maps a request with both to RFC 9396 `invalid_authorization_details`.", + specAnchor: "#stream-selection-parameters", + applicability: "the AS supports views", + observable: "black-box-http", + partial: true, + caseIds: ["AS-2/view-and-fields-mutually-exclusive"], + gapNote: + "The case refuses-then-classifies: it requires 400 `invalid_request`, but the locked text maps a request with both to RFC 9396 `invalid_authorization_details`. The refusal half agrees with the clause; the classification half contradicts it, so a conforming AS fails the case.", + }, + { + clauseId: "6.11-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: '`["*"]` requests all permitted fields; `*` MUST then be the only element.', + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a request that has `resources` and more than one `instance_ids` entry for the stream; the OAuth/RAR binding returns RFC 9396 `invalid_authorization_details`.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST resolve each retained stream's schema-required field set from the retained declaration snapshot before final owner review.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.11-7", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST include that set in the resolved `fields` allowlist.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-8", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST record that set as `required_fields` in the issued StreamGrant.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT derive `required_fields` from `minimum.fields`, stream `necessity`, current records, or a later declaration revision.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-10", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST show which fields will remain in every client record projection.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-11", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST bind the exact `required_fields` set into the final approval artifact and review revision.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-12", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST retain that binding in the consent evidence.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.11-13", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The resolved selection MUST contain at least one field for each retained stream.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-14", + specVersion: "0.1", + requirementIds: ["AS-2"], + level: "must", + role: ["authorization-server"], + text: "The authorization server MUST reject selection requests that specify `time_range` on a stream without that field.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-2/time-range-without-consent-time-field-refused"], + }, + { + clauseId: "6.11-15", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Each bound MUST have the type of the stream's `consent_time_field`: a `full-date` for a `date` field, and a `date-time` for a `date-time` field.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-16", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "A `time_range` MUST contain `since`, `until`, or both.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-17", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a `time_range` with no bound, a bound of the wrong type, a bound that is not valid under [Time constraint semantics](#time-constraint-semantics), or a `since` that is not before `until`.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-18", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT convert or round a bound.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-19", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-4"], + level: "must", + role: ["authorization-server"], + text: "A wildcard entry MUST be the only entry in `streams`. Otherwise stream names MUST be unique within the request.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-2/wildcard-beside-named-stream-refused", "AS-2/duplicate-stream-name-refused"], + }, + { + clauseId: "6.11-20", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Each stream request, including each stream in a selection preset, MUST carry either `fields` or `view`.", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.11-21", + specVersion: "0.1", + requirementIds: ["CL-1"], + level: "should", + role: ["client"], + text: "Clients SHOULD request only the data they need (see [Section 11, Data Minimization](#data-minimization)).", + specAnchor: "#stream-selection-parameters", + applicability: "always", + observable: "client-capture", + caseIds: [], + }, + { + clauseId: "6.12-1", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "Before authenticating the owner, the AS MUST NOT make an externally observable authorization-processing result depend on protected owner state.", + specAnchor: "#authorization-privacy", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-2", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "For representative approval, the AS MUST verify the representative's authority before applying owner-dependent resolution or showing subject-specific information, as [Representative approval](#representative-approval) requires.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-3", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST preserve the errors required by the authorization binding for those failures.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-4", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT redirect to an invalid redirect URI.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-5", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST defer a validation or resolution step when its result depends on protected owner state.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-6", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST perform that step within the authenticated owner's review interaction.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-7", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT treat the absence of an eligible owner source or instance as malformed authorization details.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-8", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT return a client-visible declaration-dependent validation error for an unapproved source or stream.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-9", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "An AS that uses public source capabilities for early validation MUST determine those capabilities independently of the owner's private connections and eligibility.", + specAnchor: "#authorization-privacy", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-10", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT derive a public capability response from protected owner state.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-11", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT retrieve or accept a new declaration authority merely because an authorization request names it.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-12", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST treat an otherwise valid stream that has no eligible instance as unapproved.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-13", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST refuse the detail when a required stream cannot be retained.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-14", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST apply the existing required-stream and authorization-minimum rules before issuance.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-15", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT disclose that explanation to the client without authorization for that disclosure.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-16", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST report an authorization refusal caused by source or instance ineligibility or failed representative-authority verification as `access_denied` in the OAuth binding.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-17", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "For each of these causes, the error, `error_description`, parameters, and headers MUST be those of an owner or representative declining.", + specAnchor: "#authorization-privacy", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-18", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT release the refusal to the client before the authenticated person's interaction concludes.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-19", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT treat owner authentication alone as authorization to disclose unapproved source or instance inventory to the client.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-20", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST limit a successful client result to the approved result defined in [Client-visible authorization result](#authorization-result).", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-21", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "When a private validation failure requires owner interaction, the AS MUST NOT release an owner-dependent terminal result to the client before that interaction concludes.", + specAnchor: "#authorization-privacy", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-22", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST apply this boundary at every supported authorization entry point, including pushed authorization requests and noninteractive requests.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-23", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "Client authentication at a pushed authorization endpoint MUST NOT substitute for owner authentication.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-24", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "Acceptance of a pushed request MUST NOT assert that an owner can approve it.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-25", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST use the pushed-request binding's validation and response rules without returning an owner-interaction error at that endpoint.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-26", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "When a noninteractive request cannot proceed without owner authentication or review, the AS MUST use an error permitted by that binding for the missing interaction.", + specAnchor: "#authorization-privacy", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-27", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT choose that error according to source or instance eligibility.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-28", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "An identity hint MUST NOT satisfy it.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-29", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52", "AS-54"], + level: "must", + role: ["authorization-server"], + text: "For requests that differ only in protected owner state, the AS MUST use equivalent pre-authentication status, response structure, redirects, headers, and transaction-state behavior.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-30", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST prevent systematic differences in processing time from revealing that state.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-31", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52", "AS-54"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST apply the same requirement to client-visible eligibility denials after authenticated review.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-32", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST apply this boundary before it resolves owner-specific source or instance eligibility for final owner review (Section 7).", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.12-33", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "A failed or stale eligibility check MUST remain within authenticated review until that interaction concludes.", + specAnchor: "#authorization-privacy", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.12-34", + specVersion: "0.1", + requirementIds: ["AS-2", "AS-27", "AS-52"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT expose the failed instance or the reason for its ineligibility in the client result.", + specAnchor: "#authorization-privacy", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.13-1", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST expand presets, views, and wildcards against the retained declaration before applying owner choices.", + specAnchor: "#request-resolution", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.13-2", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST resolve the request into explicit grant constraints before final approval.", + specAnchor: "#request-resolution", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.13-3", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST preserve the client identity associated with the authorization request.", + specAnchor: "#request-resolution", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-1", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST compute the ceiling before owner narrowing and minimum checks.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-2", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT resolve outside those limits.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-3", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-51"], + level: "must", + role: ["authorization-server"], + text: "The schema-required fields here are the set that the AS freezes as the stream's `required_fields`; the AS MUST use that same set for the field ceiling, owner narrowing, and grant issuance.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-4", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT infer a client requirement from a schema-required property.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-5", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST retain each required stream or refuse the authorization.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-6", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST refuse issuance when the owner approves no streams.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-7", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT add another source, stream, instance, field, or resource to compensate for a refused selection.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.14-8", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST show the resolved disclosure and commitments before final approval.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.14-9", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT present optional selections as compulsory.", + specAnchor: "#limits-and-owner-choices", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-1", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST recognize only `fields` and `time_range` inside `minimum`.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-2", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST reject empty minimum objects, unknown minimum members, duplicate field names, and fields outside the stream's field ceiling.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-3", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "A supplied `minimum.fields` MUST be a non-empty array of declared top-level field names.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-4", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "For a retained stream, the AS MUST include every `minimum.fields` member in the resolved field selection.", + specAnchor: "#explicit-authorization-minima", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-5", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT treat a schema's `required` array as `minimum.fields`.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-6", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "A supplied `minimum.time_range` MUST contain both `since` and `until`.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-7", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "Each bound MUST have the type of the stream's retained `consent_time_field` and MUST be valid under [Time constraint semantics](#time-constraint-semantics).", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-8", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "`since` MUST be before `until`.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-9", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a minimum window outside the requested time window or on a stream without `consent_time_field`.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-10", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "It MUST resolve a retained stream to a time window containing the entire minimum window.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-11", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-53"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST refuse issuance if an owner-approved required stream cannot meet its minimum.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-12", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The OAuth binding MUST report this refusal as `access_denied`.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-13", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "Error descriptions MUST NOT reveal unapproved source, instance, record, or field existence.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-14", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT require records to exist to satisfy an authorization minimum.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-15", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["client"], + text: "A client MUST evaluate application sufficiency separately.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-16", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST distinguish validation of a minimum's syntax and declared capability from satisfaction of that minimum by the owner's approved selection.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-17", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST apply [Authorization privacy boundary](#authorization-privacy) to each check.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.15-18", + specVersion: "0.1", + requirementIds: ["AS-30"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT test record presence, collection status, or data completeness to decide whether the minimum is satisfied.", + specAnchor: "#explicit-authorization-minima", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-1", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT interpret narrower access as recipient acceptance of a new purpose or retention obligation.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-2", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45", "AS-31"], + level: "must", + role: ["authorization-server"], + text: "It MUST resolve purpose and retention from the current request or applicable recipient-authorized standing terms before owner approval.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-3", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "It MUST retain the evidence identifying those terms and their version with the consent evidence.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.16-4", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "An AS MUST NOT treat a capability advertisement or owner-authored condition alone as recipient acceptance.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-5", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "When an owner condition is not covered by recipient authority, the AS MUST refuse issuance or obtain recipient acceptance before presenting the revised final approval.", + specAnchor: "#recipient-commitments", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-6", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "An AS MUST NOT claim that an unrepresented owner condition is a Core grant constraint.", + specAnchor: "#recipient-commitments", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-7", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "A companion that adds recipient obligations MUST define their result representation and acceptance evidence.", + specAnchor: "#recipient-commitments", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "6.16-8", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT change `access_mode` merely as a consequence of narrowing data.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-9", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST include the resolved purpose in final owner review.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.16-10", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "It MUST include that purpose in the grant.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-11", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "If retention is specified, the AS MUST include it in final owner review.", + specAnchor: "#recipient-commitments", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.16-12", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "It MUST include that retention in the grant.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-13", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "It MUST include that expiry in the grant.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-14", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST include any processing permissions in final owner review and in the grant, as [Processing permissions](#processing-permissions) defines.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.16-15", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45", "AS-56"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST state during final owner review that ordinary access does not authorize the client to disclose PDPP data to an independent recipient.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.16-16", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST distinguish that restriction from processing by parties acting for the client under [Downstream holders](#downstream-holders).", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "6.16-17", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45", "AS-56"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT describe this client duty as technical prevention of onward disclosure.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-18", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "Final owner review MUST also show what happens to held data after the grant ends, as [Held data after a grant ends](#held-data) defines: [...] for a grant without a processing permission, that the client may keep using the data it received for the approved purpose, unless the owner asks for deletion; [...] for a grant that carries a processing permission, that the client may keep that data but has no ordinary surviving use of it. Continued processing is limited to already-issued processing authority within the defining profile's cessation bound and any earlier applicable deadline; [...] the retention limit, or, when the grant has no `retention`, that there is no stated limit and the client may keep the data until it receives a deletion request; [...] that the owner can ask for deletion at any time while the client may hold the data; [...] that the client learns of a deletion request made at the AS only when it next obtains an answer from the AS. The client attempts one at least once a day while it runs, retrying hourly after a failed attempt and at least daily during a long outage ([Positive assessment and reconciliation](#reconciliation)); [...] that an attempt is not an answer, and that there is no maximum delivery delay; [...] that a client that is not running does not check, and learns of a deletion request only when it next runs and checks; [...] that, if the AS stops answering for good, the client of a grant without a processing permission may keep and use the data for the approved purpose indefinitely when the grant states no retention limit; [...] that, for a grant that carries a processing permission, the AS's silence lets the client keep the data but never extends processing authority or permits ordinary use; [...] that a deletion covers all data the client received under the grant, its derivatives, and copies held by processors and in backups, subject to the exclusions and legal-retention exception in [Erasure](#erasure); [...] that the client's stated retention limit, if any, bounds this, and that the owner can decline a grant that states none; [...] that, if the client offers deletion, choosing it inside the client binds the client at once as an instruction, but is not instant physical deletion.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "6.16-19", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST complete required-stream and minimum checks before issuing a grant or a client token.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.16-20", + specVersion: "0.1", + requirementIds: ["AS-30", "AS-45"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT issue a temporary overbroad grant while choices are unresolved.", + specAnchor: "#recipient-commitments", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "6.17-1", + specVersion: "0.1", + requirementIds: ["AS-2"], + level: "must", + role: ["authorization-server"], + text: "Each selection preset MUST NOT contain the same stream name more than once.", + specAnchor: "#selection-presets", + applicability: "the AS supports selection presets", + observable: "black-box-http", + caseIds: ["AS-16/duplicate-stream-in-a-selection-preset-refused"], + }, +]; + +// Clause inventory for Core section 7 (7. Grant). +const SECTION_7: readonly ClauseEntry[] = [ + { + clauseId: "7.0-1", + specVersion: "0.1", + requirementIds: ["AS-24"], + level: "must", + role: ["authorization-server"], + text: "Without such a credential, the AS MUST accept at most one `https://pdpp.dev/data-access` element in an authorization request.", + specAnchor: "#grant", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.0-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST reject a request with more than one with `invalid_authorization_details`.", + specAnchor: "#grant", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.0-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "A companion binding that expands one requested detail into several resolved details MUST define them and MUST correlate every requested detail with its outcome.", + specAnchor: "#grant", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.1-1", + specVersion: "0.1", + requirementIds: ["AS-3", "AS-11"], + level: "must", + role: ["authorization-server"], + text: "**The following field table is normative.** [Section 13](#typescript-types)'s TypeScript types are a non-normative convenience mirror; on conflict, this table wins.", + specAnchor: "#grant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: [ + "AS-3/issued-grant-artifact-matches-section-7-schema", + "AS-3/resolved-grant-matches-observable-schema-fields", + ], + gapNote: "The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's.", + }, + { + clauseId: "7.2-1", + specVersion: "0.1", + requirementIds: ["AS-11"], + level: "must", + role: ["authorization-server"], + text: "Unique non-empty resolved field allowlist, authoritative for RS enforcement. Includes every `required_fields` entry. Top-level field names only.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + partial: true, + caseIds: ["AS-2/dotted-nested-field-refused"], + gapNote: + "The `fields` row of the normative StreamGrant table; carries no RFC 2119 keyword, so its MUST level is Section 9 AS item 11's. The top-level-only constraint is tested; uniqueness, non-emptiness, and inclusion of every `required_fields` entry in the issued allowlist are not.", + }, + { + clauseId: "7.2-2", + specVersion: "0.1", + requirementIds: ["AS-51"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST include `required_fields` in every StreamGrant, including when its value is an empty array.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.2-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST freeze this set at issuance.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.2-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT change the set when it refreshes a token or returns a later authorization result.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.2-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST return the same `required_fields` set in the complete grant delivered to the client and to the authenticated RS.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.2-6", + specVersion: "0.1", + requirementIds: ["AS-15", "AS-4"], + level: "must", + role: ["authorization-server"], + text: "Before the final approval surface is shown, the AS MUST resolve omitted `instance_ids` to exact eligible instance handles or require an explicit owner choice.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-4/omitted-fields-expanded-before-issuance"], + }, + { + clauseId: "7.2-7", + specVersion: "0.1", + requirementIds: ["AS-15"], + level: "must", + role: ["authorization-server"], + text: "The final approval artifact MUST include the exact resolved `instance_ids`, stream names, fields, `required_fields`, resources, temporal field, `since` stated as inclusive, `until` stated as exclusive, purpose, retention, processing permissions, client identity, and grant expiry.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + partial: true, + caseIds: ["AS-15/final-approval-artifact-carries-resolved-terms"], + gapNote: + "The case checks the instance ids, streams, fields, resources, temporal field, since/until, purpose, retention, client identity and grant expiry. The locked text also requires `required_fields`, the inclusive/exclusive statement of the bounds, and processing permissions, which the case does not check.", + }, + { + clauseId: "7.2-8", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "For purpose, it MUST include the exact `purpose_code` and any `purpose_description`.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.2-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The client identity it includes MUST cover the client display metadata actually rendered on the final review surface.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.2-10", + specVersion: "0.1", + requirementIds: ["AS-7", "AS-15"], + level: "must", + role: ["authorization-server"], + text: "If `client_claims` are rendered during final review, the final approval artifact and review revision MUST also bind the normalized exact claims with client attribution.", + specAnchor: "#streamgrant-fields", + applicability: "`client_claims` are rendered on the final review surface", + observable: "black-box-http", + caseIds: ["AS-15/rendered-client-claims-bound-with-attribution"], + }, + { + clauseId: "7.2-11", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "For each declaration display value that final review renders, the final approval artifact and review revision MUST also bind which value it is and the exact string shown.", + specAnchor: "#streamgrant-fields", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.2-12", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "For a value chosen from a language map, they MUST bind the chosen language tag, not the whole map (see [Display language](#display-language)).", + specAnchor: "#streamgrant-fields", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.2-13", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Declaration display text shown on an earlier consent step that affected an owner choice MUST be recorded in the same way in retained consent evidence, or shown again on final review and bound there.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.2-14", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Retained consent evidence MUST preserve these bindings.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.2-15", + specVersion: "0.1", + requirementIds: ["AS-15"], + level: "must", + role: ["authorization-server"], + text: "The approval mutation MUST bind to an immutable review revision or digest over the authorization decision fields and the bound display text.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + caseIds: ["AS-15/approval-requires-the-issued-revision"], + }, + { + clauseId: "7.2-16", + specVersion: "0.1", + requirementIds: ["AS-3", "AS-15"], + level: "must", + role: ["authorization-server", "resource-server"], + text: "`client_claims` and bound display text MUST remain outside the resolved grant and RS enforcement.", + specAnchor: "#streamgrant-fields", + applicability: "the selection request carries `client_claims`", + observable: "black-box-http", + caseIds: ["AS-3/client-claims-stay-outside-the-resolved-grant"], + }, + { + clauseId: "7.2-17", + specVersion: "0.1", + requirementIds: ["AS-15"], + level: "must", + role: ["authorization-server"], + text: "If instance eligibility or the reviewed revision becomes stale before approval, the AS MUST reject approval and require a new review.", + specAnchor: "#streamgrant-fields", + applicability: "always", + observable: "black-box-http", + partial: true, + caseIds: ["AS-15/approval-requires-the-issued-revision"], + gapNote: + "The case rejects approval of a revision the AS never issued. It never makes a previously valid revision stale, nor changes instance eligibility after review, so the stale-after-change obligation is not exercised. Missing case: approve a revision after eligibility changes.", + }, + { + clauseId: "7.3-1", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST return one `authorization_details` element per covered grant in every successful OAuth token response that issues a client access token, including refresh responses.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + partial: true, + caseIds: ["AS-9/token-response-carries-granted-authorization-details"], + gapNote: + "The case checks that a token response carries a non-empty `authorization_details` array and, where introspection is available, that it agrees with introspection on the granted streams and fields. It does not count one element per covered grant, does not check the `{ type, grant }` element shape, and reads `streams` at the top level of each element where the locked text nests them under `grant`, so against a conforming response its comparison reduces to an empty-against-empty match.", + }, + { + clauseId: "7.3-2", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "Each element MUST contain `type` equal to `https://pdpp.dev/data-access` and `grant` containing the complete immutable grant defined by this section.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-3", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT substitute the original selection request or a lossy summary.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-4", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "The result MUST include source provenance, declaration revision, resolved streams, instance handles, fields, the frozen required-field subsets, resource and time constraints, recipient commitments, access mode, and processing permissions and grant expiry when present.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-5", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "Omitted optional streams are not grants and MUST NOT appear as authorized in the result.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-6", + specVersion: "0.1", + requirementIds: ["AS-32", "CL-13"], + level: "must", + role: ["client"], + text: "The client MUST inspect the result before requesting record disclosure.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-7", + specVersion: "0.1", + requirementIds: ["AS-32", "CL-13"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT infer authorization from its original request, an MCP tool listing, or a token's syntax.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-8", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["client"], + text: "A client unable to obtain or interpret the result MUST NOT request disclosure.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-9", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "For a credential that a companion binding defines to cover several grants, the AS MUST return every covered grant as a separate element.", + specAnchor: "#authorization-result", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.3-10", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT merge fields, instances, or resources across grants.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-11", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "A binding supporting such tokens MUST preserve each grant's source and client association during enforcement.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-12", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST return the same resolved grant facts to the client and to an authenticated RS, apart from token-specific active-state and expiry fields.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-13", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST protect result delivery as part of the authenticated OAuth exchange.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.3-14", + specVersion: "0.1", + requirementIds: ["AS-32"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT include inventory for unapproved sources or instances.", + specAnchor: "#authorization-result", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.4-1", + specVersion: "0.1", + requirementIds: ["AS-29"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST give different clients different identifiers for the same subject, grantor, or instance.", + specAnchor: "#pairwise-identifiers", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.4-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST give one client the same identifier for that subject, grantor, or instance in every grant and response, and MUST NOT reassign the identifier to another subject, grantor, or instance.", + specAnchor: "#pairwise-identifiers", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.4-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The identifier MUST NOT be reversible by clients: a client cannot derive the internal identifier, or another client's identifier, from it.", + specAnchor: "#pairwise-identifiers", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.4-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The final approval artifact MUST bind the exact identifier and the receiving client, and retained consent evidence MUST preserve that binding.", + specAnchor: "#pairwise-identifiers", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.4-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT change a client's `subject.id` between the pairwise value and the chosen identifier in a later grant.", + specAnchor: "#pairwise-identifiers", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.4-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST NOT disclose it to a client.", + specAnchor: "#pairwise-identifiers", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.5-1", + specVersion: "0.1", + requirementIds: ["AS-23"], + level: "must", + role: ["authorization-server"], + text: "An authorization server that issues a grant with `grantors` MUST: [...] authenticate each listed grantor; [...] verify, before showing a prospective grantor any subject-specific information or accepting their approval, that the grantor may approve the complete proposed disclosure under an explicit deployment or profile policy. Where that policy requires several grantors to approve together, the verification covers the complete set. The disclosure includes its selected data, client, purpose, access mode, and duration. The AS MUST reject approval when verification fails. How the AS establishes that authority, for example from a legal document or an authoritative register, is a deployment or profile choice; [...] show each grantor, on the approval surface, that they approve on the subject's behalf and in which capacity; [...] bind each grantor and capacity into the final approval artifact with the other decision fields; [...] never record a grantor's identity as the subject.", + specAnchor: "#representative-approval", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.7-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server", "resource-server", "client"], + text: "Three independent version axes exist in PDPP. They MUST NOT be conflated:", + specAnchor: "#version-layering", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "A design constraint on implementations rather than a checkable wire behaviour; its observable consequences are the three axis rows, recorded separately as 7.7-2, AS-17, and RS-11.", + }, + { + clauseId: "7.7-2", + specVersion: "0.1", + requirementIds: ["RS-11"], + level: "must", + role: ["resource-server"], + text: "RS MUST reject grants with unsupported major versions, returning 400 `unsupported_version`.", + specAnchor: "#version-layering", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-11/unsupported-grant-schema-version-rejected"], + }, + { + clauseId: "7.7-3", + specVersion: "0.1", + requirementIds: ["AS-21"], + level: "must", + role: ["authorization-server", "resource-server"], + text: "The current persisted-authorization-state reader MUST reject any persisted authorization state whose version or shape it cannot validate against a supported contract before its caller continues introspection or route handling. The reader MUST NOT reconstruct missing authorization or binding facts from current configuration. A deployment that cannot support or explicitly migrate such state MUST require fresh consent.", + specAnchor: "#version-layering", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "An ordering obligation over internal persisted state: the suite would need to write a legacy-shaped record into the target's store, which the adapter deliberately cannot do, and even then could not observe that the refusal happened *before* route handling.", + }, + { + clauseId: "7.8-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The first token MUST have a finite expiration, set by AS local policy and, when the grant has `expires_at`, no later than `expires_at`.", + specAnchor: "#access-modes", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.8-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "A binding that allows this MUST let the resource server enforce the window end, including under a cached introspection result and for a signed blob URL.", + specAnchor: "#access-modes", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.8-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "After the window ends, no access token carries the grant, and introspection MUST NOT report the grant active for reads.", + specAnchor: "#access-modes", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.8-4", + specVersion: "0.1", + requirementIds: ["AS-10"], + level: "must", + role: ["authorization-server"], + text: "After first issuance, the AS MUST reject further access-token issuance against the consumed grant, except a successor access token from the same refresh-token family issued before the window ends.", + specAnchor: "#access-modes", + applicability: "the AS supports `single_use` grants", + observable: "black-box-http", + caseIds: ["AS-10/single-use-grant-consumed"], + }, + { + clauseId: "7.8-5", + specVersion: "0.1", + requirementIds: ["AS-35"], + level: "should", + role: ["client"], + text: "Each client access token issued against it SHOULD have a finite expiration; the AS SHOULD issue such a token from a refresh-token family, which allows renewal without re-consent while the family remains valid. |", + specAnchor: "#access-modes", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.9-1", + specVersion: "0.1", + requirementIds: ["AS-37"], + level: "must", + role: ["authorization-server"], + text: "When a grant is revoked, read access ends and the AS MUST issue no further processing authority under it.", + specAnchor: "#processing-authority", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.9-2", + specVersion: "0.1", + requirementIds: ["AS-37"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT issue processing authority that outlasts it.", + specAnchor: "#processing-authority", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.9-3", + specVersion: "0.1", + requirementIds: ["AS-37", "AS-42"], + level: "must", + role: ["authorization-server"], + text: "An erasure instruction is the exception: from its effective time, the AS MUST issue no further processing authority, under any grant of the same client, that permits use of the data the instruction covers ([Erasure](#erasure)).", + specAnchor: "#processing-authority", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.9-4", + specVersion: "0.1", + requirementIds: ["AS-37"], + level: "must", + role: ["authorization-server"], + text: "Authority MUST NOT outlast a known future effective time of an advance instruction.", + specAnchor: "#processing-authority", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.10-1", + specVersion: "0.1", + requirementIds: ["RS-22"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT apply a host, viewer, or owner time zone.", + specAnchor: "#time-constraint-semantics", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.10-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "JSON Schema `format` is an annotation by default, so implementations MUST apply these rules themselves.", + specAnchor: "#time-constraint-semantics", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.10-3", + specVersion: "0.1", + requirementIds: [], + level: "should", + role: ["resource-server"], + text: "The resource server SHOULD report a present but invalid stored value to the operator.", + specAnchor: "#time-constraint-semantics", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.12-1", + specVersion: "0.1", + requirementIds: [], + level: "should", + role: ["authorization-server"], + text: "Authorization server UIs SHOULD model this flow as revocation followed by a new grant request.", + specAnchor: "#grant-narrowing", + applicability: "always", + observable: "review-only", + caseIds: [], + }, + { + clauseId: "7.12-2", + specVersion: "0.1", + requirementIds: ["AS-46"], + level: "must", + role: ["authorization-server"], + text: "On that surface the AS MUST state that `delete` makes the client delete its copy from the old grant, subject to the exclusions in [Erasure](#erasure).", + specAnchor: "#grant-narrowing", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.12-3", + specVersion: "0.1", + requirementIds: ["AS-46"], + level: "must", + role: ["authorization-server"], + text: "It MUST state that `keep` leaves the client's existing copy usable under the old grant's surviving-use terms, including data that the new grant does not cover.", + specAnchor: "#grant-narrowing", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.13-1", + specVersion: "0.1", + requirementIds: [], + level: "may", + role: ["client"], + text: "Unless an erasure instruction applies, the client MAY keep the grant's held data and MAY use it only within the grant's surviving-use terms:", + specAnchor: "#held-data", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.13-2", + specVersion: "0.1", + requirementIds: ["CL-19"], + level: "must", + role: ["client"], + text: "Already-issued processing authority remains usable only to the extent, and until the cessation deadline, permitted by its defining profile; the client MUST NOT otherwise use the data.", + specAnchor: "#held-data", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.14-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "ASs and clients MUST enforce these limits using conservative time bounds that include clock uncertainty.", + specAnchor: "#held-data-limits", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.14-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client MUST count time spent suspended or powered off.", + specAnchor: "#held-data-limits", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.14-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Clock rollback, restart, restored state, or an uncertain mapping between an AS timestamp and the client's clock MUST NOT extend a deadline.", + specAnchor: "#held-data-limits", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.14-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "If the client cannot establish that a use deadline has not passed, it MUST NOT use the data until it establishes that.", + specAnchor: "#held-data-limits", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.14-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Before further use after an interruption, it MUST establish whether disposal is overdue and complete any overdue disposal.", + specAnchor: "#held-data-limits", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.14-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "If it cannot establish that a disposal deadline remains in the future, it MUST treat disposal as due.", + specAnchor: "#held-data-limits", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-1", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST obtain an explicit disposition for each grant in the action's scope.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-2", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "Before the final step, the AS MUST list the complete affected set and distinguish grants it will end from ended grants whose held-data disposition it will record.", + specAnchor: "#ending-paths", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-3", + specVersion: "0.1", + requirementIds: ["AS-41", "AS-25"], + level: "should", + role: ["client"], + text: "The client SHOULD convey the choice as `pdpp_disposition` on the revocation request (Section 10). |", + specAnchor: "#ending-paths", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-4", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST show the owner that the grant ended and why, and MUST offer the disposition until the owner chooses.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.15-5", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "Before an AS stops answering lifecycle queries for good, it MUST offer the owner a disposition for every grant whose held data may remain.", + specAnchor: "#ending-paths", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.15-6", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "It MUST show each disposition as offered, recorded, sent, or receipt reported.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-7", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "It MUST warn the owner about each `delete` that no client has received yet: once the AS stops answering, the client cannot learn of it.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.15-8", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "Any interface through which the owner ends a grant, including a programmatic or MCP interface, MUST require an explicit disposition for every grant in the action's scope.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-9", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a request that lacks one and MUST NOT apply a default.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-10", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "On an interactive surface, the AS MUST present `keep` and `delete` before the final step, with neither preselected.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-11", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "For `keep`, the AS MUST show the surviving-use terms and the retention limit.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.15-12", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "For `delete`, it MUST show the statements that [Erasure](#erasure) requires.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.15-13", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST durably record the election as an erasure instruction whose effective time is the end of the grant.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.15-14", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "Before that time, the election alone MUST NOT revoke the grant, prohibit use, or prohibit issuance of processing authority.", + specAnchor: "#ending-paths", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.15-15", + specVersion: "0.1", + requirementIds: ["AS-41", "AS-42"], + level: "must", + role: ["authorization-server"], + text: "An immediate erasure request for the same grant MUST end the grant and make that instruction effective at that ending.", + specAnchor: "#ending-paths", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.15-16", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "It MUST retain the instruction identifier and original acceptance record.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.15-17", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "While held data of an ended grant may remain ([Lifecycle durability](#lifecycle-durability)), the AS MUST let the owner request its erasure.", + specAnchor: "#ending-paths", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.15-18", + specVersion: "0.1", + requirementIds: ["AS-41"], + level: "must", + role: ["authorization-server"], + text: "An effective instruction MUST NOT be cancelled or postponed.", + specAnchor: "#ending-paths", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.16-1", + specVersion: "0.1", + requirementIds: ["CL-25"], + level: "must", + role: ["client"], + text: "A client that offers the owner a way to disconnect it, or to end its access, MUST state in that flow what happens to held data.", + specAnchor: "#client-disconnect", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.16-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST let the owner choose `keep` or `delete`, with neither preselected.", + specAnchor: "#client-disconnect", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.17-1", + specVersion: "0.1", + requirementIds: ["CL-21"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT return a positive assessment for a grant to which an erasure instruction applies.", + specAnchor: "#reconciliation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.17-2", + specVersion: "0.1", + requirementIds: ["CL-21", "AS-40"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST compute every positive assessment from state ordered after every terminal event it has acknowledged ([Lifecycle durability](#lifecycle-durability)).", + specAnchor: "#reconciliation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.17-3", + specVersion: "0.1", + requirementIds: ["CL-21"], + level: "must", + role: ["client"], + text: "The client MUST associate each assessment with its original request.", + specAnchor: "#reconciliation", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.17-4", + specVersion: "0.1", + requirementIds: ["CL-21", "CL-20"], + level: "must", + role: ["client"], + text: "First use.** Before first use of a grant's held data, the client MUST hold a positive assessment for that grant.", + specAnchor: "#reconciliation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.17-5", + specVersion: "0.1", + requirementIds: ["CL-21"], + level: "must", + role: ["client"], + text: "Reconciliation.** While the client runs and it, or a party acting for it, holds any held data of a grant, the client MUST attempt a lifecycle query for that grant at least once per reconciliation interval.", + specAnchor: "#reconciliation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.17-6", + specVersion: "0.1", + requirementIds: ["CL-21"], + level: "must", + role: ["authorization-server"], + text: "If it later holds such data again, the duty resumes, and once a reconciliation interval has passed it MUST start a lifecycle query before further use.", + specAnchor: "#reconciliation", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.17-7", + specVersion: "0.1", + requirementIds: ["CL-21"], + level: "must", + role: ["client"], + text: "The client MUST also start a query before further use of held data in two cases: [...] a read or refresh fails in a way that shows the grant may have ended, such as HTTP 401 with Bearer error `invalid_token`, `grant_revoked`, `grant_expired`, `package_child_inactive`, or `invalid_grant` from the token endpoint; [...] the client resumes from suspension or power-off after an attempt fell due.", + specAnchor: "#reconciliation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.17-8", + specVersion: "0.1", + requirementIds: ["CL-21"], + level: "must", + role: ["client"], + text: "While the client runs, unless an attempt succeeds, it MUST start its next attempt within the retry interval of the preceding attempt's start, even if that attempt remains unanswered.", + specAnchor: "#reconciliation", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.17-9", + specVersion: "0.1", + requirementIds: ["CL-21"], + level: "must", + role: ["client"], + text: "The reconciliation interval takes precedence over `Retry-After`: the client MUST still attempt a query once per reconciliation interval.", + specAnchor: "#reconciliation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-1", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST record each instruction durably before acknowledging it, with an identifier, the grant, and its acceptance time ([Lifecycle durability](#lifecycle-durability)).", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.18-2", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "A later `keep` disposition, a retry, expiry, a new grant, or reapproval MUST NOT cancel it.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-3", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "A derivative that mixes covered data with other data MUST be deleted, or regenerated without the covered data.", + specAnchor: "#erasure", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-4", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "Regeneration MUST remove the old derivative.", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-5", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "Stop use.** On learning of an erasure instruction, the client MUST stop use of the covered data as soon as practicable, including queued and in-flight work.", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-6", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST stop no later than the stop-use bound after the effective time.", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-7", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "If it learns of the instruction after that deadline, it MUST stop at once.", + specAnchor: "#erasure", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-8", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "Delete.** The client MUST delete the covered data no later than the earlier of these times: [...] its first authenticated receipt of the instruction plus the deletion bound; [...] each record's [retention ceiling](#grant-retention).", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-9", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST record when it occurred.", + specAnchor: "#erasure", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-10", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "A later delivery, acknowledgement, forwarding, or AS record MUST NOT restart the deadline.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-11", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "A restore MUST replay deletion state before any restored data becomes usable.", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.18-12", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "Overdue disposal.** A client that misses a deletion deadline, for example while powered off, MUST complete the deletion before any further use of data of that grant.", + specAnchor: "#erasure", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-13", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST record the missed deadline and MUST NOT report the deletion as completed on time.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-14", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT process that data except as the obligation requires.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-15", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST record the scope of the retained data, the legal basis, and the end of the obligation.", + specAnchor: "#erasure", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-16", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It records the end as a date, as an event or condition, or as unknown, and MUST NOT record an invented date.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-17", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST report the exception, with its scope and basis, through the [grant lifecycle operation](#grant-lifecycle), unless law prohibits the report or the client has no current client authentication for the grant.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-18", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST keep a record of each instruction: receipt time, stop-use time, completion time, any exception, and the parties it informed.", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-19", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "Processing authority.** From an instruction's effective time, when it covers data that a grant of the same client carrying a processing permission also covers, the AS MUST issue no further processing authority for that data ([Read access and processing authority](#processing-authority)).", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-20", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: 'The AS MUST show the owner the time by which that processing stops under the defining profile, for example "training on this data stops by 21:40".', + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.18-21", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "On learning of the instruction, it MUST apply the defining profile's immediate stop requirements.", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.18-22", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "AS records and owner statements.** For each instruction, the AS MUST record separately its acceptance time and the time it first returned the instruction in a lifecycle answer.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-23", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST also record, separately, the receipt time and any completion or exception that a client reports.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-24", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "When the owner chooses `delete`, the AS MUST state the applicable scope and exceptions.", + specAnchor: "#erasure", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-25", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST state that the client learns of the instruction only when it obtains an answer from the AS, which it attempts daily while it runs.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-26", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST state that an attempt is not an answer, and that there is no maximum delivery delay.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-27", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST state what happens if the AS stops answering for good before the client receives the instruction.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-28", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26", "CL-19"], + level: "must", + role: ["authorization-server"], + text: "For a grant that carries a processing permission, it MUST state that the client may keep the data but gains no use or processing authority.", + specAnchor: "#erasure", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.18-29", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST describe these as duties of conforming clients, not proof of remote deletion.", + specAnchor: "#erasure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.18-30", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT show a receipt-based deletion deadline calculated from its acceptance time or from its first return of the instruction.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-31", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST show which stages it has recorded: requested, sent, receipt reported, and completion or exception reported.", + specAnchor: "#erasure", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.18-32", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["authorization-server"], + text: 'Until a receipt is reported, it MUST show the latest recorded stage and no deletion date, for example "sent on 2 November; no receipt reported".', + specAnchor: "#erasure", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.19-1", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-24", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST impose the grant's purpose and retention on that party.", + specAnchor: "#downstream-holders", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.19-2", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-24", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST impose these held-data duties on that party.", + specAnchor: "#downstream-holders", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.19-3", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-24", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST impose the [AI training](#ai-training) restriction on that party.", + specAnchor: "#downstream-holders", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.19-4", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-24", "CL-26", "CL-30"], + level: "must", + role: ["client"], + text: "The client MUST NOT treat a party's independent use of the data as processing for the client.", + specAnchor: "#downstream-holders", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.19-5", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-24", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST relay every erasure instruction to each party acting for it.", + specAnchor: "#downstream-holders", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.19-6", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-24", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST relay the deadlines as absolute times.", + specAnchor: "#downstream-holders", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.19-7", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-24", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST keep a record of the parties it informed and their confirmations.", + specAnchor: "#downstream-holders", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.20-1", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "A client MUST NOT disclose PDPP data to an independent recipient unless a registered processing permission expressly covers that disclosure.", + specAnchor: "#independent-recipient-disclosure", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.20-2", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26", "CL-29"], + level: "must", + role: ["client"], + text: "The client MUST hold valid processing authority under the permission's defining profile when it discloses the data.", + specAnchor: "#independent-recipient-disclosure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.20-3", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26", "CL-31"], + level: "must", + role: ["client"], + text: "A client MUST NOT infer disclosure authority from the grant's purpose, retention, an owner's action instruction, client claims, a positive assessment, or read-token activity.", + specAnchor: "#independent-recipient-disclosure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.20-4", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "A client MUST NOT infer that authority from an exception to erasure or retention.", + specAnchor: "#independent-recipient-disclosure", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.20-5", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "may", + role: ["client"], + text: "A client MAY disclose data to the owner within the grant's applicable use terms.", + specAnchor: "#independent-recipient-disclosure", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.20-6", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26", "AS-56"], + level: "may", + role: ["client"], + text: "A client MAY use parties acting for it as [Downstream holders](#downstream-holders) permits.", + specAnchor: "#independent-recipient-disclosure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.20-7", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "may", + role: ["resource-server"], + text: "An RS MAY disclose owner-held data under a fresh grant to the receiving client, as Section 3 describes.", + specAnchor: "#independent-recipient-disclosure", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.20-8", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "A client relying on that exception MUST limit the disclosure to what the obligation requires.", + specAnchor: "#independent-recipient-disclosure", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.20-9", + specVersion: "0.1", + requirementIds: ["AS-43", "CL-22", "CL-23", "CL-26"], + level: "must", + role: ["client"], + text: "The client MUST record the data disclosed, recipient, time, and legal obligation, unless law prohibits that record.", + specAnchor: "#independent-recipient-disclosure", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-1", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "Every AS MUST provide it.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-2", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-25"], + level: "must", + role: ["authorization-server"], + text: "Discovery.** In the OAuth binding, the AS MUST advertise the operation as `pdpp_grant_lifecycle_endpoint` in its RFC 8414 metadata.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-3", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["client"], + text: "A public client MUST send it once.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-4", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "A write MUST name exactly one grant.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-5", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject, with HTTP 400 `invalid_request`, a malformed request, a missing required parameter, an unknown parameter value, and a request that carries both `report` and `pdpp_disposition`.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-6", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST accept any access token or refresh token that it issued to that client and that covers the grant, whether active, expired, superseded, or revoked.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-7", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["client"], + text: "A confidential client MUST authenticate as at the token endpoint.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-8", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "A token bound to a key under [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449) (DPoP) MUST come with a valid DPoP proof for that key.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.21-9", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["client"], + text: "Assertions and proofs.** A client that authenticates with a JWT assertion ([RFC 7523](https://www.rfc-editor.org/rfc/rfc7523)) MUST set its `aud` to the AS issuer identifier and MUST include a non-empty `jti` string that uniquely identifies the assertion for that client.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-10", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject an assertion with any other audience or with a missing or empty `jti`.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-11", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "It MUST reject reuse of a `jti` already accepted for that client while the previously accepted assertion remains valid.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-12", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "A DPoP proof MUST carry `htm` `POST` and an `htu` equal to the lifecycle endpoint URL.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.21-13", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "When `token` is an access token, the proof MUST carry `ath` for that token.", + specAnchor: "#grant-lifecycle", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.21-14", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST check the proof as RFC 9449 Section 4.3 defines, including `jti` replay detection.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "7.21-15", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "It MUST be able to disable any token it issued, including one never presented to this operation.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-16", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "It MUST then refuse that token for this operation.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-17", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["client"], + text: "Recovery.** A client whose tokens are disabled for lifecycle use MUST be able to recover lifecycle access without a new data grant and without read access.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-18", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "For a public client, the AS MUST support [lifecycle recovery](#lifecycle-recovery).", + specAnchor: "#grant-lifecycle", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-19", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "Storage.** For this operation the AS MUST keep digests of tokens, not the tokens themselves.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.21-20", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST use the same entry schema and error in each of these cases, echoing only the requested `grant_id`, so that a caller cannot tell them apart.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-21", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MAY rate-limit the operation with HTTP 429, but MUST admit at least one query per grant per retry interval from each client.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-22", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST end the grant if it has not ended.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-23", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "When `reported_at` is present, the request also reports that receipt, and the AS MUST record that event time under the rules for `report=received`.", + specAnchor: "#grant-lifecycle", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-24", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "When it is absent, the AS records the erasure request but MUST NOT infer a receipt time or a receipt-based deadline from the request's arrival.", + specAnchor: "#grant-lifecycle", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.21-25", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject, with `invalid_request`, a `reported_at` later than the request, a `retained_until` in none of its three forms, and a date-time `retained_until` earlier than `reported_at`.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-26", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT show a `retained` report, or the absence of a report, as completed deletion.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.21-27", + specVersion: "0.1", + requirementIds: ["AS-39"], + level: "must", + role: ["authorization-server"], + text: "It MUST show the end of a `retained` report in the form the client gave: a date, the event or condition, or that the end is unknown.", + specAnchor: "#grant-lifecycle", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.22-1", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "Every AS MUST support this type and advertise it in `authorization_details_types_supported`.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.22-2", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "A recovery request MUST contain no other authorization detail and MUST NOT request read or processing authority.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.22-3", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject a malformed or mixed request with `invalid_authorization_details`.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.22-4", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST authenticate the owner and verify that it issued each named grant to the requesting client for that owner, including when the grant has ended.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.22-5", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "It MUST reject the element with `invalid_authorization_details` when any grant fails that check, without revealing which grant failed.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.22-6", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "Before approval it MUST show the client, source, purpose, and issue date of each grant.", + specAnchor: "#lifecycle-recovery", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.22-7", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "It MUST state that recovery permits lifecycle queries, erasure reports, and client-conveyed erasure requests for those grants, but gives no read or processing authority.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.22-8", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["client"], + text: "The approved grant identifier set and client binding MUST be fixed in the approval artifact and retained consent evidence.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.22-9", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "Its lifecycle validity is separate from introspection `active`: introspection MUST report `active: false`, and the RS MUST reject it.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.22-10", + specVersion: "0.1", + requirementIds: ["AS-39", "AS-47"], + level: "must", + role: ["authorization-server"], + text: "Issuance MUST NOT create or reopen any data grant, read window, or processing authority.", + specAnchor: "#lifecycle-recovery", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-1", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST disclose that limitation in its durability statement below.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-2", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT declare that its mechanism detects a path on which it does not fail closed.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-3", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST record each terminal event durably before it acknowledges the event to the owner or to a client.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-4", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "That record MUST be totally ordered against the AS's issuance of tokens, processing authority, and positive assessments.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-5", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "A backup restore or a failover through a path that the AS declares its mechanism detects MUST NOT make any enforcement point act on a history that omits an acknowledged terminal event.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-6", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "Each path MUST compute its result from state that includes every terminal event acknowledged before the computation.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-7", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "That result's original computation MUST meet the ordering rule.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-8", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "A delayed response, restore, replay, or clock discontinuity MUST NOT restart it.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-9", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "A read path that establishes status directly instead of using such a cached result MUST establish current ordered state.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-10", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT then use it to report a revoked grant active, to issue or re-deliver a token for that grant, or to issue processing authority.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-11", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT use it to authenticate a write or to return a positive assessment.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-12", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT promote an advance election to an effective instruction, or infer an effective time, from stale state.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-13", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "A restore of the data that read enforcement uses MUST NOT reopen reads under a grant whose revocation the AS acknowledged.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-14", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "After a restore or failover through a declared path, an enforcement point MUST NOT treat a history as current until it establishes that the history includes every acknowledged terminal event.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-15", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "Before acknowledging a terminal event, the AS MUST durably advance whatever evidence its mechanism uses.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-16", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "If an enforcement point cannot establish currency, it MUST fail closed as this section requires until authoritative facts are recovered.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-17", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST record each durably before it returns a response that discloses or relies on it.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-18", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT reuse either after any restore, failover, or rollback, whether or not its mechanism detects it.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-19", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "On a declared path, it MUST NOT reassign a pairwise identifier.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-20", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "While the AS cannot establish a grant's history, it MUST fail closed for that grant: it issues and re-delivers no token, issues no processing authority, reports no active introspection result, and returns no positive assessment.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-21", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT rebuild an active grant from an older backup.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-22", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "While its history is unavailable, the AS MUST still accept an owner's revocation, disposition, or erasure request, and MUST NOT reject one for that reason.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-23", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST record each such request durably before it acknowledges it, and MUST apply each to the history before any answer for the affected grant once the history is re-established.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-24", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST show the owner that the action is recorded and pending, and that the AS issues no tokens or processing authority and returns no positive assessments for the affected grants meanwhile.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.23-25", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST explain that read access ends within Section 10's bounded propagation period; an existing cached introspection result can permit reads only within its original cache lifetime and applicable read deadline.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-26", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "For an action affecting several grants, the AS MUST durably record a result for each grant.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-27", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT report the action complete until every requested grant ending and every requested disposition has been durably recorded.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-28", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "A partial result MUST identify each grant as recorded or pending, without treating a pending result as complete.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-29", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST show the owner whether each terminal event is pending or recorded.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation governs what the authorization server shows or retains during owner review, which the black-box adapter cannot observe.", + }, + { + clauseId: "7.23-30", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST describe the durability it actually provides, including its restore mechanism, the disk and host losses it does not survive, and the restore and rollback paths its mechanism does not detect.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-31", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST state that a rollback it does not detect, such as a revert of a virtual-machine snapshot, can revive revoked grants and lose recorded deletions, unless the deployment declares a mechanism that detects it.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-32", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MUST also state that such a rollback can revive consumed `single_use` grants, redeemed authorization codes, and superseded refresh tokens.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.23-33", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "An AS that keeps its history on a single disk MUST NOT claim that the record survives loss of that disk.", + specAnchor: "#lifecycle-durability", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.23-34", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST retain the grant's lifecycle and authentication bindings while the grant can authorize further reads or processing, while held-data duties remain unresolved, or while legally retained data may remain.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-35", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "The AS MUST retain effective erasure instructions, reported exceptions, and the bindings needed for queries and reports until those duties are resolved.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-36", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "It MAY compact resolved history to minimal terminal records, but MUST preserve the facts needed to prevent a restore, replay, or later issuance from reviving ended authority or cancelling erasure.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.23-37", + specVersion: "0.1", + requirementIds: ["AS-44"], + level: "must", + role: ["authorization-server"], + text: "These records MUST NOT contain deleted source data.", + specAnchor: "#lifecycle-durability", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.24-1", + specVersion: "0.1", + requirementIds: ["CL-6"], + level: "must", + role: ["client"], + text: "Retention is a structured policy declaration and policy commitment by the data recipient (the client). PDPP does not technically enforce retention. Enforcement is through legal agreements or contractual obligations; a trust registry supports admission and accountability for a retention commitment rather than enforcing it, because Core defines no compliance-evidence query a registry could answer.", + specAnchor: "#grant-retention", + applicability: "the grant carries a `retention` policy", + observable: "review-only", + caseIds: [], + gapNote: + "The quoted text carries no RFC 2119 keyword; its MUST level is the Section 9 conformance list's. The spec states outright that PDPP does not technically enforce retention, so no protocol test can exist; Section 9 CL item 6 states it as a client MUST, and the only available evidence is contractual rather than executable.", + }, + { + clauseId: "7.24-2", + specVersion: "0.1", + requirementIds: ["AS-31"], + level: "must", + role: ["authorization-server"], + text: "In v0.1, `max_duration` MUST have the form `PnD`, where n is a positive whole number of days, such as `P90D`.", + specAnchor: "#grant-retention", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.24-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST reject any other form, including months, years, weeks, and time components, with `invalid_authorization_details` in the OAuth/RAR binding.", + specAnchor: "#grant-retention", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "7.24-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client that receives a `max_duration` in any other form MUST treat it as invalid and fail closed: it treats each covered record's retention ceiling as already reached.", + specAnchor: "#grant-retention", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "7.24-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "At a record's retention ceiling, the client MUST complete the `on_expiry` action for that record and its derivatives, with no further grace period, subject to the learned-parameter exception in [Held data after a grant ends](#held-data).", + specAnchor: "#grant-retention", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, + { + clauseId: "7.24-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client that cannot complete qualifying anonymization by the ceiling MUST delete the data by then.", + specAnchor: "#grant-retention", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, +]; + +// Clause inventory for Core section 8 (8. Resource Server Interface). +const SECTION_8: readonly ClauseEntry[] = [ + { + clauseId: "8.0-1", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "A conformant resource server MUST implement this interface for cross-deployment interoperability.", + specAnchor: "#resource-server-interface", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.1-1", + specVersion: "0.1", + requirementIds: ["RS-3"], + level: "must", + role: ["resource-server"], + text: "Positive introspection results MUST NOT be cached for more than 60 seconds, and never past `exp` when present.", + specAnchor: "#grant-enforcement", + applicability: "the deployment separates AS and RS", + observable: "black-box-http", + caseIds: [], + gapNote: + "Needs a separated AS/RS target plus a clock the case controls: the suite would have to revoke a grant and prove the RS stops serving within 60 seconds, which no current target topology supports.", + }, + { + clauseId: "8.1-2", + specVersion: "0.1", + requirementIds: ["RS-5"], + level: "must", + role: ["resource-server"], + text: "For owner-token current-capability reads, the effective filter is the permitted owner request filter alone: an owner token carries no grant, so there is no grant filter to intersect. Request filters can only narrow the current owner read and cannot widen it. [...] In v0.1, client-token reads do not have request-time predicate filters (see List records below); the resource server enforces the frozen grant constraints and rejects a client request-time predicate filter rather than evaluating it.", + specAnchor: "#grant-enforcement", + applicability: "always", + observable: "black-box-http", + partial: true, + caseIds: ["RS-9/client-token-exact-filter-rejected", "RS-10/owner-filter-unknown-field-rejected"], + gapNote: + "Carries no RFC 2119 keyword here; its MUST level is Section 9 RS item 5's, and the enforceable half is restated with keywords at 8.12-2 and 8.12-10. The owner-side \"cannot widen\" property is not directly tested — that needs an owner filter naming a record outside the owner's scope.", + }, + { + clauseId: "8.1-3", + specVersion: "0.1", + requirementIds: ["RS-2", "RS-15"], + level: "must", + role: ["resource-server"], + text: "The RS MUST NOT re-validate authorization against the current SourceDeclaration. All enforcement constraints are in the resolved grant. Current serving metadata MAY route a granted instance, describe current schemas or query capabilities, or reject a request that cannot currently be served. It MUST NOT widen or reinterpret a stream, instance, field, time field, time type, bound, or resource key.", + specAnchor: "#grant-enforcement", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-15/client-metadata-projection-closed", "RS-2/field-projection-not-exceeded"], + }, + { + clauseId: "8.1-4", + specVersion: "0.1", + requirementIds: ["RS-4"], + level: "must", + role: ["resource-server"], + text: "The RS MUST determine the token's properties (including `pdpp_token_kind`) solely from the introspection response, never from token syntax.", + specAnchor: "#grant-enforcement", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-4/token-kind-not-inferred-from-syntax"], + }, + { + clauseId: "8.2-1", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "A PDPP data grant MUST NOT authorize those actions.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-2", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "For an operation using PDPP-governed data access, the RS MUST enforce the resolved grant independently of provider action permission.", + specAnchor: "#existing-permissions", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-3", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "Existing broad read scopes MUST NOT widen that grant.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-4", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "When an operation needs both permissions, the server MUST verify both before execution.", + specAnchor: "#existing-permissions", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-5", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "Denial of action permission MUST NOT cancel independently approved reads unless the authorization transaction explicitly requires both.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-6", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "An action response containing existing PDPP-governed records or facts derived from them MUST satisfy the applicable data grant.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-7", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "Status MUST describe only the outcome of the authorized action.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-8", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "The receipt MUST NOT reveal pre-existing field values, unrelated record existence, or private associations, or confer permission to retrieve source data.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-9", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "If the action response would disclose unauthorized data, the server MUST redact that data or refuse the operation.", + specAnchor: "#existing-permissions", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-10", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "The server MUST resolve a predictable disclosure failure before performing the side effect.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.2-11", + specVersion: "0.1", + requirementIds: ["RS-27"], + level: "must", + role: ["resource-server"], + text: "An unexpected post-action failure MUST NOT cause broader disclosure; the provider's action protocol governs retry and recovery.", + specAnchor: "#existing-permissions", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-1", + specVersion: "0.1", + requirementIds: ["RS-3", "AS-18"], + level: "must", + role: ["resource-server"], + text: "For separated AS/RS deployments, the RS MUST authenticate to the AS introspection endpoint as required by RFC 7662.", + specAnchor: "#token-introspection", + applicability: "the deployment separates AS and RS", + observable: "black-box-http", + caseIds: ["AS-18/introspection-requires-authentication"], + }, + { + clauseId: "8.3-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "It MAY be an internal identifier that differs from the grant's pairwise `subject.id`, and the RS MUST NOT disclose it to a client (see [Pairwise identifiers](#pairwise-identifiers)).", + specAnchor: "#token-introspection", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "8.3-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "Every entry for one `(source_id, instance_id)` MUST name the same `rs_instance`; the RS MUST reject conflicting entries as `grant_invalid` before serving data.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "For a given issuer, subject, and receiving client, the AS MUST assign instance handles that identify only one instance across all sources.", + specAnchor: "#token-introspection", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST apply the same cross-source uniqueness requirement to an owner's handles.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "A `pdpp_instance_bindings` response MUST NOT contain the same `instance_id` under different `source_id` values.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-7", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST reject such a response with 403 `grant_invalid` before serving data.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-8", + specVersion: "0.1", + requirementIds: ["AS-18", "RS-3"], + level: "must", + role: ["authorization-server", "resource-server"], + text: "The introspection response MUST contain the complete context needed to enforce the request. The separated RS MUST enforce only from that response and MUST NOT make a second AS lookup while handling the request.", + specAnchor: "#token-introspection", + applicability: "the deployment separates AS and RS", + observable: "review-only", + caseIds: [], + gapNote: + "The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer.", + }, + { + clauseId: "8.3-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "For a Core client token, the AS MUST include each stream's `required_fields` in the grant within `authorization_details`.", + specAnchor: "#token-introspection", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-10", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The separated RS MUST obtain this set only from that grant in the authenticated introspection response.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-11", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "A co-located RS MUST obtain the same set through its local equivalent.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-12", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "Before serving data under a grant, the RS MUST verify that every StreamGrant contains a `required_fields` array of unique strings.", + specAnchor: "#token-introspection", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-13", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST verify that every entry also appears in that StreamGrant's `fields`.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-14", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST reject a grant that fails either check with HTTP 403 `grant_invalid`.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-15", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST NOT interpret an absent `required_fields` member as an empty set.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-16", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST NOT derive the set from a current declaration, a retained declaration outside the resolved context, stored record contents, primary keys, or another grant.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "8.3-17", + specVersion: "0.1", + requirementIds: ["RS-24"], + level: "must", + role: ["resource-server"], + text: "A separated RS MUST resolve each handle only through this member and MUST fail closed when a binding is missing.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "8.3-18", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST NOT return `pdpp_instance_bindings` to a client.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.3-19", + specVersion: "0.1", + requirementIds: ["RS-4"], + level: "must", + role: ["resource-server"], + text: "A resource server that receives a `pdpp_token_kind` value it does not recognize MUST treat the token as unauthorized for all operations defined in this specification.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-4/unrecognized-token-kind-is-unauthorized"], + }, + { + clauseId: "8.3-20", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "Any other profile-defined kind MUST be an absolute URI that the profile's publisher controls, so that kinds from different profiles do not collide.", + specAnchor: "#token-introspection", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "8.3-21", + specVersion: "0.1", + requirementIds: ["RS-3"], + level: "must", + role: ["resource-server"], + text: "Self-contained JWTs (e.g., signed JWTs) are allowed as an optimization but MUST NOT be the sole revocation mechanism; the RS MUST still be able to check active status through introspection or local equivalent.", + specAnchor: "#token-introspection", + applicability: "the RS issues or accepts self-contained tokens", + observable: "black-box-http", + partial: true, + caseIds: ["AS-8/revoked-grant-refused"], + gapNote: + "A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path.", + }, + { + clauseId: "8.4-1", + specVersion: "0.1", + requirementIds: ["CL-12", "AS-33"], + level: "must", + role: ["authorization-server"], + text: "An AS MUST advertise `https://pdpp.dev/data-access` in `authorization_details_types_supported` in its OAuth authorization-server metadata.", + specAnchor: "#authentication", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.4-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client MUST establish support for this type before requesting it.", + specAnchor: "#authentication", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.4-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "Unknown detail types MUST be rejected.", + specAnchor: "#authentication", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.4-4", + specVersion: "0.1", + requirementIds: ["RS-12"], + level: "must", + role: ["resource-server"], + text: "The RS MUST derive the `subject_id` from the introspection response and MUST reject any request attempting to access data outside that subject's scope.", + specAnchor: "#authentication", + applicability: "the target issues owner tokens", + observable: "black-box-http", + caseIds: ["RS-12/foreign-subject-cannot-read"], + }, + { + clauseId: "8.4-5", + specVersion: "0.1", + requirementIds: ["RS-13"], + level: "should", + role: ["resource-server"], + text: "An owner holding a valid owner token MAY query their own data using the standard client query endpoints without a client grant. This is the v0.1 self-export mechanism and does not require a separate grant. Conformant Core RS implementations SHOULD support this capability (see Section 9 conformance item 13).", + specAnchor: "#authentication", + applicability: "the target declares `pdpp_self_export_supported`", + observable: "black-box-http", + caseIds: ["RS-13/self-export-supported"], + }, + { + clauseId: "8.5-1", + specVersion: "0.1", + requirementIds: ["RS-16"], + level: "must", + role: ["resource-server"], + text: "A resource server MUST publish OAuth 2.0 Protected Resource Metadata as defined in RFC 9728.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-16/protected-resource-metadata-published"], + }, + { + clauseId: "8.5-2", + specVersion: "0.1", + requirementIds: ["RS-16"], + level: "must", + role: ["resource-server"], + text: 'On a 401, the resource server MUST include a `WWW-Authenticate: Bearer` challenge, as RFC 6750 Section 3 requires. It MUST set `error="invalid_token"` when a token was presented and rejected. And it MUST include the `resource_metadata` parameter RFC 9728 Section 5.1 defines, carrying the URL of this document.', + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-16/401-carries-resource-metadata-challenge", "RS-16/unauthenticated-request-refused"], + }, + { + clauseId: "8.5-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The first four are REQUIRED.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.5-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "`pdpp_freshness_granularity_seconds` is REQUIRED when the resource server attaches `freshness` to a client-token response.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.5-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: 'For a resource server claiming this specification\'s v0.1 conformance, the value MUST be `"0.1"` and that contract MUST be supported.', + specAnchor: "#protected-resource-metadata", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.5-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["client"], + text: "A client MUST obtain protected resource metadata without depending on `PDPP-Version` negotiation.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.5-7", + specVersion: "0.1", + requirementIds: ["CL-32"], + level: "must", + role: ["client"], + text: "A client MUST NOT infer an HTTP contract from a version-looking component of `pdpp_core_query_base`.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.5-8", + specVersion: "0.1", + requirementIds: ["RS-16"], + level: "should", + role: ["resource-server"], + text: "a resource server SHOULD publish it because a consent surface has no other name to display for the resource.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "black-box-http", + caseIds: [], + }, + { + clauseId: "8.5-9", + specVersion: "0.1", + requirementIds: ["RS-16"], + level: "must", + role: ["resource-server"], + text: "A PDPP resource server MUST publish `authorization_servers` when that set is enumerable, so a client can reach the issuer without prior configuration. When the set is not enumerable, the resource server MUST omit the member rather than publish a partial list.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "Whether the target's authorization-server set is enumerable is not declared anywhere the suite can read, so the presence or absence of the member cannot be judged; closing this needs a target-config field stating enumerability.", + }, + { + clauseId: "8.5-10", + specVersion: "0.1", + requirementIds: ["CL-28"], + level: "must", + role: ["client"], + text: "The grant object contains no issuer identifier, so the client MUST retain the issuer with each grant and MUST NOT infer it from `grant_id`, `source.id`, or an untrusted credential.", + specAnchor: "#protected-resource-metadata", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.6-1", + specVersion: "0.1", + requirementIds: ["CL-15"], + level: "must", + role: ["client"], + text: "A client MUST ignore any member that it does not recognize in a Core-defined JSON object that a Section 8 endpoint returns.", + specAnchor: "#response-forward-compatibility", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.6-2", + specVersion: "0.1", + requirementIds: ["CL-15"], + level: "must", + role: ["resource-server"], + text: "An added member MUST NOT change the meaning of a member that this specification defines, so a client that ignores it still reads the response correctly.", + specAnchor: "#response-forward-compatibility", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.9-1", + specVersion: "0.1", + requirementIds: ["RS-15"], + level: "must", + role: ["resource-server"], + text: "It MUST NOT expose the current `cursor_field`. Current query, view, relationship, filter, expansion, and aggregation capabilities MUST NOT appear unless a future frozen grant vocabulary includes them. Current metadata MAY report availability or `freshness` (see [Freshness metadata](#freshness)), or reject an unavailable operation, but MUST NOT make the grant appear broader or semantically different than issued. A source declaration change after grant issuance MUST NOT become visible through this endpoint.", + specAnchor: "#stream-metadata", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-15/client-metadata-projection-closed"], + }, + { + clauseId: "8.10-1", + specVersion: "0.1", + requirementIds: ["RS-25"], + level: "may", + role: ["resource-server"], + text: "A resource server MAY attach a `freshness` array to stream listings, stream metadata, and record-list responses.", + specAnchor: "#freshness", + applicability: "always", + observable: "black-box-http", + caseIds: [], + }, + { + clauseId: "8.10-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "An operation confined to data outside this scope MUST NOT change the value.", + specAnchor: "#freshness", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.10-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "Failure timing.** A client-token response MUST NOT carry `last_failure_at` or any other failure time.", + specAnchor: "#freshness", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.10-4", + specVersion: "0.1", + requirementIds: ["RS-25"], + level: "must", + role: ["resource-server"], + text: "Coarsening.** In a client-token response, the resource server MUST truncate each `last_success_at` value to the granularity that it declares as `pdpp_freshness_granularity_seconds` in its [protected resource metadata](#protected-resource-metadata).", + specAnchor: "#freshness", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.10-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The granularity MUST be at least 3600 seconds.", + specAnchor: "#freshness", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.10-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The resource server MUST NOT report a truncated value before its interval ends.", + specAnchor: "#freshness", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.11-1", + specVersion: "0.1", + requirementIds: ["CL-11"], + level: "must", + role: ["client"], + text: "A client MUST NOT infer from these that a record does not exist at the source, or that collection is complete.", + specAnchor: "#absence-of-records", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.12-1", + specVersion: "0.1", + requirementIds: ["CL-3"], + level: "must", + role: ["client"], + text: "Clients MUST NOT parse or construct cursor tokens.", + specAnchor: "#list-records", + applicability: "always", + observable: "client-capture", + caseIds: ["CL-3/forwards-cursors-as-opaque-values"], + }, + { + clauseId: "8.12-2", + specVersion: "0.1", + requirementIds: ["RS-9"], + level: "must", + role: ["resource-server"], + text: "Owner-token current-capability filters only. Client-token requests MUST reject exact and range forms in v0.1.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-9/client-token-exact-filter-rejected"], + }, + { + clauseId: "8.12-3", + specVersion: "0.1", + requirementIds: ["RS-9"], + level: "must", + role: ["resource-server"], + text: "Client-token records requests MUST reject `view`; clients use explicit `fields` or the field projection already frozen into the grant. Mutually exclusive with `fields`.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + partial: true, + caseIds: ["RS-9/client-token-view-rejected"], + gapNote: + "The client-token rejection half is covered. The sentence's second half — `view` and `fields` being mutually exclusive — is an owner-token concern (a client token must reject `view` outright, so the pair can never both be honoured there) and is tracked at 6.11-3, which needs a view-declaring target.", + }, + { + clauseId: "8.12-4", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "Every client-token field MUST be in the grant, or the whole read fails with 403 `field_not_granted`; see [Effective projection](#record-evidence).", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.12-5", + specVersion: "0.1", + requirementIds: ["RS-9"], + level: "must", + role: ["resource-server"], + text: "Client-token requests MUST reject this parameter in v0.1.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-9/client-token-expand-rejected"], + }, + { + clauseId: "8.12-6", + specVersion: "0.1", + requirementIds: ["RS-9", "RS-10"], + level: "must", + role: ["resource-server"], + text: "Client-token requests MUST reject this parameter in v0.1.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-9/client-token-expand-limit-rejected"], + }, + { + clauseId: "8.12-7", + specVersion: "0.1", + requirementIds: ["RS-14"], + level: "must", + role: ["resource-server", "source-declaration"], + text: "Advanced stream-specific query power MUST be declared in stream metadata under `query`.", + specAnchor: "#list-records", + applicability: "the target supports advanced query capability", + observable: "black-box-http", + caseIds: ["RS-14/owner-metadata-full-current-document"], + }, + { + clauseId: "8.12-8", + specVersion: "0.1", + requirementIds: ["RS-10"], + level: "must", + role: ["resource-server"], + text: "Unknown query parameters and unsupported query shapes MUST be rejected with HTTP 400 and MUST NOT be silently ignored.", + specAnchor: "#list-records", applicability: "always", - observable: "review-only", - caseIds: [], + observable: "black-box-http", + caseIds: [ + "RS-10/unknown-parameter-rejected", + "RS-10/unsupported-bracketed-shape-rejected", + "RS-10/owner-filter-unknown-field-rejected", + "RS-10/owner-expand-undeclared-relation-rejected", + "RS-10/malformed-limit-rejected", + ], }, { - clauseId: "7.10-1", + clauseId: "8.12-9", specVersion: "0.1", - requirementIds: ["CL-6"], + requirementIds: ["RS-10"], level: "must", - role: ["client"], - text: "Retention is a structured policy declaration and policy commitment by the data recipient (the client). PDPP does not technically enforce retention. Enforcement is through legal agreements or contractual obligations; a trust registry supports admission and accountability for a retention commitment rather than enforcing it, because Core defines no compliance-evidence query a registry could answer.", - specAnchor: "#retention", - applicability: "the grant carries a `retention` policy", - observable: "review-only", - caseIds: [], - gapNote: - "The spec states outright that PDPP does not technically enforce retention, so no protocol test can exist; Section 9 CL item 6 states it as a client MUST, and the only available evidence is contractual rather than executable.", + role: ["resource-server", "client"], + text: "clients SHOULD branch on `code`, not on message text. [...] Warnings are not errors and MUST NOT change the HTTP status.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-10/oversized-limit-clamped-with-warning"], }, -]; - -// --------------------------------------------------------------------------- -// Section 8: Resource Server Interface -// --------------------------------------------------------------------------- - -const SECTION_8: readonly ClauseEntry[] = [ { - clauseId: "8.1-1", + clauseId: "8.12-10", specVersion: "0.1", - requirementIds: ["RS-3"], + requirementIds: ["RS-9"], level: "must", role: ["resource-server"], - text: "Positive introspection results MUST NOT be cached longer than `min(token_exp, 60 seconds)`.", - specAnchor: "#grant-enforcement", - applicability: "the deployment separates AS and RS", + text: "Client-token requests that contain any exact or range `filter[...]` parameter MUST be rejected with HTTP 400 `invalid_request` before the RS consults current SourceDeclaration or serving metadata. This rejection applies regardless of whether the field or operator would otherwise be declared.", + specAnchor: "#list-records", + applicability: "always", observable: "black-box-http", - caseIds: [], + partial: true, + caseIds: ["RS-9/client-token-exact-filter-rejected"], gapNote: - "Needs a separated AS/RS target plus a clock the case controls: the suite would have to revoke a grant and prove the RS stops serving within 60 seconds, which no current target topology supports.", + 'The status and code are tested; the ordering half ("before the RS consults ... metadata") is not, and black-box observation cannot establish it.', }, { - clauseId: "8.1-1a", + clauseId: "8.12-11", specVersion: "0.1", - requirementIds: ["RS-5"], + requirementIds: ["RS-9"], level: "must", role: ["resource-server"], - text: "For owner-token current-capability reads, the effective filter is the permitted owner request filter alone: an owner token carries no grant, so there is no grant filter to intersect. Request filters can only narrow the current owner read and cannot widen it. [...] In v0.1, client-token reads do not have request-time predicate filters (see List records below); the resource server enforces the frozen grant constraints and rejects a client request-time predicate filter rather than evaluating it.", - specAnchor: "#grant-enforcement", + text: "Client-token requests that contain `expand[]` or `expand_limit[...]` MUST be rejected with HTTP 400 `invalid_request` before the RS consults current SourceDeclaration or serving metadata.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + caseIds: ["RS-9/client-token-expand-rejected"], + }, + { + clauseId: "8.12-12", + specVersion: "0.1", + requirementIds: ["RS-6", "CL-3"], + level: "must", + role: ["resource-server", "client"], + text: "A client MUST follow a `next_cursor` with the same context that produced it. To change the context, the client MUST restart pagination without a cursor. Resource servers MUST reject a page cursor used with a different context as `invalid_cursor`.", + specAnchor: "#list-records", applicability: "always", observable: "black-box-http", partial: true, - caseIds: ["RS-9/client-token-exact-filter-rejected", "RS-10/owner-filter-unknown-field-rejected"], + caseIds: ["RS-6/order-mismatched-cursor-rejected"], gapNote: - "Carries no RFC 2119 keyword here; its MUST level is Section 9 RS item 5's, and the enforceable half is restated with keywords at 8.9-2 and 8.9-9. The owner-side \"cannot widen\" property is not directly tested — that needs an owner filter naming a record outside the owner's scope.", + "The resource-server half is covered. The client half — that a client follows a `next_cursor` with the same `order` and restarts pagination to change direction — binds the client's outgoing requests and stays `client-capture`: the adapter has no reverse channel onto a client under test.", }, { - clauseId: "8.1-2", + clauseId: "8.12-13", specVersion: "0.1", - requirementIds: ["RS-2", "RS-15"], + requirementIds: ["CL-5"], + level: "must", + role: ["client"], + text: "If the token has expired (HTTP 410 Gone with error code `cursor_expired`), the client MUST start again from `beginning`.", + specAnchor: "#list-records", + applicability: "the client performs incremental sync", + observable: "client-capture", + caseIds: ["CL-5/endpoint-cursor-expiry-also-full-resyncs"], + }, + { + clauseId: "8.12-14", + specVersion: "0.1", + requirementIds: ["RS-7"], level: "must", role: ["resource-server"], - text: "The RS MUST NOT re-validate authorization against the current SourceDeclaration. All enforcement constraints are in the resolved grant. Current serving metadata MAY route a granted instance, describe current schemas or query capabilities, or reject a request that cannot currently be served. It MUST NOT widen or reinterpret a stream, instance, field, time field, bound, or resource key.", - specAnchor: "#grant-enforcement", - applicability: "always", + text: "For a record visible at both horizons, eligibility MUST be computed on the grant-authorized projection, not on the unprojected record.", + specAnchor: "#list-records", + applicability: "the target supports `changes_since`", observable: "black-box-http", - caseIds: ["RS-15/client-metadata-projection-closed", "RS-2/field-projection-not-exceeded"], + caseIds: ["RS-7/sync-eligibility-computed-on-the-authorized-projection"], }, { - clauseId: "8.1-3", + clauseId: "8.12-15", specVersion: "0.1", - requirementIds: ["RS-4"], + requirementIds: ["RS-21"], level: "must", role: ["resource-server"], - text: "The RS MUST determine the token's properties (including `pdpp_token_kind`) solely from the introspection response, never from token syntax.", - specAnchor: "#grant-enforcement", + text: "The resource server MUST reject a token used under another grant, subject, scope, or query context as `invalid_cursor`.", + specAnchor: "#list-records", applicability: "always", observable: "black-box-http", - caseIds: ["RS-4/token-kind-not-inferred-from-syntax"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.2-1", + clauseId: "8.12-16", specVersion: "0.1", - requirementIds: ["RS-3", "AS-18"], + requirementIds: ["RS-7", "RS-8"], level: "must", role: ["resource-server"], - text: "For separated AS/RS deployments, the RS MUST authenticate to the AS introspection endpoint as required by RFC 7662.", - specAnchor: "#token-introspection", - applicability: "the deployment separates AS and RS", + text: "If a `changes_since` response is paginated, all pages in that session MUST be anchored to the same session horizon selected on the first page. New writes arriving after page 1 MUST NOT appear in later pages of that same session; they surface in the next session via the terminal-page `next_changes_since`.", + specAnchor: "#list-records", + applicability: "the target supports `changes_since`", observable: "black-box-http", - caseIds: ["AS-18/introspection-requires-authentication"], + caseIds: ["RS-7/paginated-sync-session-anchored-to-one-horizon"], }, { - clauseId: "8.2-2", + clauseId: "8.12-17", specVersion: "0.1", - requirementIds: ["AS-18", "RS-3"], + requirementIds: [], level: "must", - role: ["authorization-server", "resource-server"], - text: "The introspection response MUST contain the complete context needed to enforce the request. The separated RS MUST enforce only from that response and MUST NOT make a second AS lookup while handling the request.", - specAnchor: "#token-introspection", - applicability: "the deployment separates AS and RS", - observable: "review-only", + role: ["resource-server"], + text: "Deletion is the exception: the resource server MUST NOT serve a record on a later page if the record was deleted after page 1.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", caseIds: [], - gapNote: - "The no-second-lookup half is an internal call-count obligation; black-box observation cannot distinguish one lookup from two that produced the same answer.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.2-3", + clauseId: "8.12-18", specVersion: "0.1", - requirementIds: ["RS-4"], + requirementIds: [], + level: "may", + role: ["resource-server"], + text: "**Evidence:** A record in the response MAY carry `evidence` under [Record evidence](#record-evidence), for client and owner tokens alike.", + specAnchor: "#list-records", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.12-19", + specVersion: "0.1", + requirementIds: ["RS-8"], level: "must", role: ["resource-server"], - text: "A resource server that receives a `pdpp_token_kind` value it does not recognize MUST treat the token as unauthorized for all operations defined in this specification.", - specAnchor: "#token-introspection", + text: "The terminal page of a `changes_since` request (i.e., `has_more: false`) MUST include `next_changes_since`.", + specAnchor: "#list-records", + applicability: "the target supports `changes_since`", + observable: "black-box-http", + caseIds: ["RS-8/terminal-page-carries-next-changes-since", "RS-7/changes-since-accepted-on-multi-instance-grant"], + }, + { + clauseId: "8.13-1", + specVersion: "0.1", + requirementIds: [], + level: "may", + role: ["resource-server"], + text: "The record MAY carry `evidence` under the same rules as in list records.", + specAnchor: "#get-a-single-record", applicability: "always", observable: "black-box-http", - caseIds: ["RS-4/unrecognized-token-kind-is-unauthorized"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.2-4", + clauseId: "8.13-2", specVersion: "0.1", - requirementIds: ["RS-3"], + requirementIds: ["RS-23"], level: "must", role: ["resource-server"], - text: "Self-contained JWTs (e.g., signed JWTs) are allowed as an optimization but MUST NOT be the sole revocation mechanism; the RS MUST still be able to check active status through introspection or local equivalent.", - specAnchor: "#token-introspection", - applicability: "the RS issues or accepts self-contained tokens", + text: "It is REQUIRED when the query context covers more than one instance of the stream.", + specAnchor: "#get-a-single-record", + applicability: "always", observable: "black-box-http", - partial: true, - caseIds: ["AS-8/revoked-grant-refused"], - gapNote: - "A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path.", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.3-1", + clauseId: "8.13-3", specVersion: "0.1", - requirementIds: ["RS-12"], + requirementIds: ["RS-23"], level: "must", role: ["resource-server"], - text: "The RS MUST derive the `subject_id` from the introspection response and MUST reject any request attempting to access data outside that subject's scope.", - specAnchor: "#authentication", - applicability: "the target issues owner tokens", + text: "The resource server MUST reject a missing required `instance_id` with 400 `invalid_request` before it looks up the record.", + specAnchor: "#get-a-single-record", + applicability: "always", observable: "black-box-http", - caseIds: ["RS-12/foreign-subject-cannot-read"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.3-2", + clauseId: "8.15-1", specVersion: "0.1", - requirementIds: ["RS-13"], - level: "should", + requirementIds: [], + level: "must", role: ["resource-server"], - text: "An owner holding a valid owner token MAY query their own data using the standard client query endpoints without a client grant. This is the v0.1 self-export mechanism and does not require a separate grant. Conformant Core RS implementations SHOULD support this capability (see Section 9 conformance item 13).", - specAnchor: "#authentication", - applicability: "the target declares `pdpp_self_export_supported`", + text: "The `instance_id` query parameter selects the blob's instance and is REQUIRED on every blob request.", + specAnchor: "#get-a-blob", + applicability: "always", observable: "black-box-http", - caseIds: ["RS-13/self-export-supported"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.4-1", + clauseId: "8.15-2", specVersion: "0.1", - requirementIds: ["RS-16"], + requirementIds: [], level: "must", role: ["resource-server"], - text: "A resource server MUST publish OAuth 2.0 Protected Resource Metadata as defined in RFC 9728.", - specAnchor: "#protected-resource-metadata", + text: "The resource server MUST reject a blob request without it with 400 `invalid_request`.", + specAnchor: "#get-a-blob", applicability: "always", observable: "black-box-http", - caseIds: ["RS-16/protected-resource-metadata-published"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.4-2", + clauseId: "8.15-3", specVersion: "0.1", - requirementIds: ["RS-16"], + requirementIds: [], level: "must", role: ["resource-server"], - text: 'On a 401, the resource server MUST include a `WWW-Authenticate: Bearer` challenge, as RFC 6750 Section 3 requires. It MUST set `error="invalid_token"` when a token was presented and rejected. And it MUST include the `resource_metadata` parameter RFC 9728 Section 5.1 defines, carrying the URL of this document.', - specAnchor: "#protected-resource-metadata", + text: "The `fetch_url` that the resource server injects MUST include `instance_id`.", + specAnchor: "#get-a-blob", applicability: "always", observable: "black-box-http", - caseIds: ["RS-16/401-carries-resource-metadata-challenge", "RS-16/unauthenticated-request-refused"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.4-3", + clauseId: "8.15-4", specVersion: "0.1", - requirementIds: ["RS-16"], - level: "should", + requirementIds: ["RS-1", "RS-2", "RS-18"], + level: "must", + role: ["client"], + text: "The resource server MUST NOT serve a blob unless a record that the requesting token is currently authorized to read references it.", + specAnchor: "#get-a-blob", + applicability: "the target serves blobs", + observable: "black-box-http", + caseIds: ["RS-1/blob-outside-grant-refused", "RS-1/blob-unauthenticated-refused"], + }, + { + clauseId: "8.15-5", + specVersion: "0.1", + requirementIds: ["RS-1"], + level: "must", role: ["resource-server"], - text: "a resource server SHOULD publish it because a consent surface has no other name to display for the resource.", - specAnchor: "#protected-resource-metadata", + text: "**Direct response** MUST include: [...] `Content-Type` (IANA media type) [...] `Content-Length` if known [...] `Cache-Control: private, no-store` [...] `Accept-Ranges: bytes` if range requests are supported", + specAnchor: "#get-a-blob", + applicability: "the target serves blobs", + observable: "black-box-http", + partial: true, + caseIds: ["RS-1/get-blob-bytes"], + gapNote: + "Content-Length is checked against the body only when present: the case cannot tell whether the length was known, so an omitted Content-Length is not a finding. `Accept-Ranges: bytes` is not asserted: the case never issues a Range request, so it cannot tell whether the target supports ranges.", + }, + { + clauseId: "8.15-6", + specVersion: "0.1", + requirementIds: ["RS-1"], + level: "must", + role: ["resource-server"], + text: "**Redirect response** (HTTP 302) MUST include: [...] `Location` header pointing to a signed URL [...] `Cache-Control: no-store`", + specAnchor: "#get-a-blob", + applicability: "the target answers blob fetches with a 302", + observable: "black-box-http", + partial: true, + caseIds: ["RS-1/blob-redirect-response-valid"], + gapNote: + "The header half is checked. The suite deliberately does not follow the signed URL, so the expiry bounds the following paragraph sets on it are not observed.", + }, + { + clauseId: "8.15-7", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The signed URL MUST expire no later than the positive-status cache expiry.", + specAnchor: "#get-a-blob", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.15-8", + specVersion: "0.1", + requirementIds: ["RS-18"], + level: "must", + role: ["resource-server"], + text: "If no result was cached, it MUST expire no later than 60 seconds after the RS sent the introspection request that validated the token.", + specAnchor: "#get-a-blob", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.15-9", + specVersion: "0.1", + requirementIds: ["RS-18"], + level: "must", + role: ["resource-server"], + text: "It MUST also expire no later than the access token or grant expiration when present.", + specAnchor: "#get-a-blob", applicability: "always", observable: "black-box-http", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.4-4", + clauseId: "8.15-10", specVersion: "0.1", - requirementIds: ["RS-16"], + requirementIds: [], level: "must", - role: ["resource-server"], - text: "A PDPP resource server MUST publish `authorization_servers` when that set is enumerable, so a client can reach the issuer without prior configuration. When the set is not enumerable, the resource server MUST omit the member rather than publish a partial list.", - specAnchor: "#protected-resource-metadata", + role: ["authorization-server"], + text: "The AS MUST include `exp` in every positive introspection response for an expiring PDPP token.", + specAnchor: "#get-a-blob", applicability: "always", - observable: "review-only", + observable: "black-box-http", caseIds: [], - gapNote: - "Whether the target's authorization-server set is enumerable is not declared anywhere the suite can read, so the presence or absence of the member cannot be judged; closing this needs a target-config field stating enumerability.", + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.7-1", + clauseId: "8.16-1", specVersion: "0.1", - requirementIds: ["RS-15"], + requirementIds: ["CL-7"], level: "must", - role: ["resource-server"], - text: "Current query, view, relationship, filter, expansion, and aggregation capabilities MUST NOT appear unless that capability is explicitly part of a future frozen grant vocabulary. Current metadata MAY report availability/freshness or reject an unavailable operation, but MUST NOT make the grant appear broader or semantically different than what was issued. A source declaration change made after the grant was issued (e.g., a new field) MUST NOT become visible through this endpoint for that grant.", - specAnchor: "#stream-metadata", + role: ["client"], + text: "Clients MUST treat unrecognized error codes as opaque and fall back to the actual HTTP status code and applicable response headers.", + specAnchor: "#errors", applicability: "always", - observable: "black-box-http", - caseIds: ["RS-15/client-metadata-projection-closed"], + observable: "client-capture", + caseIds: ["CL-7/unknown-error-code-keeps-http-status-authoritative"], }, { - clauseId: "8.8-1", + clauseId: "8.16-2", specVersion: "0.1", - requirementIds: [], + requirementIds: ["CL-7"], level: "may", - role: ["resource-server"], - text: "A resource server MAY attach a `freshness` object to stream listings, stream metadata, and record-list responses.", - specAnchor: "#freshness-metadata", + role: ["client"], + text: "A recognized `error.type` or `error.code` MAY refine PDPP-specific category, presentation, or recovery behavior only when its defined semantics are compatible with the actual status code and headers.", + specAnchor: "#errors", applicability: "always", - observable: "black-box-http", + observable: "client-capture", caseIds: [], }, { - clauseId: "8.9-1", + clauseId: "8.16-3", specVersion: "0.1", - requirementIds: ["CL-3"], + requirementIds: ["CL-7"], level: "must", role: ["client"], - text: "Clients MUST NOT parse or construct cursor tokens.", - specAnchor: "#list-records", + text: "An absent, unknown, malformed, or status-incompatible `type` or `code` is opaque and MUST NOT override the actual status code or relevant headers.", + specAnchor: "#errors", applicability: "always", observable: "client-capture", - caseIds: ["CL-3/forwards-cursors-as-opaque-values"], + caseIds: ["CL-7/status-incompatible-code-does-not-override-status"], }, { - clauseId: "8.9-2", + clauseId: "8.16-4", specVersion: "0.1", - requirementIds: ["RS-9"], + requirementIds: ["CL-7"], level: "must", - role: ["resource-server"], - text: "Owner-token current-capability filters only. Client-token requests MUST reject exact and range forms in v0.1.", - specAnchor: "#list-records", + role: ["client"], + text: "Unknown identifiers MUST NOT cause parse failure.", + specAnchor: "#errors", + applicability: "always", + observable: "client-capture", + caseIds: ["CL-7/unknown-error-identifiers-do-not-break-parser"], + }, + { + clauseId: "8.16-5", + specVersion: "0.1", + requirementIds: ["CL-7"], + level: "may", + role: ["client"], + text: "Clients MAY retain unknown identifiers for diagnostics, subject to local size limits, safe rendering/escaping, and privacy policy.", + specAnchor: "#errors", applicability: "always", + observable: "client-capture", + caseIds: [], + }, + { + clauseId: "8.16-6", + specVersion: "0.1", + requirementIds: ["RS-19"], + level: "may", + role: ["resource-server"], + text: "When authenticated context establishes the cause, the structured error MAY use `grant_expired` or `grant_revoked`; those codes have HTTP status 401 and type `authentication_error`.", + specAnchor: "#errors", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["RS-9/client-token-exact-filter-rejected"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-3", + clauseId: "8.17-1", specVersion: "0.1", - requirementIds: ["RS-9"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "Client-token records requests MUST reject `view`; clients use explicit `fields` or the field projection already frozen into the grant. Mutually exclusive with `fields`.", - specAnchor: "#list-records", + text: "An RS claiming conformance to this specification MUST support `0.1` at its advertised `pdpp_core_query_base`.", + specAnchor: "#api-versioning", applicability: "always", observable: "black-box-http", - partial: true, - caseIds: ["RS-9/client-token-view-rejected"], - gapNote: - "The client-token rejection half is covered. The sentence's second half — `view` and `fields` being mutually exclusive — is an owner-token concern (a client token must reject `view` outright, so the pair can never both be honoured there) and is tracked at 6.8-1, which needs a view-declaring target.", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-4", + clauseId: "8.17-2", specVersion: "0.1", - requirementIds: ["RS-9"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "Client-token requests MUST reject this parameter in v0.1.", - specAnchor: "#list-records", - applicability: "always", - observable: "black-box-http", - caseIds: ["RS-9/client-token-expand-rejected"], + text: "A companion binding that uses Core RS operations MUST preserve their selected Core contract.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, { - clauseId: "8.9-5", + clauseId: "8.17-3", specVersion: "0.1", - requirementIds: ["RS-9", "RS-10"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "Client-token requests MUST reject this parameter in v0.1.", - specAnchor: "#list-records", + text: "A sender MUST send at most one `PDPP-Version` field line in the header section and MUST NOT send this field in trailers.", + specAnchor: "#api-versioning", applicability: "always", observable: "black-box-http", - caseIds: ["RS-9/client-token-expand-limit-rejected"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-6", + clauseId: "8.17-4", specVersion: "0.1", - requirementIds: ["RS-14"], + requirementIds: ["RS-11"], level: "must", - role: ["resource-server", "source-declaration"], - text: "Advanced stream-specific query power MUST be declared in stream metadata under `query`.", - specAnchor: "#list-records", - applicability: "the target supports advanced query capability", + role: ["resource-server"], + text: "A receiver MUST compare identifiers exactly after HTTP field parsing has removed surrounding whitespace.", + specAnchor: "#api-versioning", + applicability: "always", observable: "black-box-http", - caseIds: ["RS-14/owner-metadata-full-current-document"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-7", + clauseId: "8.17-5", specVersion: "0.1", - requirementIds: ["RS-10"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "Unknown query parameters and unsupported query shapes MUST be rejected with HTTP 400 and MUST NOT be silently ignored.", - specAnchor: "#list-records", + text: "A receiver MUST NOT parse the value as a number, a semantic-version range, or a date.", + specAnchor: "#api-versioning", applicability: "always", observable: "black-box-http", - caseIds: [ - "RS-10/unknown-parameter-rejected", - "RS-10/unsupported-bracketed-shape-rejected", - "RS-10/owner-filter-unknown-field-rejected", - "RS-10/owner-expand-undeclared-relation-rejected", - "RS-10/malformed-limit-rejected", - ], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-8", + clauseId: "8.17-6", specVersion: "0.1", - requirementIds: ["RS-10"], + requirementIds: ["RS-11"], level: "must", - role: ["resource-server", "client"], - text: "clients SHOULD branch on `code`, not on message text. [...] Warnings are not errors and MUST NOT change the HTTP status.", - specAnchor: "#list-records", + role: ["client"], + text: "A conformant client MUST send the exact identifier of a contract it implements on every Core RS operation, including retries and subsequent pages.", + specAnchor: "#api-versioning", applicability: "always", - observable: "black-box-http", - caseIds: ["RS-10/oversized-limit-clamped-with-warning"], + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-9", + clauseId: "8.17-7", specVersion: "0.1", - requirementIds: ["RS-9"], + requirementIds: ["AS-17", "RS-11"], level: "must", - role: ["resource-server"], - text: "Client-token requests that contain any exact or range `filter[...]` parameter MUST be rejected with HTTP 400 `invalid_request` before the RS consults current SourceDeclaration or serving metadata. This rejection applies regardless of whether the field or operator would otherwise be declared.", - specAnchor: "#list-records", + role: ["resource-server", "authorization-server"], + text: "If the field is absent, an RS implementing this contract MUST select `0.1`. [...] If the field is present and syntactically valid, the RS MUST either select that exact supported identifier or reject the request with HTTP 400 and error code `unsupported_version`.", + specAnchor: "#api-versioning", applicability: "always", observable: "black-box-http", partial: true, - caseIds: ["RS-9/client-token-exact-filter-rejected"], + caseIds: ["RS-11/unsupported-version-rejected"], gapNote: - 'The status and code are tested; the ordering half ("before the RS consults ... metadata") is not, and black-box observation cannot establish it.', + "The unsupported-version half is tested on the resource server. No case asserts that an absent header selects `0.1`, or that the response carries exactly one `PDPP-Version` field naming the selected identifier. AS-17/unsupported-version-rejected is no longer mapped here: the locked text makes `PDPP-Version` negotiation a Resource Server requirement and retires AS item 17.", }, { - clauseId: "8.9-10", + clauseId: "8.17-8", specVersion: "0.1", - requirementIds: ["RS-9"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "Client-token requests that contain `expand[]` or `expand_limit[...]` MUST be rejected with HTTP 400 `invalid_request` before the RS consults current SourceDeclaration or serving metadata.", - specAnchor: "#list-records", + text: "It MUST NOT change that selection merely because a later document or another HTTP contract becomes available.", + specAnchor: "#api-versioning", applicability: "always", observable: "black-box-http", - caseIds: ["RS-9/client-token-expand-rejected"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-11", + clauseId: "8.17-9", specVersion: "0.1", - requirementIds: ["RS-6", "CL-3"], + requirementIds: ["RS-11"], level: "must", - role: ["resource-server", "client"], - text: "a client MUST follow a `next_cursor` with the same `order` value that produced it. To change direction, the client MUST restart pagination without a cursor. Resource servers MUST reject order-mismatched page cursors as `invalid_cursor`.", - specAnchor: "#list-records", + role: ["resource-server"], + text: "It MUST NOT substitute another identifier or retry the operation internally under another contract.", + specAnchor: "#api-versioning", applicability: "always", observable: "black-box-http", - partial: true, - caseIds: ["RS-6/order-mismatched-cursor-rejected"], - gapNote: - "The resource-server half is covered. The client half — that a client follows a `next_cursor` with the same `order` and restarts pagination to change direction — binds the client's outgoing requests and stays `client-capture`: the adapter has no reverse channel onto a client under test.", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-12", + clauseId: "8.17-10", specVersion: "0.1", - requirementIds: ["CL-5"], + requirementIds: ["RS-11"], level: "must", - role: ["client"], - text: "If the cursor has expired (HTTP 410 Gone with error code `cursor_expired`), the client MUST perform a full re-sync.", - specAnchor: "#list-records", - applicability: "the client performs incremental sync", - observable: "client-capture", - caseIds: ["CL-5/endpoint-cursor-expiry-also-full-resyncs"], + role: ["resource-server"], + text: "When version validation finds an invalid field, the RS MUST reject the request with HTTP 400 and error code `invalid_request`.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-13", + clauseId: "8.17-11", specVersion: "0.1", - requirementIds: ["RS-7"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "Eligibility for `changes_since` MUST be computed on the grant-authorized projection, not on the unprojected record. Returning a record whose authorized projection is unchanged is a protocol violation because it leaks that hidden fields changed.", - specAnchor: "#list-records", - applicability: "the target supports `changes_since`", + text: "For a version-field error, the structured error's `param` MUST be `PDPP-Version`.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["RS-7/sync-eligibility-computed-on-the-authorized-projection"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-14", + clauseId: "8.17-12", specVersion: "0.1", - requirementIds: ["RS-7", "RS-8"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "If a `changes_since` response is paginated, all pages in that session MUST be anchored to the same session horizon selected on the first page. New writes arriving after page 1 MUST NOT appear in later pages of that same session; they surface in the next session via the terminal-page `next_changes_since`.", - specAnchor: "#list-records", - applicability: "the target supports `changes_since`", + text: "After selecting a contract, the RS MUST include exactly one `PDPP-Version` response field containing the selected identifier on every response it generates for that operation, including errors, `HEAD` responses, and blob redirects.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["RS-7/paginated-sync-session-anchored-to-one-horizon"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-15", + clauseId: "8.17-13", specVersion: "0.1", - requirementIds: ["RS-8"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "The terminal page of a `changes_since` request (i.e., `has_more: false`) MUST include `next_changes_since`.", - specAnchor: "#list-records", - applicability: "the target supports `changes_since`", + text: "When version validation fails without selecting a contract, the RS MUST omit that response field.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["RS-8/terminal-page-carries-next-changes-since", "RS-7/changes-since-accepted-on-multi-instance-grant"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.9-16", + clauseId: "8.17-14", specVersion: "0.1", - requirementIds: ["CL-4"], + requirementIds: ["RS-11"], + level: "must", + role: ["resource-server"], + text: "It MUST NOT echo an unsupported requested identifier as though it had been selected.", + specAnchor: "#api-versioning", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.17-15", + specVersion: "0.1", + requirementIds: ["RS-11", "CL-32"], level: "must", role: ["client"], - text: "then store `next_changes_since` from the terminal page for the next session.", - specAnchor: "#list-records", - applicability: "the client performs incremental sync", + text: "Before using a successful Core RS response, a client MUST verify that the response contains exactly one valid `PDPP-Version` value equal to its requested identifier.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", observable: "client-capture", - caseIds: ["CL-4/stores-terminal-next-changes-since"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.12-1", + clauseId: "8.17-16", specVersion: "0.1", - requirementIds: ["RS-1", "RS-2"], + requirementIds: ["RS-11"], level: "must", - role: ["client"], - text: "A `blob_id` alone does not grant access. The client MUST have discovered the blob through an authorized record.", - specAnchor: "#get-a-blob", - applicability: "the target serves blobs", + role: ["resource-server"], + text: "If that check fails, it MUST treat the response as a protocol failure and MUST NOT use its data.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", observable: "black-box-http", - caseIds: ["RS-1/blob-outside-grant-refused", "RS-1/blob-unauthenticated-refused"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.12-2", + clauseId: "8.17-17", specVersion: "0.1", - requirementIds: ["RS-1"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "**Direct response** MUST include: [...] `Content-Type` (IANA media type) [...] `Content-Length` if known [...] `Cache-Control: private, no-store` [...] `Accept-Ranges: bytes` if range requests are supported", - specAnchor: "#get-a-blob", - applicability: "the target serves blobs", + text: "An RS MUST apply the selected contract without widening or reinterpreting the resolved authorization context.", + specAnchor: "#api-versioning", + applicability: "always", observable: "black-box-http", - caseIds: ["RS-1/get-blob-bytes"], - partial: true, - gapNote: - "Content-Length is checked against the body only when present: the case cannot tell whether the length was known, so an omitted Content-Length is not a finding. `Accept-Ranges: bytes` is not asserted: the case never issues a Range request, so it cannot tell whether the target supports ranges.", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.12-3", + clauseId: "8.17-18", specVersion: "0.1", - requirementIds: ["RS-1"], + requirementIds: ["RS-11"], level: "must", role: ["resource-server"], - text: "**Redirect response** (HTTP 302) MUST include: [...] `Location` header pointing to a short-lived signed URL (valid for at least 60 seconds) [...] `Cache-Control: no-store`", - specAnchor: "#get-a-blob", - applicability: "the target answers blob fetches with a 302", + text: "An RS MUST reject use under another contract with HTTP 400 and error code `invalid_cursor`.", + specAnchor: "#api-versioning", + applicability: "always", observable: "black-box-http", - partial: true, - caseIds: ["RS-1/blob-redirect-response-valid"], - gapNote: - 'The header half is checked; the "valid for at least 60 seconds" half is not, because the suite deliberately does not follow the signed URL and so cannot observe its expiry.', + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.13-1", + clauseId: "8.17-19", specVersion: "0.1", - requirementIds: ["CL-7"], + requirementIds: ["RS-11"], level: "must", role: ["client"], - text: "Clients MUST treat unrecognized error codes as opaque and fall back to the actual HTTP status code and applicable response headers.", - specAnchor: "#errors", + text: "A client MUST keep the same identifier throughout a pagination or synchronization session.", + specAnchor: "#api-versioning", applicability: "always", observable: "client-capture", - caseIds: ["CL-7/unknown-error-code-keeps-http-status-authoritative"], + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "8.17-20", + specVersion: "0.1", + requirementIds: ["RS-11"], + level: "must", + role: ["resource-server"], + text: "If a response is reusable by an HTTP cache and its representation depends on `PDPP-Version`, the RS MUST include `PDPP-Version` in `Vary`, or use `Vary: *`.", + specAnchor: "#api-versioning", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.13-2", + clauseId: "8.17-21", specVersion: "0.1", - requirementIds: ["CL-7"], - level: "may", + requirementIds: ["RS-11"], + level: "must", role: ["client"], - text: "A recognized `error.type` or `error.code` MAY refine PDPP-specific category, presentation, or recovery behavior only when its defined semantics are compatible with the actual status code and headers.", - specAnchor: "#errors", + text: "A deployment supporting browser cross-origin clients MUST permit `PDPP-Version` on its authorized cross-origin Core requests and expose `PDPP-Version` to those clients, without broadening which origins or credentials it authorizes.", + specAnchor: "#api-versioning", applicability: "always", observable: "client-capture", caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.13-3", + clauseId: "8.17-22", specVersion: "0.1", - requirementIds: ["CL-7"], + requirementIds: ["RS-11"], level: "must", - role: ["client"], - text: "An absent, unknown, malformed, or status-incompatible `type` or `code` is opaque and MUST NOT override the actual status code or relevant headers.", - specAnchor: "#errors", + role: ["resource-server"], + text: "An incompatible HTTP contract MUST have a distinct identifier and MUST NOT be served as `0.1`.", + specAnchor: "#api-versioning", applicability: "always", - observable: "client-capture", - caseIds: ["CL-7/status-incompatible-code-does-not-override-status"], + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { - clauseId: "8.13-4", + clauseId: "8.17-23", specVersion: "0.1", - requirementIds: ["CL-7"], + requirementIds: ["RS-11"], level: "must", - role: ["client"], - text: "Unknown identifiers MUST NOT cause parse failure.", - specAnchor: "#errors", + role: ["resource-server"], + text: "A companion profile MUST NOT redefine `0.1` or alter the meaning of a Core member under that identifier.", + specAnchor: "#api-versioning", applicability: "always", - observable: "client-capture", - caseIds: ["CL-7/unknown-error-identifiers-do-not-break-parser"], + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", }, +]; + +// Clause inventory for Core section 9 (9. Conformance). +const SECTION_9: readonly ClauseEntry[] = [ { - clauseId: "8.13-5", + clauseId: "9.4-1", specVersion: "0.1", - requirementIds: ["CL-7"], - level: "may", - role: ["client"], - text: "Clients MAY retain unknown identifiers for diagnostics, subject to local size limits, safe rendering/escaping, and privacy policy.", - specAnchor: "#errors", + requirementIds: ["AS-55"], + level: "must", + role: ["authorization-server"], + text: "A conformance report MUST classify each assertion as protocol-visible or adapter-observed.", + specAnchor: "#held-data-cases", applicability: "always", - observable: "client-capture", + observable: "review-only", caseIds: [], + gapNote: + "The obligation binds the conformance report an implementer publishes about its own authorization-privacy tests, not a request the suite sends. No case can observe it.", }, { - clauseId: "8.14-1", + clauseId: "9.5-1", specVersion: "0.1", - requirementIds: ["AS-17", "RS-11"], + requirementIds: ["AS-57"], level: "must", - role: ["resource-server", "authorization-server"], - text: "If the `PDPP-Version` header is absent, the RS uses the current stable version and returns the selected version in the `PDPP-Version` response header. If the requested version is not supported, the RS returns 400 `unsupported_version`.", - specAnchor: "#api-versioning", + role: ["authorization-server"], + text: "A report MUST identify which assertions are protocol-visible and which are adapter-observed.", + specAnchor: "#independent-recipient-cases", applicability: "always", - observable: "black-box-http", - partial: true, - caseIds: ["RS-11/unsupported-version-rejected", "AS-17/unsupported-version-rejected"], + observable: "review-only", + caseIds: [], gapNote: - "Carries no RFC 2119 keyword; its MUST level is Section 9 AS item 17 and RS item 11. The unsupported-version half is tested on both roles; the absent-header half — that the response echoes the selected version in a `PDPP-Version` response header — is not asserted by any case, and needs only an unwritten case, not a new hook.", + "The obligation binds the conformance report an implementer publishes, and the differential authorization-privacy case it names needs a matched owner refusal the black-box adapter cannot arrange. No case in this suite version exercises it.", }, ]; -// --------------------------------------------------------------------------- -// Section 10: Security Considerations -// --------------------------------------------------------------------------- - +// Clause inventory for Core section 10 (10. Security Considerations). const SECTION_10: readonly ClauseEntry[] = [ { clauseId: "10.1-1", @@ -1719,29 +9025,43 @@ const SECTION_10: readonly ClauseEntry[] = [ partial: true, caseIds: ["AS-18/introspection-requires-authentication"], gapNote: - "The authentication half is tested; the no-second-lookup half is an internal call-count obligation, as at 8.2-2.", + "The authentication half is tested; the no-second-lookup half is an internal call-count obligation, as at 8.3-8.", }, { clauseId: "10.2-2", specVersion: "0.1", - requirementIds: ["RS-3", "AS-8"], + requirementIds: ["AS-8"], level: "must", - role: ["resource-server"], - text: "Positive introspection results MUST NOT be cached longer than `min(token_exp, 60 seconds)`. This bounds the propagation window for revocation.", + role: ["authorization-server", "resource-server"], + text: "Self-contained JWTs are allowed as an optimization but MUST NOT be the sole revocation mechanism.", specAnchor: "#token-security", - applicability: "the deployment separates AS and RS", + applicability: "always", observable: "black-box-http", - caseIds: [], + partial: true, + caseIds: ["AS-8/revoked-grant-refused"], gapNote: - "Restates 8.1-1 as a revocation-propagation bound; needs a separated AS/RS target and a timed revocation observation, neither of which the current topologies support.", + "A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path.", }, { clauseId: "10.2-3", specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["resource-server"], + text: "The RS MUST NOT use a result after that lifetime ends, cached or not.", + specAnchor: "#token-security", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.2-4", + specVersion: "0.1", requirementIds: ["AS-9", "AS-20"], level: "must", role: ["authorization-server"], - text: "An access token issued with or from a refresh-token family MUST be linked to that family and MUST have a short, token-specific expiration no later than the family or grant expiration. A token response MUST derive `expires_in` from the access token's persisted expiration. It MUST omit `expires_in` when the access token has no expiration. An RFC 7662 response MUST likewise omit `exp` when no expiration exists.", + text: "An access token issued with or from a refresh-token family MUST be linked to that family and MUST have a short, token-specific expiration no later than the family or grant expiration. A token response MUST derive `expires_in` from the access token's persisted expiration. It MUST omit `expires_in` when the access token has no expiration, which Section 7 does not permit for a `single_use` grant. An RFC 7662 response MUST likewise omit `exp` when no expiration exists.", specAnchor: "#token-security", applicability: "the AS issues refresh tokens", observable: "black-box-http", @@ -1751,7 +9071,7 @@ const SECTION_10: readonly ClauseEntry[] = [ "Family linkage is tested through reuse revocation; the `expires_in`/`exp` derivation and omission rules are not, and would need a target that can issue a non-expiring access token.", }, { - clauseId: "10.2-4", + clauseId: "10.2-5", specVersion: "0.1", requirementIds: ["AS-9"], level: "must", @@ -1763,7 +9083,7 @@ const SECTION_10: readonly ClauseEntry[] = [ caseIds: ["AS-9/token-response-forbids-caching"], }, { - clauseId: "10.2-5", + clauseId: "10.2-6", specVersion: "0.1", requirementIds: ["AS-19"], level: "must", @@ -1775,24 +9095,27 @@ const SECTION_10: readonly ClauseEntry[] = [ caseIds: ["AS-19/authorization-code-redemption-is-not-replayable"], }, { - clauseId: "10.2-6", + clauseId: "10.2-7", specVersion: "0.1", requirementIds: ["AS-20"], level: "must", role: ["authorization-server"], - text: "each token MUST belong to a family and MUST rotate after successful use. The AS MUST atomically supersede the presented token and issue one active successor. Reuse of any superseded token, including a retry after a lost successful response, MUST revoke the token family and every access token linked to that family, return `invalid_grant`, and require fresh authorization.", + text: "each token MUST belong to a family and MUST rotate after successful use. Refresh issuance and family revocation MUST be serialized, or use equivalent atomic checks, so that a refresh cannot issue a usable token after the family is revoked. The AS MUST atomically supersede the presented token and issue one active successor. Reuse of any superseded token, including a retry after a lost successful response, MUST revoke the token family and every access token linked to that family, return `invalid_grant`, and require fresh authorization.", specAnchor: "#token-security", applicability: "the AS issues refresh tokens", observable: "black-box-http", + partial: true, caseIds: ["AS-20/refresh-reuse-revokes-the-family"], + gapNote: + "Rotation and replay revocation are tested. That refresh issuance and family revocation are serialized is an internal ordering property that no black-box case can observe.", }, { - clauseId: "10.2-7", + clauseId: "10.2-8", specVersion: "0.1", requirementIds: ["AS-20", "AS-8"], level: "must", role: ["authorization-server"], - text: "Introspection MUST report every family-linked access token inactive after the replay is detected. An AS MUST NOT issue refresh tokens for a `single_use` grant. It MUST NOT issue one for a grant package unless every child grant is `continuous`.", + text: "Introspection MUST report every family-linked access token inactive after the replay is detected. [...] an AS MUST NOT issue a refresh token unless at least one covered grant is `continuous`.", specAnchor: "#token-security", applicability: "the AS issues refresh tokens", observable: "black-box-http", @@ -1802,7 +9125,7 @@ const SECTION_10: readonly ClauseEntry[] = [ "Introspection of the family-linked token is checked only where the target offers an introspection route; otherwise the case judges the resource read alone, which is not the introspection interface this clause names. The prohibition on issuing refresh tokens for a `single_use` grant is not covered, and needs a target supporting both `single_use` and refresh tokens at once.", }, { - clauseId: "10.2-8", + clauseId: "10.2-9", specVersion: "0.1", requirementIds: ["AS-21"], level: "must", @@ -1816,7 +9139,7 @@ const SECTION_10: readonly ClauseEntry[] = [ "An upgrade-path obligation over pre-existing persisted state: the suite would have to plant legacy tokens in the target's store before boot, which the adapter deliberately cannot do.", }, { - clauseId: "10.2-9", + clauseId: "10.2-10", specVersion: "0.1", requirementIds: [], level: "should", @@ -1830,19 +9153,17 @@ const SECTION_10: readonly ClauseEntry[] = [ "The spec marks this subsection non-normative guidance; recorded so the scan's keyword hit is accounted for rather than appearing as an untracked clause.", }, { - clauseId: "10.2-10", + clauseId: "10.2-11", specVersion: "0.1", - requirementIds: ["AS-8"], + requirementIds: [], level: "must", - role: ["authorization-server", "resource-server"], - text: "Self-contained JWTs are allowed as an optimization but MUST NOT be the sole revocation mechanism.", + role: ["authorization-server"], + text: "A future version that makes sender-constrained tokens mandatory is expected to make finite access-token expiry a MUST at the same time.", specAnchor: "#token-security", - applicability: "always", + applicability: "the condition the clause states holds", observable: "black-box-http", - partial: true, - caseIds: ["AS-8/revoked-grant-refused"], - gapNote: - "A case shows a revoked grant is refused within the 60-second propagation ceiling; it cannot tell a JWT-only design that happens to expire quickly from a design with a real revocation path.", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", }, { clauseId: "10.3-1", @@ -1897,33 +9218,232 @@ const SECTION_10: readonly ClauseEntry[] = [ caseIds: [], }, { - clauseId: "10.7-1", + clauseId: "10.6-1", + specVersion: "0.1", + requirementIds: [], + level: "should", + role: ["client"], + text: "A client that gives records to an automated agent SHOULD treat them as untrusted data.", + specAnchor: "#record-content-and-automated-agents", + applicability: "the condition the clause states holds", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-1", + specVersion: "0.1", + requirementIds: ["AS-25"], + level: "must", + role: ["authorization-server"], + text: "When a client revokes a refresh token, the AS MUST end every grant that the token's family covers, and every access token issued under those grants.", + specAnchor: "#revocation", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-2", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "When a client revokes an access token, the AS MUST end that token.", + specAnchor: "#revocation", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-3", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST also end the token's grants, but only when no refresh-token family covers them.", + specAnchor: "#revocation", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-4", + specVersion: "0.1", + requirementIds: ["AS-26"], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT revoke the shared credential or end the other grants.", + specAnchor: "#revocation", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-5", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "An AS that supports it MUST advertise `revocation_endpoint` in its [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) metadata.", + specAnchor: "#revocation", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-6", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Recording the disposition MUST NOT change the `read_state` or `ended_at` of a grant that had already expired or been revoked, and MUST NOT change the clocks of any existing instruction.", + specAnchor: "#revocation", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-7", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "An AS that supports RFC 7009 revocation MUST accept the parameter and MUST reject any other value with `invalid_request`.", + specAnchor: "#revocation", + applicability: "the condition the clause states holds", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-8", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Absence of that time MUST NOT make the AS record request arrival as first receipt.", + specAnchor: "#revocation", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-9", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "Before it applies any effect of a revocation request that carries a recorded disposition, the AS MUST durably retain the request's complete grant scope, including grants already ended or consumed, the disposition, any `reported_at`, and credential binding.", + specAnchor: "#revocation", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "10.8-10", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "The AS MUST record a conveyed disposition before, or atomically with, the refresh-family revocation that carries it.", + specAnchor: "#revocation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation concerns internal state or ordering that a black-box request sequence cannot distinguish from a non-conforming implementation.", + }, + { + clauseId: "10.8-11", + specVersion: "0.1", + requirementIds: [], + level: "must", + role: ["authorization-server"], + text: "It MUST NOT authorize a new scope, disposition, or report with that credential.", + specAnchor: "#revocation", + applicability: "always", + observable: "black-box-http", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-12", specVersion: "0.1", requirementIds: ["AS-8"], level: "must", role: ["authorization-server"], - text: "The AS MUST reflect revocation immediately in introspection responses (`active: false`).", + text: "The AS MUST reflect revocation immediately in introspection: the revoked grant is inactive at once.", specAnchor: "#revocation", applicability: "always", observable: "black-box-http", caseIds: ["AS-8/revoked-token-introspects-inactive"], }, { - clauseId: "10.7-2", + clauseId: "10.8-13", + specVersion: "0.1", + requirementIds: ["RS-19", "CL-9"], + level: "must", + role: ["client"], + text: "On a 401 response with Bearer error `invalid_token`, the client MUST stop using that access token.", + specAnchor: "#revocation", + applicability: "always", + observable: "client-capture", + caseIds: [], + gapNote: "No case in this suite version exercises this clause.", + }, + { + clauseId: "10.8-14", specVersion: "0.1", requirementIds: ["CL-2"], level: "must", role: ["client"], - text: "Upon receiving any 403 `grant_revoked` response, the client MUST stop further requests against that grant.", + text: "On a `grant_revoked` error, the client MUST stop further requests against that grant.", specAnchor: "#revocation", applicability: "always", observable: "client-capture", caseIds: ["CL-2/grant-revocation-stops-further-requests"], }, + { + clauseId: "10.8-15", + specVersion: "0.1", + requirementIds: ["CL-9"], + level: "must", + role: ["client"], + text: "A client MUST NOT treat an inactive token as proof of revocation: the token may instead have expired or lost its refresh-token family.", + specAnchor: "#revocation", + applicability: "always", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds a profile, a registry or an evidence mechanism that Core leaves to a companion document, so no Core wire behavior exists to observe.", + }, + { + clauseId: "10.8-16", + specVersion: "0.1", + requirementIds: ["CL-9"], + level: "must", + role: ["client"], + text: "Before further use of held data of that grant, the client MUST start a grant lifecycle query, which is an attempt and not a successful answer ([Positive assessment and reconciliation](#reconciliation)).", + specAnchor: "#revocation", + applicability: "the condition the clause states holds", + observable: "review-only", + caseIds: [], + gapNote: + "The obligation binds how a party holds, uses or disposes of data after delivery, or its internal handling of lifecycle state, which neither the target adapter nor the client fixture observes.", + }, ]; -// Clause inventory for Core section 12 (Scope and Boundaries) and Appendix A. -const SECTION_12_AND_APPENDIX: readonly ClauseEntry[] = [ +// Clause inventory for Core section 12 (12. Scope and Boundaries). +const SECTION_12: readonly ClauseEntry[] = [ { clauseId: "12.3-1", specVersion: "0.1", @@ -1944,7 +9464,7 @@ const SECTION_12_AND_APPENDIX: readonly ClauseEntry[] = [ requirementIds: [], level: "must", role: ["authorization-server", "resource-server"], - text: "Implementations MUST NOT change the meaning of Core-granted access via extensions: a grant issued under this specification authorizes exactly what Sections 7 and 8 define, regardless of what additional capabilities a deployment offers.", + text: "Implementations MUST NOT change the meaning of Core-granted access via extensions: a grant issued under this specification authorizes exactly what Sections 6, 7, and 8 define, including any processing permission it carries, regardless of what additional capabilities a deployment offers.", specAnchor: "#extensions", applicability: "the deployment offers capabilities beyond Core", observable: "review-only", @@ -1972,7 +9492,7 @@ const SECTION_12_AND_APPENDIX: readonly ClauseEntry[] = [ requirementIds: [], level: "must", role: ["client"], - text: "Unrecognized declared capabilities MUST be ignorable by clients.", + text: "Unrecognized declared capabilities MUST be ignorable by clients, as unknown response members are ([Response forward compatibility](#response-forward-compatibility)).", specAnchor: "#extensions", applicability: "always", observable: "client-capture", @@ -1980,29 +9500,23 @@ const SECTION_12_AND_APPENDIX: readonly ClauseEntry[] = [ gapNote: "Needs a client fixture response that advertises an unknown capability; no Section 9 client item names it. Missing case: the client under test completes its flow against metadata carrying an undeclared capability.", }, +]; + +// Clause inventory for Core appendix A (Appendix A: PDPP Purposes). +const SECTION_A: readonly ClauseEntry[] = [ { clauseId: "A.0-1", specVersion: "0.1", requirementIds: ["AS-6"], level: "must", role: ["authorization-server"], - text: "Implementations MUST treat unrecognized purpose URIs as opaque identifiers and MUST NOT reject requests solely because a purpose code is unrecognized.", - specAnchor: "#appendix-a-purpose-code-registry", + text: "Implementations MUST treat unrecognized purpose URIs as opaque identifiers and MUST NOT reject a complete detail solely because its purpose code is unrecognized.", + specAnchor: "#appendix-a-pdpp-purposes", applicability: "always", observable: "black-box-http", - caseIds: ["AS-6/unregistered-purpose-not-rejected"], - }, - { - clauseId: "A.0-2", - specVersion: "0.1", - requirementIds: ["AS-14"], - level: "must", - role: ["authorization-server"], - text: "The AS MUST obtain explicit affirmative user consent before issuing any grant with this purpose code.", - specAnchor: "#appendix-a-purpose-code-registry", - applicability: "the AS issues grants carrying the ai_training purpose code", - observable: "review-only", - caseIds: ["AS-14/explicit-consent-required-for-ai-training"], + caseIds: [], + gapNote: + "AS-6/unregistered-purpose-not-rejected submits a detail with no `purpose_description`. The locked text requires the AS to reject such a detail for a code outside Appendix A, so the case does not probe the MUST NOT, which concerns a complete detail. No case submits a complete detail with an unrecognized purpose code.", }, ]; @@ -2014,8 +9528,10 @@ export const CLAUSE_MATRIX: readonly ClauseEntry[] = [ ...SECTION_6, ...SECTION_7, ...SECTION_8, + ...SECTION_9, ...SECTION_10, - ...SECTION_12_AND_APPENDIX, + ...SECTION_12, + ...SECTION_A, ]; const BY_CLAUSE_ID = new Map(CLAUSE_MATRIX.map((c) => [c.clauseId, c])); diff --git a/packages/conformance-suite/targets/reference-implementation.expected.json b/packages/conformance-suite/targets/reference-implementation.expected.json index 8ca17da46..6428345f0 100644 --- a/packages/conformance-suite/targets/reference-implementation.expected.json +++ b/packages/conformance-suite/targets/reference-implementation.expected.json @@ -1,7 +1,7 @@ { "comment": "Case outcomes and MUST clause results expected from this target at its pinned commit. Regenerate with scripts/check-expectations.ts --update and review the diff: every changed line is a behaviour change.", "target": "pdpp-reference-implementation", - "specSha256": "80a38c963f777541ab10325e292291255c65df9211499b39e7f8eaf0083ad6b2", + "specSha256": "176e0a2c1fd6a0e8cd5d13185d11172bb2ed315de256b8bf06505ef9b8bd6297", "cases": { "AS-10/single-use-grant-consumed": "pass", "AS-12/view-within-declared-schema": "unsupported", @@ -100,18 +100,33 @@ }, "must": { "10.2-1": "partial", - "10.2-10": "partial", - "10.2-2": "not-tested", - "10.2-3": "not-applicable", - "10.2-4": "pass", + "10.2-11": "not-tested", + "10.2-2": "partial", + "10.2-3": "not-tested", + "10.2-4": "not-applicable", "10.2-5": "pass", - "10.2-6": "not-applicable", + "10.2-6": "pass", "10.2-7": "not-applicable", - "10.2-8": "untestable", + "10.2-8": "not-applicable", + "10.2-9": "untestable", "10.3-1": "untestable", "10.4-1": "untestable", - "10.7-1": "pass", - "10.7-2": "not-applicable", + "10.8-1": "not-tested", + "10.8-10": "untestable", + "10.8-11": "not-tested", + "10.8-12": "pass", + "10.8-13": "not-tested", + "10.8-14": "not-applicable", + "10.8-15": "untestable", + "10.8-16": "untestable", + "10.8-2": "not-tested", + "10.8-3": "not-tested", + "10.8-4": "not-tested", + "10.8-5": "not-tested", + "10.8-6": "not-tested", + "10.8-7": "not-tested", + "10.8-8": "not-tested", + "10.8-9": "untestable", "12.3-1": "untestable", "12.4-1": "untestable", "12.4-2": "not-tested", @@ -119,104 +134,637 @@ "2.2-1": "partial", "2.2-2": "untestable", "2.2-3": "partial", + "2.5-1": "untestable", "3.0-1": "untestable", "3.0-2": "untestable", - "4.3-2": "skip", - "4.3-3": "not-applicable", - "4.3-4": "not-applicable", - "4.3-5": "fail", - "4.3-6": "pass", - "4.5-1": "not-applicable", - "4.5-2": "untestable", - "4.8-1": "partial", - "5.2-1": "untestable", - "5.2-2": "skip", - "5.2-3": "skip", - "5.2-4": "skip", - "5.2-5": "skip", - "5.3-4": "skip", - "5.4-1": "not-tested", - "5.4-2": "untestable", - "5.6-2": "not-applicable", - "5.6-2a": "not-applicable", - "5.6-3": "not-applicable", - "5.8-1": "not-tested", - "5.8-2": "skip", - "5.8-3": "untestable", - "5.8-4": "fail", - "5.8-5": "not-tested", - "5.8-6": "untestable", - "5.8-7": "untestable", - "5.8-8": "pass", - "6.1-10": "not-tested", + "4.10-1": "partial", + "4.11-1": "not-tested", + "4.11-2": "not-tested", + "4.11-3": "not-tested", + "4.11-4": "not-tested", + "4.11-5": "untestable", + "4.11-6": "not-tested", + "4.11-7": "not-tested", + "4.11-8": "not-tested", + "4.11-9": "not-tested", + "4.2-1": "untestable", + "4.2-2": "untestable", + "4.2-3": "not-tested", + "4.2-4": "not-tested", + "4.3-1": "untestable", + "4.3-2": "partial", + "4.3-3": "not-tested", + "4.3-4": "not-tested", + "4.3-5": "untestable", + "4.3-6": "not-tested", + "4.3-7": "not-tested", + "4.3-8": "not-tested", + "4.4-1": "not-tested", + "4.4-11": "skip", + "4.4-12": "not-applicable", + "4.4-13": "not-applicable", + "4.4-14": "fail", + "4.4-15": "not-tested", + "4.4-16": "not-applicable", + "4.4-2": "not-tested", + "4.4-3": "not-tested", + "4.4-4": "partial", + "4.4-5": "not-tested", + "4.4-6": "not-tested", + "4.4-7": "not-tested", + "4.4-8": "not-tested", + "4.4-9": "not-tested", + "4.6-1": "not-tested", + "4.6-10": "not-tested", + "4.6-11": "not-tested", + "4.6-12": "untestable", + "4.6-13": "untestable", + "4.6-2": "not-applicable", + "4.6-3": "not-tested", + "4.6-4": "not-tested", + "4.6-5": "not-tested", + "4.6-6": "untestable", + "4.6-7": "not-tested", + "4.6-8": "not-tested", + "4.6-9": "untestable", + "4.7-10": "not-tested", + "4.7-11": "not-tested", + "4.7-12": "untestable", + "4.7-13": "not-tested", + "4.7-14": "not-tested", + "4.7-15": "not-tested", + "4.7-16": "not-tested", + "4.7-17": "not-tested", + "4.7-18": "not-tested", + "4.7-19": "not-tested", + "4.7-2": "not-tested", + "4.7-20": "partial", + "4.7-21": "not-tested", + "4.7-22": "not-tested", + "4.7-23": "not-tested", + "4.7-24": "not-tested", + "4.7-25": "not-tested", + "4.7-26": "not-tested", + "4.7-28": "untestable", + "4.7-29": "not-tested", + "4.7-3": "untestable", + "4.7-30": "untestable", + "4.7-31": "untestable", + "4.7-4": "not-tested", + "4.7-5": "untestable", + "4.7-6": "not-tested", + "4.7-7": "not-tested", + "4.7-8": "untestable", + "4.7-9": "not-tested", + "5.10-1": "not-tested", + "5.10-2": "skip", + "5.10-3": "untestable", + "5.10-4": "fail", + "5.10-5": "not-tested", + "5.10-6": "untestable", + "5.10-7": "untestable", + "5.10-8": "not-tested", + "5.10-9": "pass", + "5.3-1": "untestable", + "5.3-10": "untestable", + "5.3-2": "not-tested", + "5.3-3": "skip", + "5.3-4": "not-tested", + "5.3-5": "skip", + "5.3-6": "not-tested", + "5.3-7": "skip", + "5.3-8": "skip", + "5.3-9": "not-tested", + "5.4-4": "not-tested", + "5.4-5": "skip", + "5.5-1": "not-tested", + "5.5-2": "not-tested", + "5.5-3": "not-tested", + "5.5-4": "untestable", + "5.5-5": "not-tested", + "5.5-6": "not-tested", + "5.5-7": "not-tested", + "5.6-1": "not-tested", + "5.6-10": "not-tested", + "5.6-11": "not-tested", + "5.6-12": "not-tested", + "5.6-2": "not-tested", + "5.6-3": "untestable", + "5.6-4": "not-tested", + "5.6-5": "not-tested", + "5.6-6": "not-tested", + "5.6-7": "untestable", + "5.6-9": "not-tested", + "5.8-2": "not-applicable", + "5.8-3": "not-applicable", + "5.8-4": "not-applicable", + "6.0-1": "not-tested", + "6.1-10": "untestable", + "6.1-11": "not-tested", "6.1-2": "skip", "6.1-4": "skip", "6.1-5": "untestable", "6.1-6": "untestable", - "6.1-8": "untestable", + "6.1-8": "not-tested", "6.1-9": "untestable", + "6.10-1": "untestable", + "6.10-10": "untestable", + "6.10-2": "not-tested", + "6.10-3": "untestable", + "6.10-4": "untestable", + "6.10-5": "not-tested", + "6.10-6": "untestable", + "6.10-7": "untestable", + "6.10-8": "untestable", + "6.10-9": "untestable", + "6.11-1": "not-tested", + "6.11-10": "not-tested", + "6.11-11": "not-tested", + "6.11-12": "untestable", + "6.11-13": "not-tested", + "6.11-14": "skip", + "6.11-15": "not-tested", + "6.11-16": "not-tested", + "6.11-17": "not-tested", + "6.11-18": "not-tested", + "6.11-19": "pass", + "6.11-2": "not-tested", + "6.11-20": "not-tested", + "6.11-3": "not-applicable", + "6.11-4": "not-tested", + "6.11-5": "not-tested", + "6.11-6": "untestable", + "6.11-7": "not-tested", + "6.11-8": "not-tested", + "6.11-9": "not-tested", + "6.12-1": "not-tested", + "6.12-10": "not-tested", + "6.12-11": "not-tested", + "6.12-12": "not-tested", + "6.12-13": "not-tested", + "6.12-14": "not-tested", + "6.12-15": "not-tested", + "6.12-16": "not-tested", + "6.12-17": "not-tested", + "6.12-18": "not-tested", + "6.12-19": "not-tested", + "6.12-2": "not-tested", + "6.12-20": "not-tested", + "6.12-21": "not-tested", + "6.12-22": "not-tested", + "6.12-23": "not-tested", + "6.12-24": "not-tested", + "6.12-25": "not-tested", + "6.12-26": "not-tested", + "6.12-27": "not-tested", + "6.12-28": "not-tested", + "6.12-29": "not-tested", + "6.12-3": "not-tested", + "6.12-30": "not-tested", + "6.12-31": "not-tested", + "6.12-32": "untestable", + "6.12-33": "not-tested", + "6.12-34": "not-tested", + "6.12-4": "not-tested", + "6.12-5": "not-tested", + "6.12-6": "not-tested", + "6.12-7": "not-tested", + "6.12-8": "not-tested", + "6.12-9": "not-tested", + "6.13-1": "not-tested", + "6.13-2": "not-tested", + "6.13-3": "not-tested", + "6.14-1": "not-tested", + "6.14-2": "not-tested", + "6.14-3": "not-tested", + "6.14-4": "not-tested", + "6.14-5": "not-tested", + "6.14-6": "not-tested", + "6.14-7": "not-tested", + "6.14-8": "untestable", + "6.14-9": "not-tested", + "6.15-1": "not-tested", + "6.15-10": "not-tested", + "6.15-11": "not-tested", + "6.15-12": "not-tested", + "6.15-13": "not-tested", + "6.15-14": "not-tested", + "6.15-15": "not-tested", + "6.15-16": "not-tested", + "6.15-17": "not-tested", + "6.15-18": "not-tested", + "6.15-2": "not-tested", + "6.15-3": "not-tested", + "6.15-4": "not-tested", + "6.15-5": "not-tested", + "6.15-6": "not-tested", + "6.15-7": "not-tested", + "6.15-8": "not-tested", + "6.15-9": "not-tested", + "6.16-1": "not-tested", + "6.16-10": "not-tested", + "6.16-11": "untestable", + "6.16-12": "not-tested", + "6.16-13": "not-tested", + "6.16-14": "untestable", + "6.16-15": "untestable", + "6.16-16": "untestable", + "6.16-17": "not-tested", + "6.16-18": "untestable", + "6.16-19": "not-tested", + "6.16-2": "not-tested", + "6.16-20": "not-tested", + "6.16-3": "untestable", + "6.16-4": "not-tested", + "6.16-5": "not-tested", + "6.16-6": "not-tested", + "6.16-7": "untestable", + "6.16-8": "not-tested", + "6.16-9": "untestable", + "6.17-1": "pass", "6.2-2": "pass", "6.3-1": "untestable", "6.3-2": "skip", "6.4-1": "untestable", - "6.5-1": "pass", - "6.5-2": "untestable", - "6.6-1": "not-applicable", - "6.7-1": "untestable", - "6.8-1": "not-applicable", - "6.8-2": "skip", - "6.8-3": "pass", - "6.9-1": "pass", + "6.5-1": "not-tested", + "6.5-3": "untestable", + "6.6-1": "not-tested", + "6.6-2": "not-tested", + "6.6-3": "untestable", + "6.6-4": "untestable", + "6.6-5": "untestable", + "6.6-6": "not-tested", + "6.7-1": "not-applicable", + "6.7-2": "not-tested", + "6.7-3": "not-tested", + "6.7-4": "not-tested", + "6.7-5": "untestable", + "6.8-1": "not-tested", + "6.8-10": "not-tested", + "6.8-11": "untestable", + "6.8-12": "untestable", + "6.8-13": "untestable", + "6.8-14": "not-tested", + "6.8-15": "not-tested", + "6.8-16": "untestable", + "6.8-17": "not-tested", + "6.8-18": "untestable", + "6.8-19": "untestable", + "6.8-2": "not-tested", + "6.8-20": "not-tested", + "6.8-21": "not-tested", + "6.8-22": "not-tested", + "6.8-23": "not-tested", + "6.8-24": "not-tested", + "6.8-25": "untestable", + "6.8-3": "not-tested", + "6.8-4": "not-tested", + "6.8-5": "untestable", + "6.8-6": "untestable", + "6.8-7": "untestable", + "6.8-8": "untestable", + "6.8-9": "untestable", + "6.9-1": "untestable", + "6.9-2": "not-tested", + "6.9-3": "not-tested", + "6.9-4": "untestable", + "6.9-5": "not-tested", + "6.9-6": "untestable", + "6.9-7": "not-tested", + "7.0-1": "not-tested", + "7.0-2": "not-tested", + "7.0-3": "untestable", "7.1-1": "pass", - "7.10-1": "untestable", - "7.2-0": "partial", - "7.2-1": "pass", - "7.2-2": "pass", - "7.2-3": "pass", - "7.2-4": "pass", - "7.2-5": "partial", - "7.2-6": "skip", - "7.4-1": "untestable", - "7.4-2": "skip", - "7.4-3": "untestable", - "7.5-1": "pass", + "7.10-1": "not-tested", + "7.10-2": "not-tested", + "7.12-2": "untestable", + "7.12-3": "untestable", + "7.13-2": "untestable", + "7.14-1": "not-tested", + "7.14-2": "not-tested", + "7.14-3": "untestable", + "7.14-4": "not-tested", + "7.14-5": "not-tested", + "7.14-6": "not-tested", + "7.15-1": "not-tested", + "7.15-10": "not-tested", + "7.15-11": "untestable", + "7.15-12": "untestable", + "7.15-13": "untestable", + "7.15-14": "untestable", + "7.15-15": "untestable", + "7.15-16": "not-tested", + "7.15-17": "untestable", + "7.15-18": "not-tested", + "7.15-2": "not-tested", + "7.15-4": "untestable", + "7.15-5": "untestable", + "7.15-6": "not-tested", + "7.15-7": "untestable", + "7.15-8": "not-tested", + "7.15-9": "not-tested", + "7.16-1": "untestable", + "7.16-2": "not-tested", + "7.17-1": "untestable", + "7.17-2": "untestable", + "7.17-3": "not-tested", + "7.17-4": "untestable", + "7.17-5": "untestable", + "7.17-6": "untestable", + "7.17-7": "untestable", + "7.17-8": "not-tested", + "7.17-9": "untestable", + "7.18-1": "untestable", + "7.18-10": "not-tested", + "7.18-11": "untestable", + "7.18-12": "not-tested", + "7.18-13": "not-tested", + "7.18-14": "not-tested", + "7.18-15": "not-tested", + "7.18-16": "not-tested", + "7.18-17": "not-tested", + "7.18-18": "untestable", + "7.18-19": "untestable", + "7.18-2": "not-tested", + "7.18-20": "untestable", + "7.18-21": "untestable", + "7.18-22": "not-tested", + "7.18-23": "not-tested", + "7.18-24": "not-tested", + "7.18-25": "not-tested", + "7.18-26": "not-tested", + "7.18-27": "not-tested", + "7.18-28": "untestable", + "7.18-29": "untestable", + "7.18-3": "untestable", + "7.18-30": "not-tested", + "7.18-31": "not-tested", + "7.18-32": "not-tested", + "7.18-4": "untestable", + "7.18-5": "untestable", + "7.18-6": "untestable", + "7.18-7": "not-tested", + "7.18-8": "untestable", + "7.18-9": "not-tested", + "7.19-1": "not-tested", + "7.19-2": "not-tested", + "7.19-3": "untestable", + "7.19-4": "not-tested", + "7.19-5": "untestable", + "7.19-6": "not-tested", + "7.19-7": "not-tested", + "7.2-1": "partial", + "7.2-10": "skip", + "7.2-11": "untestable", + "7.2-12": "not-tested", + "7.2-13": "untestable", + "7.2-14": "untestable", + "7.2-15": "pass", + "7.2-16": "pass", + "7.2-17": "partial", + "7.2-2": "not-tested", + "7.2-3": "not-tested", + "7.2-4": "not-tested", + "7.2-5": "not-tested", + "7.2-6": "pass", + "7.2-7": "partial", + "7.2-8": "not-tested", + "7.2-9": "untestable", + "7.20-1": "untestable", + "7.20-2": "untestable", + "7.20-3": "untestable", + "7.20-4": "untestable", + "7.20-8": "not-tested", + "7.20-9": "not-tested", + "7.21-1": "not-tested", + "7.21-10": "not-tested", + "7.21-11": "not-tested", + "7.21-12": "untestable", + "7.21-13": "untestable", + "7.21-14": "untestable", + "7.21-15": "not-tested", + "7.21-16": "not-tested", + "7.21-17": "not-tested", + "7.21-18": "not-tested", + "7.21-19": "untestable", + "7.21-2": "not-tested", + "7.21-20": "not-tested", + "7.21-21": "not-tested", + "7.21-22": "not-tested", + "7.21-23": "not-tested", + "7.21-24": "untestable", + "7.21-25": "not-tested", + "7.21-26": "not-tested", + "7.21-27": "not-tested", + "7.21-3": "not-tested", + "7.21-4": "not-tested", + "7.21-5": "not-tested", + "7.21-6": "not-tested", + "7.21-7": "not-tested", + "7.21-8": "untestable", + "7.21-9": "not-tested", + "7.22-1": "not-tested", + "7.22-10": "untestable", + "7.22-2": "untestable", + "7.22-3": "not-tested", + "7.22-4": "not-tested", + "7.22-5": "not-tested", + "7.22-6": "not-tested", + "7.22-7": "untestable", + "7.22-8": "untestable", + "7.22-9": "not-tested", + "7.23-1": "not-tested", + "7.23-10": "untestable", + "7.23-11": "untestable", + "7.23-12": "not-tested", + "7.23-13": "untestable", + "7.23-14": "untestable", + "7.23-15": "untestable", + "7.23-16": "untestable", + "7.23-17": "untestable", + "7.23-18": "untestable", + "7.23-19": "untestable", + "7.23-2": "untestable", + "7.23-20": "untestable", + "7.23-21": "untestable", + "7.23-22": "untestable", + "7.23-23": "untestable", + "7.23-24": "untestable", + "7.23-25": "not-tested", + "7.23-26": "untestable", + "7.23-27": "untestable", + "7.23-28": "not-tested", + "7.23-29": "untestable", + "7.23-3": "untestable", + "7.23-30": "untestable", + "7.23-31": "untestable", + "7.23-32": "untestable", + "7.23-33": "not-tested", + "7.23-34": "untestable", + "7.23-35": "untestable", + "7.23-36": "untestable", + "7.23-37": "not-tested", + "7.23-4": "untestable", + "7.23-5": "untestable", + "7.23-6": "not-tested", + "7.23-7": "not-tested", + "7.23-8": "untestable", + "7.23-9": "not-tested", + "7.24-1": "untestable", + "7.24-2": "not-tested", + "7.24-3": "not-tested", + "7.24-4": "untestable", + "7.24-5": "untestable", + "7.24-6": "not-tested", + "7.3-1": "partial", + "7.3-10": "not-tested", + "7.3-11": "not-tested", + "7.3-12": "not-tested", + "7.3-13": "not-tested", + "7.3-14": "not-tested", + "7.3-2": "not-tested", + "7.3-3": "not-tested", + "7.3-4": "not-tested", + "7.3-5": "not-tested", + "7.3-6": "not-tested", + "7.3-7": "not-tested", + "7.3-8": "not-tested", + "7.3-9": "untestable", + "7.4-1": "not-tested", + "7.4-2": "not-tested", + "7.4-3": "not-tested", + "7.4-4": "untestable", + "7.4-5": "untestable", + "7.4-6": "not-tested", + "7.5-1": "untestable", + "7.7-1": "untestable", + "7.7-2": "skip", + "7.7-3": "untestable", + "7.8-1": "not-tested", + "7.8-2": "not-tested", + "7.8-3": "not-tested", + "7.8-4": "pass", + "7.9-1": "untestable", + "7.9-2": "untestable", + "7.9-3": "untestable", + "7.9-4": "not-tested", + "8.0-1": "not-tested", "8.1-1": "not-tested", - "8.1-1a": "partial", - "8.1-2": "pass", + "8.1-2": "partial", "8.1-3": "pass", - "8.12-1": "pass", - "8.12-2": "partial", - "8.12-3": "not-applicable", - "8.13-1": "not-applicable", - "8.13-3": "not-applicable", - "8.13-4": "not-applicable", - "8.14-1": "fail", - "8.2-1": "pass", - "8.2-2": "untestable", - "8.2-3": "skip", - "8.2-4": "partial", + "8.1-4": "pass", + "8.10-2": "not-tested", + "8.10-3": "not-tested", + "8.10-4": "not-tested", + "8.10-5": "not-tested", + "8.10-6": "not-tested", + "8.11-1": "not-tested", + "8.12-1": "not-applicable", + "8.12-10": "partial", + "8.12-11": "pass", + "8.12-12": "partial", + "8.12-13": "not-applicable", + "8.12-14": "pass", + "8.12-15": "not-tested", + "8.12-16": "pass", + "8.12-17": "not-tested", + "8.12-19": "fail", + "8.12-2": "pass", + "8.12-3": "partial", + "8.12-4": "not-tested", + "8.12-5": "pass", + "8.12-6": "pass", + "8.12-7": "pass", + "8.12-8": "pass", + "8.12-9": "pass", + "8.13-2": "not-tested", + "8.13-3": "not-tested", + "8.15-1": "not-tested", + "8.15-10": "not-tested", + "8.15-2": "not-tested", + "8.15-3": "not-tested", + "8.15-4": "pass", + "8.15-5": "partial", + "8.15-6": "not-applicable", + "8.15-7": "not-tested", + "8.15-8": "not-tested", + "8.15-9": "not-tested", + "8.16-1": "not-applicable", + "8.16-3": "not-applicable", + "8.16-4": "not-applicable", + "8.17-1": "not-tested", + "8.17-10": "not-tested", + "8.17-11": "not-tested", + "8.17-12": "not-tested", + "8.17-13": "not-tested", + "8.17-14": "not-tested", + "8.17-15": "not-tested", + "8.17-16": "not-tested", + "8.17-17": "not-tested", + "8.17-18": "not-tested", + "8.17-19": "not-tested", + "8.17-2": "untestable", + "8.17-20": "not-tested", + "8.17-21": "not-tested", + "8.17-22": "not-tested", + "8.17-23": "untestable", + "8.17-3": "not-tested", + "8.17-4": "not-tested", + "8.17-5": "not-tested", + "8.17-6": "not-tested", + "8.17-7": "partial", + "8.17-8": "not-tested", + "8.17-9": "not-tested", + "8.2-1": "not-tested", + "8.2-10": "not-tested", + "8.2-11": "not-tested", + "8.2-2": "not-tested", + "8.2-3": "not-tested", + "8.2-4": "not-tested", + "8.2-5": "not-tested", + "8.2-6": "not-tested", + "8.2-7": "not-tested", + "8.2-8": "not-tested", + "8.2-9": "not-tested", "8.3-1": "pass", - "8.4-1": "pass", - "8.4-2": "pass", - "8.4-4": "untestable", - "8.7-1": "pass", - "8.9-1": "not-applicable", - "8.9-10": "pass", - "8.9-11": "partial", - "8.9-12": "not-applicable", - "8.9-13": "pass", - "8.9-14": "pass", - "8.9-15": "fail", - "8.9-16": "not-applicable", - "8.9-2": "pass", - "8.9-3": "partial", - "8.9-4": "pass", - "8.9-5": "pass", - "8.9-6": "pass", - "8.9-7": "pass", - "8.9-8": "pass", - "8.9-9": "partial", - "A.0-1": "pass", - "A.0-2": "untestable" + "8.3-10": "not-tested", + "8.3-11": "not-tested", + "8.3-12": "not-tested", + "8.3-13": "not-tested", + "8.3-14": "not-tested", + "8.3-15": "not-tested", + "8.3-16": "untestable", + "8.3-17": "untestable", + "8.3-18": "not-tested", + "8.3-19": "skip", + "8.3-2": "untestable", + "8.3-20": "untestable", + "8.3-21": "partial", + "8.3-3": "not-tested", + "8.3-4": "not-tested", + "8.3-5": "not-tested", + "8.3-6": "not-tested", + "8.3-7": "not-tested", + "8.3-8": "untestable", + "8.3-9": "not-tested", + "8.4-1": "not-tested", + "8.4-2": "not-tested", + "8.4-3": "not-tested", + "8.4-4": "pass", + "8.5-1": "pass", + "8.5-10": "not-tested", + "8.5-2": "pass", + "8.5-3": "not-tested", + "8.5-4": "not-tested", + "8.5-5": "not-tested", + "8.5-6": "not-tested", + "8.5-7": "not-tested", + "8.5-9": "untestable", + "8.6-1": "not-tested", + "8.6-2": "not-tested", + "8.9-1": "pass", + "9.4-1": "untestable", + "9.5-1": "untestable", + "A.0-1": "not-tested" } } diff --git a/packages/conformance-suite/test/normative-matrix.test.ts b/packages/conformance-suite/test/normative-matrix.test.ts index b2a549696..985a4da24 100644 --- a/packages/conformance-suite/test/normative-matrix.test.ts +++ b/packages/conformance-suite/test/normative-matrix.test.ts @@ -8,12 +8,12 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; -import { REQUIREMENTS, SPEC_SOURCE } from "../src/requirements/catalog.ts"; +import { REQUIREMENTS } from "../src/requirements/catalog.ts"; import { CLAUSE_MATRIX, parseSpecVersion } from "../src/requirements/matrix.ts"; import { ALL_CASES } from "../src/suite.ts"; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); -const SPEC = readFileSync(join(REPO_ROOT, SPEC_SOURCE.path), "utf8"); +const SPEC = readFileSync(join(REPO_ROOT, "spec-core.md"), "utf8"); const SPEC_LINES = SPEC.split("\n"); const HEADING = /^#{1,6} (.+)$/; const EXPLICIT_ANCHOR = /\{#([^}]+)\}/; diff --git a/packages/conformance-suite/test/report-honesty.test.ts b/packages/conformance-suite/test/report-honesty.test.ts index e867b235f..a393cb6d9 100644 --- a/packages/conformance-suite/test/report-honesty.test.ts +++ b/packages/conformance-suite/test/report-honesty.test.ts @@ -63,21 +63,15 @@ describe("the report cannot present a partial run as complete", () => { assert.ok(markdown.includes("## Review evidence")); assert.ok(markdown.includes("do not create cases, change coverage, or count as tested")); }); - it("does not leave the newly observable client-capture MUST clauses unregistered", () => { + it("names a gap for every client-capture MUST clause that no case exercises", () => { const clientCaptureMusts = CLAUSE_MATRIX.filter( (clause) => clause.level === "must" && clause.observable === "client-capture" ); - assert.equal( - clientCaptureMusts.length, - 12, - "the inventory must account for all twelve client-capture MUST clauses." - ); - // 12.4-3 (Section 12 Extensions) is transcribed with no case yet; its gap note names the fixture it needs. + // The locked Core text carries 58 client-capture MUST clauses; the client fixture exercises 11. + // Every other one is transcribed with no case, and its gap note says so. + assert.equal(clientCaptureMusts.length, 58, "the inventory must account for all client-capture MUST clauses."); const uncovered = clientCaptureMusts.filter((clause) => clause.caseIds.length === 0); - assert.deepEqual( - uncovered.map((clause) => clause.clauseId), - ["12.4-3"] - ); + assert.equal(uncovered.length, 47); for (const clause of uncovered) { assert.ok(clause.gapNote?.trim(), `Uncovered client-capture MUST ${clause.clauseId} has no gap note.`); } diff --git a/packages/conformance-suite/test/requirements-report.test.ts b/packages/conformance-suite/test/requirements-report.test.ts index 5ad1789ac..38315a831 100644 --- a/packages/conformance-suite/test/requirements-report.test.ts +++ b/packages/conformance-suite/test/requirements-report.test.ts @@ -14,7 +14,6 @@ import { rollUpClauseResult, } from "../src/report/requirements.ts"; import { buildReport, type CaseResult, exitCodeFor } from "../src/report/result.ts"; -import { SPEC_SOURCE } from "../src/requirements/catalog.ts"; import { CLAUSE_MATRIX, type Observable } from "../src/requirements/matrix.ts"; import { clauseFragments, normalizeSpecText } from "../src/requirements/spec-scan.ts"; import { ALL_CASES, SUITE } from "../src/suite.ts"; @@ -106,7 +105,7 @@ test("headline: every case passing still exits 2 while a MUST clause is partial, assert.ok(report.requirements.every((r) => r.outcome === "pass" || r.outcome === "not-tested")); // ...yet clause-level evidence is incomplete, so the headline must not claim completion. const incomplete = report.summary.incompleteMustClauses.map((c) => c.clauseId); - assert.ok(incomplete.includes("8.12-2"), "8.12-2 is acknowledged-partial and must block completion"); + assert.ok(incomplete.includes("8.15-5"), "8.15-5 is acknowledged-partial and must block completion"); assert.equal(report.summary.allApplicableMustsTestedAndPassed, false); assert.equal(exitCodeFor(report), 2); }); @@ -159,7 +158,7 @@ test("the header names the spec file, its SHA-256, the suite version, the target reportWith(() => "pass"), SPEC ); - assert.equal(out.header.specFile, SPEC_SOURCE.path); + assert.equal(out.header.specFile, "spec-core.md"); assert.equal(out.header.specSha256, createHash("sha256").update(SPEC).digest("hex")); assert.equal(out.header.suiteVersion, SUITE.version); assert.equal(out.header.targetId, "synthetic"); @@ -171,7 +170,7 @@ test("a failing case surfaces as a failing clause with the case detail", () => { reportWith((id) => (id === "RS-7/expired-sync-cursor-reported-as-gone" ? "fail" : "pass")), SPEC ); - const clause = out.must.find((e) => e.clauseId === "4.3-2"); + const clause = out.must.find((e) => e.clauseId === "4.4-11"); assert.equal(clause?.result, "fail"); assert.match(clause?.reason ?? "", SYNTHETIC_FAILURE); assert.equal(out.summary.must.fail, out.must.filter((e) => e.result === "fail").length); diff --git a/packages/conformance-suite/test/spec-completeness.test.ts b/packages/conformance-suite/test/spec-completeness.test.ts index ac03ae42f..2ed4898f5 100644 --- a/packages/conformance-suite/test/spec-completeness.test.ts +++ b/packages/conformance-suite/test/spec-completeness.test.ts @@ -11,7 +11,7 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { describe, it, test } from "node:test"; import { fileURLToPath } from "node:url"; -import { REQUIREMENTS, SPEC_SOURCE } from "../src/requirements/catalog.ts"; +import { REQUIREMENTS } from "../src/requirements/catalog.ts"; import { SPEC_SENTENCE_EXCLUSIONS } from "../src/requirements/exclusions.ts"; import { CLAUSE_MATRIX } from "../src/requirements/matrix.ts"; import { @@ -25,7 +25,7 @@ import { const PATTERN_BETA_ITEM = /beta item/; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); -const SPEC = readFileSync(join(REPO_ROOT, SPEC_SOURCE.path), "utf8"); +const SPEC = readFileSync(join(REPO_ROOT, "spec-core.md"), "utf8"); const SPEC_LINES = SPEC.split("\n"); const CLAUSE_TEXTS = CLAUSE_MATRIX.map((clause) => clause.text); const CONFORMANCE_SECTION = "9"; @@ -65,7 +65,7 @@ test("every list item under a MUST lead-in is quoted by the clause that keeps th test("a clause that keeps the MUST lead-in but elides a list item fails the lead-in check", () => { const elided = CLAUSE_MATRIX.map((clause) => - clause.clauseId === "8.12-2" ? clause.text.replace(" [...] `Cache-Control: private, no-store`", "") : clause.text + clause.clauseId === "8.15-5" ? clause.text.replace(" [...] `Cache-Control: private, no-store`", "") : clause.text ); assert.notEqual(elided.join("\n"), CLAUSE_TEXTS.join("\n"), "the doctored clause must differ"); const gaps = leadInGaps(SPEC, elided); @@ -117,8 +117,8 @@ describe("lead-in scan handles the list layouts Markdown allows", () => { assert.equal(scanLeadIns(spec).length, 0); }); - it("finds the real blank-line-separated lead-in at spec-core.md 560", () => { - assert.ok(scanLeadIns(SPEC).some((l) => l.line === 560 && l.items.length === 2)); + it("finds the real blank-line-separated lead-in at spec-core.md 868", () => { + assert.ok(scanLeadIns(SPEC).some((l) => l.line === 868 && l.items.length === 2)); }); });