diff --git a/docs/current/SPECIFICATION.md b/docs/current/SPECIFICATION.md index 1d6c4f6..9548bd9 100644 --- a/docs/current/SPECIFICATION.md +++ b/docs/current/SPECIFICATION.md @@ -209,6 +209,43 @@ Entities MUST declare their EEP capabilities in the JSON response: The `gated`, `gates_url`, `commerce`, and `services_url` fields are OPTIONAL. They MUST be present if the entity uses gated access or offers services. +### 3.3.1 Error responses (normative) + +EEP error responses are **RFC 9457 problem details**. Publishers MUST serve +them with `Content-Type: application/problem+json` and MUST include the +`type`, `title` and `status` members; `detail` and `instance` are RECOMMENDED. + +| Member | Level | Meaning | +|---|---|---| +| `type` | MUST | URI identifying the problem type. Clients match on this, not on the HTTP status. | +| `title` | MUST | Short summary of the type. MUST NOT vary between occurrences. | +| `status` | MUST | The HTTP status, repeated so the document survives being logged or forwarded. | +| `detail` | SHOULD | Explanation specific to this occurrence, for a developer reading a log. | +| `instance` | MAY | URI reference identifying this occurrence. | + +Registered EEP problem types: + +| Status | `type` | +|---|---| +| 402 | `https://eep.dev/problems/payment-required` | +| 403 | `https://eep.dev/problems/access-restricted` | +| 429 | `https://eep.dev/problems/rate-limited` | +| 451 | `https://eep.dev/problems/legally-restricted` | + +Everything EEP already defined on these responses — `unmet_requirements`, +`required_tier`, `retry_after_seconds`, `signed_challenge` and the rest — +remains, as RFC 9457 **extension members**. This is additive: a client reading +the existing fields keeps working, and a client that understands problem +details gains a shape it already knows. + +Publishers MUST NOT vary `type` to encode per-occurrence information; that is +what `detail` is for. Clients MUST tolerate unknown extension members and MUST +NOT treat an unrecognised `type` as a different HTTP status than `status` says. + +Bespoke error envelopes were a needless dialect: `application/problem+json` is +understood by generic HTTP clients, API gateways and agent frameworks without +EEP-specific parsing, and it is what a standards reviewer will expect. + ### 3.4 Gated access Entities MAY define **gates** to restrict access to resources. A gate configuration has entity-defined **tiers**, each with a list of **requirements** and a set of **access patterns** that tier opens up. diff --git a/packages/@eep-dev/gates/src/http-402.ts b/packages/@eep-dev/gates/src/http-402.ts index b2b1695..aa6d02c 100644 --- a/packages/@eep-dev/gates/src/http-402.ts +++ b/packages/@eep-dev/gates/src/http-402.ts @@ -6,6 +6,11 @@ */ import type { GateConfig, GateProof, AccessRestrictionResponse, RateLimitResponse, Requirement } from './types.js'; +import { + PROBLEM_TYPE_PAYMENT_REQUIRED, + PROBLEM_TYPE_RATE_LIMITED, + PROBLEM_JSON_CONTENT_TYPE, +} from './types.js'; import { resolveAccess } from './access-resolver.js'; import { matchesAny, findTiersForResource } from './resource-matcher.js'; @@ -44,6 +49,12 @@ export async function build402Response( } const response: AccessRestrictionResponse = { + // RFC 9457 members first: a generic problem-details client reads + // these, and the EEP fields below are extension members. + type: PROBLEM_TYPE_PAYMENT_REQUIRED, + title: 'Payment Required', + status: 402, + detail: `Access to '${resource}' requires the '${requiredTier}' tier.`, error: 'access_restricted', resource, current_tier: accessResult.tier, @@ -116,6 +127,10 @@ export async function build429Response( const signedChallenge = `${challengePayload}.${toB64(signature)}`; const body: RateLimitResponse = { + type: PROBLEM_TYPE_RATE_LIMITED, + title: 'Too Many Requests', + status: 429, + detail: `Rate limit exceeded for ${agentDid}; retry after ${retryAfterSeconds}s.`, error: 'rate_limited', did_rate_limit_key: agentDid, retry_after_seconds: retryAfterSeconds, @@ -127,6 +142,7 @@ export async function build429Response( }; const headers: Record = { + 'Content-Type': PROBLEM_JSON_CONTENT_TYPE, 'Retry-After': String(retryAfterSeconds), 'X-EEP-Rate-Limit-DID': agentDid, 'X-EEP-Rate-Reset': windowResetAt, diff --git a/packages/@eep-dev/gates/src/index.ts b/packages/@eep-dev/gates/src/index.ts index 6a90f08..2a57739 100644 --- a/packages/@eep-dev/gates/src/index.ts +++ b/packages/@eep-dev/gates/src/index.ts @@ -60,7 +60,7 @@ export type { // Proof-of-Intent (G4) IntentDocument, ProofOfIntent, // Access results - UnmetRequirement, AccessResult, AccessRestrictionResponse, + UnmetRequirement, AccessResult, AccessRestrictionResponse, ProblemDetails, // HTTP 403/451 (G6) ForbiddenResponse, LegallyRestrictedResponse, // EEP Manifest & ERC-8004 (G3/G8) @@ -146,6 +146,17 @@ export { // ── HTTP 402 ────────────────────────────────────────────────────────────────── export { build402Response, isGatedResource, build429Response } from './http-402.js'; +// RFC 9457 problem details (SPECIFICATION.md §3.3.1). Error responses carry +// these members alongside the EEP-specific fields, which become problem +// extension members. +export { + PROBLEM_TYPE_PAYMENT_REQUIRED, + PROBLEM_TYPE_ACCESS_RESTRICTED, + PROBLEM_TYPE_RATE_LIMITED, + PROBLEM_TYPE_LEGALLY_RESTRICTED, + PROBLEM_JSON_CONTENT_TYPE, +} from './types.js'; + // ── Commerce ────────────────────────────────────────────────────────────────── export { transition, getValidActions, isTerminal, diff --git a/packages/@eep-dev/gates/src/problem-details.test.ts b/packages/@eep-dev/gates/src/problem-details.test.ts new file mode 100644 index 0000000..a81042a --- /dev/null +++ b/packages/@eep-dev/gates/src/problem-details.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { + build402Response, + build429Response, + parseGateConfig, + PROBLEM_TYPE_PAYMENT_REQUIRED, + PROBLEM_TYPE_RATE_LIMITED, + PROBLEM_JSON_CONTENT_TYPE, +} from './index.js'; + +// SPECIFICATION.md §3.3.1 — EEP error responses are RFC 9457 problem details. +// The EEP-specific fields remain as problem *extension members*, so this is +// additive: a client reading the old fields keeps working. +describe('RFC 9457 problem details (§3.3.1)', () => { + const config = parseGateConfig({ + default_tier: 'public', + tiers: { + public: { requirements: [], access: ['entity.public.profile'] }, + premium: { + requirements: [{ type: 'payment', amount: 1, currency: 'usd', per: 'request' }], + access: ['content.papers.full_text'], + }, + }, + }); + + describe('402', () => { + it('carries type, title and status', async () => { + const body = await build402Response(config, 'content.papers.full_text', []); + expect(body.type).toBe(PROBLEM_TYPE_PAYMENT_REQUIRED); + expect(body.title).toBe('Payment Required'); + expect(body.status).toBe(402); + }); + + it('carries an occurrence-specific detail', async () => { + const body = await build402Response(config, 'content.papers.full_text', []); + expect(typeof body.detail).toBe('string'); + expect(body.detail).toContain('content.papers.full_text'); + }); + + // The point of RFC 9457 extension members: nothing EEP already + // defined is removed or renamed. + it('preserves every pre-existing EEP field', async () => { + const body = await build402Response(config, 'content.papers.full_text', []); + expect(body.error).toBe('access_restricted'); + expect(body.resource).toBe('content.papers.full_text'); + expect(body.current_tier).toBe('public'); + expect(body.required_tier).toBe('premium'); + expect(Array.isArray(body.unmet_requirements)).toBe(true); + }); + + // `title` identifies the problem TYPE, so it must not vary between + // occurrences; `detail` is where per-occurrence information goes. + it('keeps title stable across different resources', async () => { + const a = await build402Response(config, 'content.papers.full_text', []); + const b = await build402Response(config, 'entity.public.profile', []); + expect(a.title).toBe(b.title); + expect(a.type).toBe(b.type); + }); + }); + + describe('429', () => { + const sign = async (challenge: string) => `sig(${challenge.slice(0, 8)})`; + + it('carries type, title and status', async () => { + const { body } = await build429Response('did:key:agent', 60, sign); + expect(body.type).toBe(PROBLEM_TYPE_RATE_LIMITED); + expect(body.title).toBe('Too Many Requests'); + expect(body.status).toBe(429); + }); + + it('serves the RFC 9457 media type', async () => { + const { headers } = await build429Response('did:key:agent', 60, sign); + expect(headers['Content-Type']).toBe(PROBLEM_JSON_CONTENT_TYPE); + }); + + it('preserves the pre-existing rate-limit fields', async () => { + const { body, headers } = await build429Response('did:key:agent', 60, sign, { + limitPerWindow: 100, + requestsMade: 101, + }); + expect(body.error).toBe('rate_limited'); + expect(body.did_rate_limit_key).toBe('did:key:agent'); + expect(body.retry_after_seconds).toBe(60); + expect(body.signed_challenge).toContain('v1.'); + expect(body.limit_per_window).toBe(100); + expect(headers['Retry-After']).toBe('60'); + }); + }); + + it('uses distinct problem type URIs per condition', () => { + expect(PROBLEM_TYPE_PAYMENT_REQUIRED).not.toBe(PROBLEM_TYPE_RATE_LIMITED); + for (const uri of [PROBLEM_TYPE_PAYMENT_REQUIRED, PROBLEM_TYPE_RATE_LIMITED]) { + expect(() => new URL(uri)).not.toThrow(); + } + }); +}); diff --git a/packages/@eep-dev/gates/src/types.ts b/packages/@eep-dev/gates/src/types.ts index a50a670..454b77a 100644 --- a/packages/@eep-dev/gates/src/types.ts +++ b/packages/@eep-dev/gates/src/types.ts @@ -297,7 +297,38 @@ export interface AccessResult { // ── HTTP 402 Response ───────────────────────────────────────────────────────── -export interface AccessRestrictionResponse { +/** + * RFC 9457 problem-details members, common to every EEP error response + * (SPECIFICATION.md §3.3.1). + * + * Everything EEP already defined on these responses stays, as RFC 9457 + * *extension members* — so a client reading the existing fields keeps + * working, and a client that speaks problem details gains a shape it + * already knows. + */ +export interface ProblemDetails { + /** URI identifying the problem type. Clients match on this, not the status. */ + type: string; + /** Short summary of the type. Does not vary between occurrences. */ + title: string; + /** The HTTP status, repeated so the document survives being logged. */ + status: number; + /** Explanation specific to this occurrence, for a developer reading a log. */ + detail?: string; + /** URI reference identifying this occurrence. */ + instance?: string; +} + +/** Registered EEP problem type URIs (SPECIFICATION.md §3.3.1). */ +export const PROBLEM_TYPE_PAYMENT_REQUIRED = 'https://eep.dev/problems/payment-required'; +export const PROBLEM_TYPE_ACCESS_RESTRICTED = 'https://eep.dev/problems/access-restricted'; +export const PROBLEM_TYPE_RATE_LIMITED = 'https://eep.dev/problems/rate-limited'; +export const PROBLEM_TYPE_LEGALLY_RESTRICTED = 'https://eep.dev/problems/legally-restricted'; + +/** Media type for every EEP error response (RFC 9457). */ +export const PROBLEM_JSON_CONTENT_TYPE = 'application/problem+json'; + +export interface AccessRestrictionResponse extends ProblemDetails { error: 'access_restricted'; resource: string; current_tier: string; @@ -311,7 +342,7 @@ export interface AccessRestrictionResponse { // ── 403 Forbidden Response ─────────────────────────────────────────────────── /** HTTP 403 response for credential/agreement/identity gate failure (G6) */ -export interface ForbiddenResponse { +export interface ForbiddenResponse extends ProblemDetails { error: 'access_forbidden'; resource: string; current_tier: string; @@ -740,7 +771,7 @@ export interface RFPClosedEvent { // ── G30: Rate-Limit 429 Response ───────────────────────────────────────────── /** HTTP 429 Too Many Requests response body per Whitepaper §10.5 / SPECIFICATION.md §3.4.6 */ -export interface RateLimitResponse { +export interface RateLimitResponse extends ProblemDetails { error: 'rate_limited'; did_rate_limit_key: string; retry_after_seconds: number; diff --git a/packages/@eep-dev/middleware/src/core/eep-server.ts b/packages/@eep-dev/middleware/src/core/eep-server.ts index aa2db6a..e84ca0c 100644 --- a/packages/@eep-dev/middleware/src/core/eep-server.ts +++ b/packages/@eep-dev/middleware/src/core/eep-server.ts @@ -7,7 +7,8 @@ import { type AccessRestrictionResponse, type GateConfig, type GateProof, - type ProofVerifier + type ProofVerifier, + PROBLEM_JSON_CONTENT_TYPE } from "@eep-dev/gates"; import { SSRFError, validateEventTypePattern, validateSSRF } from "@eep-dev/validator"; import { withConditional } from "./conditional.js"; @@ -271,7 +272,7 @@ export class EEPServer { if (!access.granted) { const payload = await build402Response(this.gateConfig, resource, proofs); - return { status: 402, body: payload }; + return { status: 402, headers: { "Content-Type": PROBLEM_JSON_CONTENT_TYPE }, body: payload }; } return { @@ -381,7 +382,7 @@ export class EEPServer { }); if (!access.granted) { const payload: AccessRestrictionResponse = await build402Response(this.gateConfig, sentinelResource, proofs); - return { status: 402, body: payload }; + return { status: 402, headers: { "Content-Type": PROBLEM_JSON_CONTENT_TYPE }, body: payload }; } } } @@ -650,6 +651,7 @@ export class EEPServer { get402Handler(resource: string, proofs: GateProof[]): Promise { return build402Response(this.gateConfig, resource, proofs).then((body) => ({ status: 402, + headers: { "Content-Type": PROBLEM_JSON_CONTENT_TYPE }, body })); } diff --git a/schemas/v0.1/gate.402-response.json b/schemas/v0.1/gate.402-response.json index 493ca0c..0730727 100644 --- a/schemas/v0.1/gate.402-response.json +++ b/schemas/v0.1/gate.402-response.json @@ -1,111 +1,146 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://eep.dev/schemas/v0.1/gate.402-response.json", - "title": "EEP Access Restriction Response (402)", - "description": "Schema for the HTTP 402 response body returned when an agent requests a resource that requires a higher tier. The response is machine-readable so agents can programmatically determine what requirements to satisfy.", - "type": "object", - "required": [ - "error", - "resource", - "current_tier", - "required_tier", - "unmet_requirements" - ], - "additionalProperties": false, - "properties": { - "error": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://eep.dev/schemas/v0.1/gate.402-response.json", + "title": "EEP Access Restriction Response (402)", + "description": "Schema for the HTTP 402 response body returned when an agent requests a resource that requires a higher tier. The response is machine-readable so agents can programmatically determine what requirements to satisfy. Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members.", + "type": "object", + "required": [ + "error", + "resource", + "current_tier", + "required_tier", + "unmet_requirements" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier.", + "format": "uri", + "default": "https://eep.dev/problems/payment-required", + "examples": [ + "https://eep.dev/problems/payment-required" + ] + }, + "title": { + "type": "string", + "description": "RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence \u2014 vary `detail`, not `title`.", + "examples": [ + "Payment Required" + ] + }, + "status": { + "type": "integer", + "description": "RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response.", + "enum": [ + 402 + ], + "examples": [ + 402 + ] + }, + "detail": { + "type": "string", + "description": "RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user." + }, + "instance": { + "type": "string", + "description": "RFC 9457 URI reference identifying this specific occurrence.", + "format": "uri-reference" + }, + "error": { + "type": "string", + "description": "Error code. Always 'access_restricted'.", + "const": "access_restricted" + }, + "resource": { + "type": "string", + "description": "The resource pattern that was requested but not accessible.", + "examples": [ + "profile.contact", + "content.papers.full_text", + "events.premium" + ] + }, + "current_tier": { + "type": "string", + "description": "The tier the agent currently has access to.", + "examples": [ + "public", + "free" + ] + }, + "required_tier": { + "type": "string", + "description": "The minimum tier required to access the requested resource." + }, + "unmet_requirements": { + "type": "array", + "description": "List of requirements the agent has not yet satisfied. Each entry includes the requirement type, the specific fields needed, and an optional resolution hint for the agent.", + "items": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "type": "string", - "description": "Error code. Always 'access_restricted'.", - "const": "access_restricted" - }, - "resource": { - "type": "string", - "description": "The resource pattern that was requested but not accessible.", - "examples": [ - "profile.contact", - "content.papers.full_text", - "events.premium" - ] - }, - "current_tier": { + "description": "The requirement type that was not met." + }, + "resolution_hint": { "type": "string", - "description": "The tier the agent currently has access to.", + "description": "A human/agent-readable hint on how to satisfy this requirement.", + "maxLength": 512, "examples": [ - "public", - "free" + "Pay $0.10 via the payment_methods URL", + "Present a VC of type AcademicAffiliation", + "Your trust score is 32; minimum required is 50", + "Follow this entity to gain access" ] + } }, - "required_tier": { - "type": "string", - "description": "The minimum tier required to access the requested resource." - }, - "unmet_requirements": { + "additionalProperties": true + } + }, + "available_tiers": { + "type": "object", + "description": "Map of tiers that would grant access to the requested resource. Includes their labels, requirements, and access patterns so agents can choose which tier to satisfy.", + "additionalProperties": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "description": { + "type": "string" + }, + "requirements": { "type": "array", - "description": "List of requirements the agent has not yet satisfied. Each entry includes the requirement type, the specific fields needed, and an optional resolution hint for the agent.", "items": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "description": "The requirement type that was not met." - }, - "resolution_hint": { - "type": "string", - "description": "A human/agent-readable hint on how to satisfy this requirement.", - "maxLength": 512, - "examples": [ - "Pay $0.10 via the payment_methods URL", - "Present a VC of type AcademicAffiliation", - "Your trust score is 32; minimum required is 50", - "Follow this entity to gain access" - ] - } - }, - "additionalProperties": true + "type": "object" } - }, - "available_tiers": { - "type": "object", - "description": "Map of tiers that would grant access to the requested resource. Includes their labels, requirements, and access patterns so agents can choose which tier to satisfy.", - "additionalProperties": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "requirements": { - "type": "array", - "items": { - "type": "object" - } - }, - "access": { - "type": "array", - "items": { - "type": "string" - } - } - } + }, + "access": { + "type": "array", + "items": { + "type": "string" } - }, - "gates_config_url": { - "type": "string", - "description": "URL where the full gate configuration can be retrieved.", - "format": "uri", - "examples": [ - "https://api.example.com/eep/gates/did:web:example.com:u:alice" - ] - }, - "retry_after": { - "type": "integer", - "description": "Optional: seconds to wait before retrying (e.g., if a time-based requirement will be met soon).", - "minimum": 0 + } } + } + }, + "gates_config_url": { + "type": "string", + "description": "URL where the full gate configuration can be retrieved.", + "format": "uri", + "examples": [ + "https://api.example.com/eep/gates/did:web:example.com:u:alice" + ] + }, + "retry_after": { + "type": "integer", + "description": "Optional: seconds to wait before retrying (e.g., if a time-based requirement will be met soon).", + "minimum": 0 } -} \ No newline at end of file + } +} diff --git a/schemas/v0.1/gate.403-response.json b/schemas/v0.1/gate.403-response.json index af2fcfc..283461b 100644 --- a/schemas/v0.1/gate.403-response.json +++ b/schemas/v0.1/gate.403-response.json @@ -1,65 +1,100 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://eep.dev/schemas/v0.1/gate.403-response.json", - "title": "EEP Gate 403 Forbidden Response", - "description": "Returned when a credential, agreement, identity, or allowlist gate prevents access", - "type": "object", - "required": [ - "error", - "resource", - "current_tier", - "required_tier", - "unmet_requirements" - ], - "additionalProperties": false, - "properties": { - "error": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://eep.dev/schemas/v0.1/gate.403-response.json", + "title": "EEP Gate 403 Forbidden Response", + "description": "Returned when a credential, agreement, identity, or allowlist gate prevents access Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members.", + "type": "object", + "required": [ + "error", + "resource", + "current_tier", + "required_tier", + "unmet_requirements" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier.", + "format": "uri", + "default": "https://eep.dev/problems/access-restricted", + "examples": [ + "https://eep.dev/problems/access-restricted" + ] + }, + "title": { + "type": "string", + "description": "RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence \u2014 vary `detail`, not `title`.", + "examples": [ + "Access Restricted" + ] + }, + "status": { + "type": "integer", + "description": "RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response.", + "enum": [ + 403 + ], + "examples": [ + 403 + ] + }, + "detail": { + "type": "string", + "description": "RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user." + }, + "instance": { + "type": "string", + "description": "RFC 9457 URI reference identifying this specific occurrence.", + "format": "uri-reference" + }, + "error": { + "type": "string", + "const": "access_forbidden", + "description": "Fixed error code distinguishing from payment restriction (402)" + }, + "resource": { + "type": "string", + "description": "The resource path or pattern that was requested" + }, + "current_tier": { + "type": "string", + "description": "The tier the requesting agent currently holds" + }, + "required_tier": { + "type": "string", + "description": "The minimum tier needed to access the resource" + }, + "unmet_requirements": { + "type": "array", + "description": "List of specific requirements not satisfied", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "type": "string", - "const": "access_forbidden", - "description": "Fixed error code distinguishing from payment restriction (402)" - }, - "resource": { + "examples": [ + "credential", + "identity", + "allowlist", + "reciprocal" + ] + }, + "resolution_hint": { "type": "string", - "description": "The resource path or pattern that was requested" - }, - "current_tier": { - "type": "string", - "description": "The tier the requesting agent currently holds" - }, - "required_tier": { - "type": "string", - "description": "The minimum tier needed to access the resource" - }, - "unmet_requirements": { - "type": "array", - "description": "List of specific requirements not satisfied", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "examples": [ - "credential", - "identity", - "allowlist", - "reciprocal" - ] - }, - "resolution_hint": { - "type": "string", - "description": "Machine-readable instruction for how to satisfy this requirement" - } - } - } - }, - "gates_config_url": { - "type": "string", - "format": "uri", - "description": "URL to full gate configuration for the entity" + "description": "Machine-readable instruction for how to satisfy this requirement" + } } + } + }, + "gates_config_url": { + "type": "string", + "format": "uri", + "description": "URL to full gate configuration for the entity" } -} \ No newline at end of file + } +} diff --git a/schemas/v0.1/gate.429-response.json b/schemas/v0.1/gate.429-response.json index 50ad32c..58a224b 100644 --- a/schemas/v0.1/gate.429-response.json +++ b/schemas/v0.1/gate.429-response.json @@ -1,72 +1,107 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://eep.dev/schemas/v0.1/gate.429-response.json", - "title": "EEP Rate-Limit Response (429)", - "description": "Schema for HTTP 429 Too Many Requests response body returned by EEP publishers implementing DID-based token-bucket rate limiting. See SPECIFICATION.md §3.4.6 and Whitepaper §10.5.", - "type": "object", - "required": [ - "error", - "did_rate_limit_key", - "retry_after_seconds", - "window_reset_at", - "signed_challenge" - ], - "additionalProperties": false, - "properties": { - "error": { - "type": "string", - "description": "Machine-readable error code. Always 'rate_limited' for 429 responses.", - "const": "rate_limited" - }, - "did_rate_limit_key": { - "type": "string", - "description": "The DID that was rate-limited. DID-based limiting (not IP-based) per EEP spec.", - "pattern": "^did:[a-z0-9]+:.+$", - "examples": [ - "did:web:agent.example.com" - ] - }, - "retry_after_seconds": { - "type": "integer", - "description": "Number of seconds the agent must wait before retrying. MUST match the Retry-After HTTP response header value.", - "minimum": 1, - "maximum": 86400 - }, - "window_reset_at": { - "type": "string", - "description": "ISO 8601 UTC timestamp when the rate-limit window resets.", - "format": "date-time", - "examples": [ - "2026-03-05T13:00:00Z" - ] - }, - "signed_challenge": { - "type": "string", - "description": "A publisher-signed challenge token the agent MUST include as the X-EEP-RL-Challenge header on its retry request. Prevents IP-rotation evasion. Format: base64url(nonce + '.' + publisher_did + '.' + expires_at + '.' + signature).", - "minLength": 16, - "maxLength": 512, - "examples": [ - "v1.eyJub25jZSI6ImFiYzEyMyIsImV4cCI6MTc0MTM3MDQwMH0.MEQCIGFiY2Q...", - "v1.b64u_payload.b64u_signature" - ] - }, - "limit_per_window": { - "type": "integer", - "description": "The total number of requests allowed per window for this DID (informational).", - "minimum": 0 - }, - "requests_made": { - "type": "integer", - "description": "Number of requests this DID has made in the current window (informational).", - "minimum": 0 - }, - "message": { - "type": "string", - "description": "Human-readable explanation of the rate limit.", - "maxLength": 256, - "examples": [ - "DID did:web:agent.example.com has exceeded the rate limit of 100 requests per minute." - ] - } + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://eep.dev/schemas/v0.1/gate.429-response.json", + "title": "EEP Rate-Limit Response (429)", + "description": "Schema for HTTP 429 Too Many Requests response body returned by EEP publishers implementing DID-based token-bucket rate limiting. See SPECIFICATION.md \u00a73.4.6 and Whitepaper \u00a710.5. Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members.", + "type": "object", + "required": [ + "error", + "did_rate_limit_key", + "retry_after_seconds", + "window_reset_at", + "signed_challenge" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier.", + "format": "uri", + "default": "https://eep.dev/problems/rate-limited", + "examples": [ + "https://eep.dev/problems/rate-limited" + ] + }, + "title": { + "type": "string", + "description": "RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence \u2014 vary `detail`, not `title`.", + "examples": [ + "Too Many Requests" + ] + }, + "status": { + "type": "integer", + "description": "RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response.", + "enum": [ + 429 + ], + "examples": [ + 429 + ] + }, + "detail": { + "type": "string", + "description": "RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user." + }, + "instance": { + "type": "string", + "description": "RFC 9457 URI reference identifying this specific occurrence.", + "format": "uri-reference" + }, + "error": { + "type": "string", + "description": "Machine-readable error code. Always 'rate_limited' for 429 responses.", + "const": "rate_limited" + }, + "did_rate_limit_key": { + "type": "string", + "description": "The DID that was rate-limited. DID-based limiting (not IP-based) per EEP spec.", + "pattern": "^did:[a-z0-9]+:.+$", + "examples": [ + "did:web:agent.example.com" + ] + }, + "retry_after_seconds": { + "type": "integer", + "description": "Number of seconds the agent must wait before retrying. MUST match the Retry-After HTTP response header value.", + "minimum": 1, + "maximum": 86400 + }, + "window_reset_at": { + "type": "string", + "description": "ISO 8601 UTC timestamp when the rate-limit window resets.", + "format": "date-time", + "examples": [ + "2026-03-05T13:00:00Z" + ] + }, + "signed_challenge": { + "type": "string", + "description": "A publisher-signed challenge token the agent MUST include as the X-EEP-RL-Challenge header on its retry request. Prevents IP-rotation evasion. Format: base64url(nonce + '.' + publisher_did + '.' + expires_at + '.' + signature).", + "minLength": 16, + "maxLength": 512, + "examples": [ + "v1.eyJub25jZSI6ImFiYzEyMyIsImV4cCI6MTc0MTM3MDQwMH0.MEQCIGFiY2Q...", + "v1.b64u_payload.b64u_signature" + ] + }, + "limit_per_window": { + "type": "integer", + "description": "The total number of requests allowed per window for this DID (informational).", + "minimum": 0 + }, + "requests_made": { + "type": "integer", + "description": "Number of requests this DID has made in the current window (informational).", + "minimum": 0 + }, + "message": { + "type": "string", + "description": "Human-readable explanation of the rate limit.", + "maxLength": 256, + "examples": [ + "DID did:web:agent.example.com has exceeded the rate limit of 100 requests per minute." + ] } + } } diff --git a/schemas/v0.1/gate.451-response.json b/schemas/v0.1/gate.451-response.json index c63fde6..b137183 100644 --- a/schemas/v0.1/gate.451-response.json +++ b/schemas/v0.1/gate.451-response.json @@ -1,51 +1,86 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://eep.dev/schemas/v0.1/gate.451-response.json", - "title": "EEP Gate 451 Legally Restricted Response", - "description": "Returned when a resource is unavailable for legal reasons (EU AI Act, DORA, judicial orders, etc.)", - "type": "object", - "required": [ - "error", - "resource", - "reason" - ], - "additionalProperties": false, - "properties": { - "error": { - "type": "string", - "const": "legally_restricted", - "description": "Fixed error code for HTTP 451 legal restriction" - }, - "resource": { - "type": "string", - "description": "The resource path that is legally restricted" - }, - "reason": { - "type": "string", - "description": "Human-readable description of the legal restriction" - }, - "legal_basis": { - "type": "string", - "description": "Legal instrument imposing the restriction", - "examples": [ - "EU AI Act Art. 6 Annex III", - "DORA Art. 28", - "GDPR Art. 17" - ] - }, - "jurisdiction": { - "type": "string", - "description": "Geographic or legal jurisdiction", - "examples": [ - "EU", - "US", - "UK", - "DE" - ] - }, - "contact": { - "type": "string", - "description": "Contact address (email or URL) for legal queries" - } + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://eep.dev/schemas/v0.1/gate.451-response.json", + "title": "EEP Gate 451 Legally Restricted Response", + "description": "Returned when a resource is unavailable for legal reasons (EU AI Act, DORA, judicial orders, etc.) Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members.", + "type": "object", + "required": [ + "error", + "resource", + "reason" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier.", + "format": "uri", + "default": "https://eep.dev/problems/legally-restricted", + "examples": [ + "https://eep.dev/problems/legally-restricted" + ] + }, + "title": { + "type": "string", + "description": "RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence \u2014 vary `detail`, not `title`.", + "examples": [ + "Unavailable For Legal Reasons" + ] + }, + "status": { + "type": "integer", + "description": "RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response.", + "enum": [ + 451 + ], + "examples": [ + 451 + ] + }, + "detail": { + "type": "string", + "description": "RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user." + }, + "instance": { + "type": "string", + "description": "RFC 9457 URI reference identifying this specific occurrence.", + "format": "uri-reference" + }, + "error": { + "type": "string", + "const": "legally_restricted", + "description": "Fixed error code for HTTP 451 legal restriction" + }, + "resource": { + "type": "string", + "description": "The resource path that is legally restricted" + }, + "reason": { + "type": "string", + "description": "Human-readable description of the legal restriction" + }, + "legal_basis": { + "type": "string", + "description": "Legal instrument imposing the restriction", + "examples": [ + "EU AI Act Art. 6 Annex III", + "DORA Art. 28", + "GDPR Art. 17" + ] + }, + "jurisdiction": { + "type": "string", + "description": "Geographic or legal jurisdiction", + "examples": [ + "EU", + "US", + "UK", + "DE" + ] + }, + "contact": { + "type": "string", + "description": "Contact address (email or URL) for legal queries" } -} \ No newline at end of file + } +} diff --git a/tests/conformance-fixtures/gates/402-problem-details.expected.json b/tests/conformance-fixtures/gates/402-problem-details.expected.json new file mode 100644 index 0000000..390256b --- /dev/null +++ b/tests/conformance-fixtures/gates/402-problem-details.expected.json @@ -0,0 +1,4 @@ +{ + "valid": true, + "reason": "RFC 9457 members alongside the EEP extension members (SPECIFICATION.md \u00a73.3.1)" +} diff --git a/tests/conformance-fixtures/gates/402-problem-details.input.json b/tests/conformance-fixtures/gates/402-problem-details.input.json new file mode 100644 index 0000000..fbaaa32 --- /dev/null +++ b/tests/conformance-fixtures/gates/402-problem-details.input.json @@ -0,0 +1,19 @@ +{ + "type": "https://eep.dev/problems/payment-required", + "title": "Payment Required", + "status": 402, + "detail": "Access to 'content.papers.full_text' requires the 'premium' tier.", + "instance": "/eep/content/content.papers.full_text", + "error": "access_restricted", + "resource": "content.papers.full_text", + "current_tier": "public", + "required_tier": "premium", + "unmet_requirements": [ + { + "type": "payment", + "amount": 1, + "currency": "usd", + "per": "request" + } + ] +} diff --git a/tests/conformance-fixtures/manifest.json b/tests/conformance-fixtures/manifest.json index a5045a8..fb409f7 100644 --- a/tests/conformance-fixtures/manifest.json +++ b/tests/conformance-fixtures/manifest.json @@ -180,6 +180,17 @@ "shape": "json-pair", "asserts_valid": true }, + { + "id": "gates-402-problem-details", + "category": "gates", + "tier": "Standard", + "spec_section": "\u00a73.3.1 Error responses (RFC 9457)", + "schema": "schemas/v0.1/gate.402-response.json", + "input": "gates/402-problem-details.input.json", + "expected": "gates/402-problem-details.expected.json", + "shape": "json-pair", + "asserts_valid": true + }, { "id": "subscription-valid-webhook-request", "category": "subscription", diff --git a/tests/types/eep-schemas.d.ts b/tests/types/eep-schemas.d.ts index c9bc4d0..90040ec 100644 --- a/tests/types/eep-schemas.d.ts +++ b/tests/types/eep-schemas.d.ts @@ -1401,9 +1401,29 @@ export interface EEPEventEnvelope { // gate.402-response.json // ──────────────────────────────────────────────────────── /** - * Schema for the HTTP 402 response body returned when an agent requests a resource that requires a higher tier. The response is machine-readable so agents can programmatically determine what requirements to satisfy. + * Schema for the HTTP 402 response body returned when an agent requests a resource that requires a higher tier. The response is machine-readable so agents can programmatically determine what requirements to satisfy. Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members. */ export interface EEPAccessRestrictionResponse402 { + /** + * RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier. + */ + type?: string; + /** + * RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence — vary `detail`, not `title`. + */ + title?: string; + /** + * RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response. + */ + status?: 402; + /** + * RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user. + */ + detail?: string; + /** + * RFC 9457 URI reference identifying this specific occurrence. + */ + instance?: string; /** * Error code. Always 'access_restricted'. */ @@ -1461,9 +1481,29 @@ export interface EEPAccessRestrictionResponse402 { // gate.403-response.json // ──────────────────────────────────────────────────────── /** - * Returned when a credential, agreement, identity, or allowlist gate prevents access + * Returned when a credential, agreement, identity, or allowlist gate prevents access Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members. */ export interface EEPGate403ForbiddenResponse { + /** + * RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier. + */ + type?: string; + /** + * RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence — vary `detail`, not `title`. + */ + title?: string; + /** + * RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response. + */ + status?: 403; + /** + * RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user. + */ + detail?: string; + /** + * RFC 9457 URI reference identifying this specific occurrence. + */ + instance?: string; /** * Fixed error code distinguishing from payment restriction (402) */ @@ -1511,9 +1551,29 @@ export interface EEPGate403ForbiddenResponse { // gate.429-response.json // ──────────────────────────────────────────────────────── /** - * Schema for HTTP 429 Too Many Requests response body returned by EEP publishers implementing DID-based token-bucket rate limiting. See SPECIFICATION.md §3.4.6 and Whitepaper §10.5. + * Schema for HTTP 429 Too Many Requests response body returned by EEP publishers implementing DID-based token-bucket rate limiting. See SPECIFICATION.md §3.4.6 and Whitepaper §10.5. Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members. */ export interface EEPRateLimitResponse429 { + /** + * RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier. + */ + type?: string; + /** + * RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence — vary `detail`, not `title`. + */ + title?: string; + /** + * RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response. + */ + status?: 429; + /** + * RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user. + */ + detail?: string; + /** + * RFC 9457 URI reference identifying this specific occurrence. + */ + instance?: string; /** * Machine-readable error code. Always 'rate_limited' for 429 responses. */ @@ -1552,9 +1612,29 @@ export interface EEPRateLimitResponse429 { // gate.451-response.json // ──────────────────────────────────────────────────────── /** - * Returned when a resource is unavailable for legal reasons (EU AI Act, DORA, judicial orders, etc.) + * Returned when a resource is unavailable for legal reasons (EU AI Act, DORA, judicial orders, etc.) Served as `application/problem+json` per RFC 9457; the EEP-specific members below are problem extension members. */ export interface EEPGate451LegallyRestrictedResponse { + /** + * RFC 9457 problem type URI identifying what went wrong. Dereferencing it SHOULD yield human-readable documentation. Clients match on this rather than on the HTTP status, which is why it is the stable identifier. + */ + type?: string; + /** + * RFC 9457 short, human-readable summary of the problem type. MUST NOT change from occurrence to occurrence — vary `detail`, not `title`. + */ + title?: string; + /** + * RFC 9457 HTTP status code, repeated in the body so the problem document survives being logged or forwarded away from its response. + */ + status?: 451; + /** + * RFC 9457 human-readable explanation specific to this occurrence. Intended for a developer reading a log, not for an end user. + */ + detail?: string; + /** + * RFC 9457 URI reference identifying this specific occurrence. + */ + instance?: string; /** * Fixed error code for HTTP 451 legal restriction */