Scope grammar implementation
Status: implementation note for A6.2. It records how the reference library implements the section; its requirements bind that library and add none for other implementations. Document version 1.0-draft-1.
This implements the grammar in A6.2, not an authorisation server. An authorisation server parses the scope of an authorisation or token request with it; no recipient route accepts scopes. Scopes from a token MUST NOT replace the stored grant as authority (A6.2; read-path verification remains pending).
The source of truth is packages/schema/src/scope/index.ts. scopes.json publishes the closed purpose/capability sets and limits, with the purpose codes and capability meanings below; its scope marker, grammar-and-meanings, says that it holds the grammar and the meanings issuance applies (A6.2), not an endpoint; scope-token.json validates one token; examples are generated and round-trip through the parser. A token schema does not validate a complete grant. Complete sets MUST pass the semantic checks below.
Parsing and canonical form
parseScopes(string) returns a canonical token array; serialiseScopes(array) revalidates and joins it. Both MUST reject unknown tokens, trailing content, invalid case and malformed boundaries with invalid_scope. No input is trimmed, decoded or case-folded. The separator is exactly one ASCII space. Input is limited to 16,384 ASCII bytes, 128 tokens before deduplication and 128 characters per domain. Non-ASCII input, leading/trailing spaces and all other whitespace are rejected. The domain character alphabet is the published scope grammar, including extension names and exact case. A syntactically valid domain is not automatically a registered vocabulary domain.
Exactly one purpose token MUST occur before deduplication. A valid token set without one fails with purpose_missing; two occurrences, even identical ones, fail with invalid_scope. An empty string also fails with purpose_missing. Malformed input is rejected before the purpose count. All ten purposes and eight capability strings are generated from schemas.
* MUST appear only on L0 read scopes. No missing domain implies an L0 floor. ceilingFor(set, domain) returns the explicit domain ceiling, or L0 for a wildcard, or null. It revalidates the whole set and rejects a wildcard or invalid queried domain. It is a ceiling lookup only: expiry, status, read counters, confidence, purpose, declared-only and every other stored grant restriction are still required before a read.
Read duplicates for the same exact domain and the same declared-only mode MUST collapse to their highest level at the first occurrence's position. Duplicate capabilities collapse to their first occurrence. Explicit domains and * remain distinct. Mixed unrestricted and :declared reads for the same domain MUST be rejected in this implementation. At grant issuance, any accepted :declared modifier makes the entire stored grant declared_only:true, including wildcard/explicit-domain combinations. Consent shows this restriction. A person wanting different declaration restrictions uses separate grants. Explicit domain ceilings take precedence over the wildcard L0 ceiling; no declaration restriction is removed. No present code treats a ceiling as permission to disclose an entry.
Validation errors contain a fixed name, registered code and title, never the input or the underlying Zod issues. invalid_scope and purpose_missing are registered as status 400 Problem Details variants for routes outside OAuth. At the authorisation and token endpoints, a record-grant scope the parser refuses fails OAuth invalid_scope (RFC 6749 sections 4.1.2.1 and 5.2) in the OAuth error form, and so does a scope without a purpose. The A6.6 lifecycle scope is not a record-grant scope: the token endpoint checks it on its own, outside the parser. The parser itself throws ScopeValidationError.
Purpose codes
Where a purpose has to be an absolute URI, such as PDPP's purpose_code (B1.4), the purpose autonym:purpose:<category> is written urn:autonym:purpose:<category>: urn:autonym:purpose:booking, for example. scopes.json lists the ten codes as purpose_codes. No other code names an Open Personal Context purpose.
Capability meanings
Each capability scope has one meaning at issuance (A6.2), published in scopes.json as capability_meanings. A client receives a third-party capability only when its class may hold it (A2).
| Scope | Who may hold it | Meaning |
|---|---|---|
autonym:propose |
Third party | Requests a contribution grant, separate from any read grant |
autonym:confirm |
Third party | Sets allow_confirmation on that contribution grant |
autonym:persist |
Third party | Permits persist_until on a read grant |
autonym:delegate |
Third party | Sets allow_delegation on a read grant |
autonym:confirm:safety |
First party | Reserved to the person's own first-party clients; never issued to a recipient |
autonym:write:declared |
First party | Reserved to the person's own first-party clients; never issued to a recipient |
autonym:log:read |
First party | Reserved to the person's own first-party clients; never issued to a recipient |
autonym:manage |
First party | Reserved to the person's own first-party clients; never issued to a recipient |
A capability of one grant kind cannot be combined with a request for the other kind, so autonym:propose with a read scope, autonym:persist or autonym:delegate is a mixed request. The second of the published examples, which reads two domains and asks for autonym:propose, is valid grammar but such a mixed request, so issuance refuses it with invalid_scope. openid and offline_access are not Open Personal Context scopes: the parser refuses them like any other unknown token.
Advertising purpose
autonym:purpose:advertising is separate from personalisation and recommendation. The parser MUST preserve that distinction and MUST NOT add read, proposal or persistence capabilities from a purpose alone. A combined advertising and personalisation set MUST be rejected by the one-purpose rule; separate consent requires separate grants. SCOPE-10 checks this grammar boundary. A6.7 defines the draft consent requirements. Parsing a scope does not establish consent, an eligible domain or permission to disclose.
Executable checks
packages/schema/test/scopes.test.ts supplies SCOPE-01 (source fixture), SCOPE-02 (closed purposes/capabilities), SCOPE-03 (one purpose), SCOPE-04 (wildcard and absence), SCOPE-05 (duplicates and declaration ambiguity), SCOPE-06 (exact parsing), SCOPE-07 (limits and caller boundary validation), SCOPE-08 (10,000 arbitrary inputs) and SCOPE-09 (10,000 valid sets, round trips and ceiling bounds). SCOPE-10 checks the advertising boundary. These are grammar checks, not certification of the A17 recipient capabilities.
Recipient OpenAPI/MCP contracts are now generated, while endpoints and SDK implementations remain pending. Parsing is not an endpoint or a conformance claim. The separate client-lifecycle scope in A6 is not a record-grant scope.
References to other standards and projects describe intended technical alignment only and do not imply endorsement or partnership.