eudi-rp-request-lint / index.html
getreadystack's picture
Upload index.html with huggingface_hub
1907b46 verified
Raw History Blame Contribute Delete
47.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>EUDI Wallet relying party rules — check OpenID4VP presentation</title>
<meta name="description" content="This browser tool checks OpenID4VP presentation requests for client registration, stated purpose, minimized claims, encrypted response, and live registration.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/eudi-rp-request-lint"></head>
<body>
<div class="wrap">
<h1>EUDI Wallet relying party rules — check OpenID4VP presentation</h1>
<p class="lede">Lint OpenID4VP presentation requests against the EU Digital Identity Wallet relying-party rules: registered client_id, a stated purpose, minimised claims, encrypted response, live registration. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">{
"response_type": "vp_token",
"client_id": "https://pay.shop-example.eu",
"response_mode": "direct_post",
"response_uri": "https://pay.shop-example.eu/oid4vp/response",
"state": "a91f",
"registration_valid_until": "2026-08-31",
"client_metadata": {
"client_name": "Shop Example Payments"
},
"dcql_query": {
"credentials": [
{
"id": "pid_basic",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["urn:eudi:pid:1"] },
"claims": [
{ "path": ["birth_date"] },
{ "path": ["resident_address"] }
]
},
{
"id": "loyalty",
"format": "mso_mdoc",
"meta": { "doctype_value": "com.example.loyalty.card" }
}
]
}
}</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Lint one OpenID4VP presentation request and get every relying-party gap with the article it comes from.. <strong>In the editor:</strong> Scan every request file in the workspace at once and export a dated relying-party evidence report you keep. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var EUDI_RULES = [
{"id": "client_id_unregistered", "sev": "error",
"title": "client_id is not a registered relying-party identifier",
"source": "Implementing Regulation (EU) 2025/848, Art. 3 + OpenID4VP Client Identifier Prefixes",
"why": "A wallet resolves the relying party from the prefix of client_id. A bare URL carries no registration entry, so a conformant wallet cannot show who is asking and may refuse the request."},
{"id": "purpose_absent", "sev": "error",
"title": "Credential query has no purpose string",
"source": "Regulation (EU) 2024/1183, Art. 5b(5)",
"why": "The relying party must inform the user which attributes it needs and for what purpose. A DCQL credential entry without a purpose leaves the wallet consent screen blank."},
{"id": "whole_credential_asked", "sev": "error",
"title": "Request asks for a whole credential instead of named claims",
"source": "Regulation (EU) 2024/1183, Art. 5b(3) data minimisation",
"why": "A credential query with no claims array releases every attribute in the credential. Registration under Implementing Regulation (EU) 2025/848 lists the attributes you may request, and an open query does not match it."},
{"id": "birth_date_over_age_flag", "sev": "warn",
"title": "birth_date requested where an age attestation exists",
"source": "Regulation (EU) 2024/1183, Art. 5b(3)",
"why": "The PID carries age_equal_or_over flags for exactly this case. Asking for the date of birth when a boolean answers the question is more data than the purpose needs."},
{"id": "response_unencrypted", "sev": "error",
"title": "response_mode direct_post sends the vp_token unencrypted",
"source": "EUDI Wallet Architecture and Reference Framework, response encryption",
"why": "direct_post posts the presentation in the clear to the response endpoint. The wallet profile expects direct_post.jwt so the vp_token is encrypted to the relying party key."},
{"id": "nonce_absent", "sev": "error",
"title": "No nonce in the authorization request",
"source": "OpenID4VP, replay protection",
"why": "Without a nonce the presentation is not bound to this request and a captured vp_token can be replayed against your endpoint."},
{"id": "request_unsigned", "sev": "warn",
"title": "Authorization request is neither signed nor fetched by request_uri",
"source": "EUDI Wallet Architecture and Reference Framework, signed request objects",
"why": "An unsigned request cannot be tied to the certificate behind your client_id, so the wallet has nothing to check the registered identity against."},
{"id": "registration_expired", "sev": "error",
"title": "Relying-party registration or certificate window has passed",
"source": "Implementing Regulation (EU) 2025/848, national register of relying parties",
"why": "A registration date already in the past means the register no longer backs this request. Wallets check the register before releasing attributes."},
{"id": "unknown_credential_type", "sev": "warn",
"title": "Credential type is not an EUDI-recognised identifier",
"source": "EUDI Wallet Architecture and Reference Framework, PID and mDL identifiers",
"why": "A private vct or doctype value is not a wallet-issued attestation, so nothing in the EUDI trust list will satisfy this query."}
]
;</script>
<script>/* EUDI Wallet Request Lint — one brain, used by the extension, the report and the web page. */
(function () {
'use strict';
var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.EUDI_RULES;
var CLIENT_ID_PREFIXES = [
'x509_san_dns:', 'x509_san_uri:', 'x509_hash:',
'decentralized_identifier:', 'verifier_attestation:', 'openid_federation:'
];
var KNOWN_TYPES = [
'urn:eudi:pid:1', 'eu.europa.ec.eudi.pid.1',
'urn:eudi:mdl:1', 'org.iso.18013.5.1.mDL',
'urn:eudi:ehic:1', 'urn:eudi:pda1:1'
];
var AGE_CLAIMS = ['birth_date', 'birthdate', 'age_birth_year', 'age_in_years', 'birth_year'];
var DATE_KEYS = ['registration_valid_until', 'valid_until', 'certificate_valid_until', 'registration_expires'];
function lineOf(lines, needle) {
for (var i = 0; i < lines.length; i++) { if (lines[i].indexOf(needle) !== -1) return i + 1; }
return 1;
}
function flat(text) { return String(text).replace(/\s+/g, ' '); }
function daysBetween(a, b) {
return Math.round((Date.parse(b + 'T00:00:00Z') - Date.parse(a + 'T00:00:00Z')) / 86400000);
}
function isDate(s) { return typeof s === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(s); }
function walk(node, visit, path) {
path = path || [];
if (node === null || typeof node !== 'object') return;
if (Object.prototype.toString.call(node) === '[object Array]') {
for (var i = 0; i < node.length; i++) walk(node[i], visit, path.concat(String(i)));
return;
}
for (var k in node) {
if (!Object.prototype.hasOwnProperty.call(node, k)) continue;
visit(k, node[k], path.concat(k), node);
walk(node[k], visit, path.concat(k));
}
}
function credentialQueries(doc) {
var out = [];
var q = doc && doc.dcql_query;
if (q && Object.prototype.toString.call(q.credentials) === '[object Array]') {
for (var i = 0; i < q.credentials.length; i++) {
var c = q.credentials[i] || {};
var meta = c.meta || {};
var types = [].concat(meta.vct_values || [], meta.doctype_value ? [meta.doctype_value] : [],
meta.doctype_values || []);
out.push({ id: c.id || ('credential ' + (i + 1)), purpose: c.purpose, claims: c.claims, types: types });
}
}
var pd = doc && doc.presentation_definition;
if (pd && Object.prototype.toString.call(pd.input_descriptors) === '[object Array]') {
for (var j = 0; j < pd.input_descriptors.length; j++) {
var d = pd.input_descriptors[j] || {};
var fields = (d.constraints && d.constraints.fields) || null;
out.push({ id: d.id || ('descriptor ' + (j + 1)), purpose: d.purpose, claims: fields, types: d.doctype ? [d.doctype] : [] });
}
}
return out;
}
function claimNames(claims) {
var names = [];
if (Object.prototype.toString.call(claims) !== '[object Array]') return names;
for (var i = 0; i < claims.length; i++) {
var c = claims[i] || {};
var p = c.path || c.claim_name || c.id;
if (Object.prototype.toString.call(p) === '[object Array]') names.push(p.join('.'));
else if (typeof p === 'string') names.push(p);
if (Object.prototype.toString.call(c.path) === '[object Array]') {
for (var k = 0; k < c.path.length; k++) if (typeof c.path[k] === 'string') names.push(c.path[k]);
}
}
return names;
}
function check(text, opts) {
opts = opts || {};
var today = isDate(opts.today) ? opts.today : '';
var src = String(text == null ? '' : text);
var lines = src.split(/\r?\n/);
var one = flat(src);
var findings = [];
function add(id, msg, line) { findings.push({ check: id, sev: sevOf(id), msg: msg, line: line || 1 }); }
var doc = null;
try { doc = JSON.parse(src); } catch (e) { doc = null; }
if (!doc || typeof doc !== 'object') {
return { findings: findings, parsed: false, ruleCount: RULES.length };
}
/* 1 — registered relying-party identifier */
if (typeof doc.client_id === 'string') {
var ok = false;
for (var p = 0; p < CLIENT_ID_PREFIXES.length; p++) {
if (doc.client_id.indexOf(CLIENT_ID_PREFIXES[p]) === 0) ok = true;
}
if (!ok) {
add('client_id_unregistered',
'client_id "' + doc.client_id + '" has no registered identifier prefix (expected one of ' +
CLIENT_ID_PREFIXES.join(', ') + ')', lineOf(lines, '"client_id"'));
}
} else {
add('client_id_unregistered', 'No client_id in the request, so the wallet cannot resolve a relying party', 1);
}
var queries = credentialQueries(doc);
for (var i = 0; i < queries.length; i++) {
var q = queries[i];
var anchor = lineOf(lines, '"' + q.id + '"');
/* 2 — purpose per credential query */
if (typeof q.purpose !== 'string' || q.purpose.replace(/\s+/g, '') === '') {
add('purpose_absent', 'Credential query "' + q.id + '" states no purpose for the attributes it asks for', anchor);
}
/* 3 — whole credential asked */
var hasClaims = Object.prototype.toString.call(q.claims) === '[object Array]' && q.claims.length > 0;
if (!hasClaims) {
add('whole_credential_asked', 'Credential query "' + q.id + '" names no claims, so it releases every attribute in the credential', anchor);
}
/* 4 — date of birth where an age flag exists */
var names = claimNames(q.claims);
var wantsDob = null, hasAgeFlag = false;
for (var n = 0; n < names.length; n++) {
for (var a = 0; a < AGE_CLAIMS.length; a++) if (names[n] === AGE_CLAIMS[a]) wantsDob = names[n];
if (names[n].indexOf('age_equal_or_over') === 0 || names[n].indexOf('age_over') === 0) hasAgeFlag = true;
}
if (wantsDob && !hasAgeFlag) {
add('birth_date_over_age_flag', 'Credential query "' + q.id + '" asks for ' + wantsDob +
' — the PID carries age_equal_or_over flags for an age check', anchor);
}
/* 9 — credential type known to the wallet */
for (var t = 0; t < q.types.length; t++) {
var known = false;
for (var kt = 0; kt < KNOWN_TYPES.length; kt++) if (q.types[t] === KNOWN_TYPES[kt]) known = true;
if (!known) {
add('unknown_credential_type', 'Credential type "' + q.types[t] + '" is not an EUDI-recognised PID or attestation identifier',
lineOf(lines, q.types[t]));
}
}
}
if (queries.length === 0) {
add('whole_credential_asked', 'No dcql_query or presentation_definition, so there is no bounded attribute request to check', 1);
}
/* 5 — encrypted response */
if (doc.response_mode === 'direct_post') {
add('response_unencrypted', 'response_mode is direct_post — the vp_token is posted unencrypted; use direct_post.jwt',
lineOf(lines, '"response_mode"'));
}
/* 6 — nonce */
if (typeof doc.nonce !== 'string' || doc.nonce.length < 8) {
add('nonce_absent', 'No nonce of at least 8 characters, so the presentation is not bound to this request', 1);
}
/* 7 — signed or fetched request */
if (!doc.request_uri && !doc.request && !(doc.client_metadata && doc.client_metadata.jwks_uri)) {
add('request_unsigned', 'Neither request_uri, a signed request object nor a jwks_uri — nothing ties this request to your certificate', 1);
}
/* 8 — registration window, read against today */
walk(doc, function (key, value) {
for (var d = 0; d < DATE_KEYS.length; d++) {
if (key === DATE_KEYS[d] && isDate(value)) {
if (today && daysBetween(today, value) < 0) {
add('registration_expired', key + ' is ' + value + ', ' + Math.abs(daysBetween(today, value)) +
' days before ' + today + ' — the register no longer backs this request', lineOf(lines, '"' + key + '"'));
}
}
}
});
return { findings: findings, parsed: true, ruleCount: RULES.length };
}
function sevOf(id) {
for (var i = 0; i < RULES.length; i++) if (RULES[i].id === id) return RULES[i].sev;
return 'warn';
}
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined') module.exports = API;
if (typeof window !== 'undefined') window.EUDIENGINE = API;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var EUDIREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = EUDIREPORT;
else window.EUDIREPORT = EUDIREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.EUDIENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.EUDIREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/eudi-rp-request-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'eudi-rp-request-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/eudi-rp-request-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'eudi-rp-request-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/eudi-rp-request-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'eudi-rp-request-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/eudi-rp-request-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'eudi-rp-request-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/eudi-rp-request-lint &lt;file&gt;
npx @readystack/eudi-rp-request-lint --mcp
docker run --rm -v &quot;$PWD:/w&quot; getreadystack/eudi-rp-request-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 9 rules EUDI Wallet Request Lint (OpenID4VP) checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"eudi-rp-request-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Nine rules that read the JSON your service sends to an EU Digital Identity Wallet</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">One hour of a European contract identity engineer costs more than $29 once — this reads the same nine rules in a second.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_HqtvAPD6WprkGwCg9mwFO4Q3CgJNJWpgaAkc83iFC45" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-21">Updated 2026-09-21</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-21","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku161547_result_card.jpg" alt="EUDI Wallet Request Lint (OpenID4VP)" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Seven errors in one EUDI wallet request: that is what this linter returns for the failing sample it ships with, and if you are the identity engineer wiring wallet onboarding at a bank in the Netherlands or Germany, it is the same JSON you are writing this quarter.</p>
<p>An OpenID4VP presentation request is the document your verifier sends to a user&#x27;s EU Digital Identity Wallet. It is plain JSON: a client_id that says who is asking, a response_mode that says how the answer comes back, a nonce that binds the answer to this question, and a dcql_query that names the attributes you want out of the user&#x27;s PID. All four fields are now governed by something other than taste.</p>
<p>Regulation (EU) 2024/1183 entered into force on 2024-05-20. Implementing Regulation (EU) 2025/848, which creates the national registers of wallet relying parties, applies from 2026-12-24 — the same date by which every Member State must offer at least one wallet. From 2026-09-21 that is 94 days. Fewer than a third of Member States currently meet the readiness benchmark for that date, and that is what misleads teams: the deadline looks soft because the wallets are late, while the register your request must match is not.</p>
<p>Here are the seven errors the linter reports on the failing sample, each with the line that replaces it.</p>
<p>The client_id is &quot;https://pay.shop-example.eu&quot;. A bare URL carries no registration entry. A wallet resolves the relying party from the prefix of client_id, so the value has to read &quot;x509_san_dns:pay.shop-example.eu&quot; or one of the other registered prefixes.</p>
<p>Two credential queries state no purpose. Regulation (EU) 2024/1183 Art. 5b(5) makes the relying party tell the user which attributes it needs and what for, and a DCQL entry without a purpose string leaves the wallet consent screen with nothing to show.</p>
<p>One credential query names no claims at all. A query with no claims array asks for every attribute in the credential. Your registration lists the attributes you may request; an open query does not match it.</p>
<p>The response_mode is &quot;direct_post&quot;, which posts the vp_token in the clear. It has to be &quot;direct_post.jwt&quot; so the presentation is encrypted to your key.</p>
<p>There is no nonce, so a captured presentation can be replayed against your endpoint.</p>
<p>The registration window has passed: registration_valid_until is 2026-08-31, which is 21 days before 2026-09-21. That is the rule that makes this linter take a date rather than just a file.</p>
<p>Three more findings come back as warnings. The request asks for birth_date where the PID carries age_equal_or_over flags built for exactly that question. The request is neither signed nor fetched through a request_uri, so nothing ties it to the certificate behind the client_id. And one credential type, &quot;com.example.loyalty.card&quot;, is not an EUDI-recognised identifier, so nothing in the trust list can ever satisfy that query.</p>
<p>Nine rules, ten findings, seven of them errors. The passing sample returns zero: the rules fire on the request, not on your formatting.</p>
<p>It reads the request document and nothing else. It does not fetch your request_uri, verify a JWT signature, call a national register, or judge whether your purpose string is truthful.</p>
<p>The free version finishes a whole job: one request file, all nine rules, each finding carrying its article. No key, no watermark, no answer held back. The full version changes the scope instead — every request file in the workspace in one pass, and a dated relying-party evidence report you export and keep. It is $29 once, and one hour of a European contract identity engineer costs more than that.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku161547_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku161547_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does this actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads an OpenID4VP presentation request — the JSON your service sends to a user&#x27;s EU Digital Identity Wallet — and applies nine rules to it: registered client_id prefix, a purpose on every credential query, named claims instead of whole credentials, age flags over date of birth, encrypted response mode, nonce, signed request, live registration window, and known credential types.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Identity, payments and onboarding engineers at EU banks, marketplaces and public services that have to accept the wallet. If you are writing the verifier side of OpenID4VP against the EUDI Architecture and Reference Framework, and your relying-party registration is being prepared for the national register, this is your file.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why will a free tool not do this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A JSON schema validator says your request is valid JSON. A general linter has never read Implementing Regulation (EU) 2025/848. Chat assistants trained before the 2025 implementing acts still suggest presentation_definition with a bare URL client_id, which is exactly the pattern rule one rejects. The rules here carry the article they come from.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what is paid?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free finishes a real job: lint one request file, in the editor or on the web page, and read all nine rules with the article behind each finding. The full version changes the scope — it scans every request file in the workspace in one pass and exports a dated relying-party evidence report you keep and hand on.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost done by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">One hour of a European contract identity engineer costs more than the full version does once. Reading Implementing Regulation (EU) 2025/848 and the Architecture and Reference Framework against one request set takes longer than an hour the first time, and the reading has to happen again each time the request changes.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. EUDI Wallet Request Lint (OpenID4VP) checks the file you open against 9 rules, each naming its legal or official basis, from a rule set dated 2026-09-21, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does this actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads an OpenID4VP presentation request — the JSON your service sends to a user's EU Digital Identity Wallet — and applies nine rules to it: registered client_id prefix, a purpose on every credential query, named claims instead of whole credentials, age flags over date of birth, encrypted response mode, nonce, signed request, live registration window, and known credential types."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Identity, payments and onboarding engineers at EU banks, marketplaces and public services that have to accept the wallet. If you are writing the verifier side of OpenID4VP against the EUDI Architecture and Reference Framework, and your relying-party registration is being prepared for the national register, this is your file."}}, {"@type": "Question", "name": "Why will a free tool not do this?", "acceptedAnswer": {"@type": "Answer", "text": "A JSON schema validator says your request is valid JSON. A general linter has never read Implementing Regulation (EU) 2025/848. Chat assistants trained before the 2025 implementing acts still suggest presentation_definition with a bare URL client_id, which is exactly the pattern rule one rejects. The rules here carry the article they come from."}}, {"@type": "Question", "name": "What is free and what is paid?", "acceptedAnswer": {"@type": "Answer", "text": "Free finishes a real job: lint one request file, in the editor or on the web page, and read all nine rules with the article behind each finding. The full version changes the scope — it scans every request file in the workspace in one pass and exports a dated relying-party evidence report you keep and hand on."}}, {"@type": "Question", "name": "What would this cost done by hand?", "acceptedAnswer": {"@type": "Answer", "text": "One hour of a European contract identity engineer costs more than the full version does once. Reading Implementing Regulation (EU) 2025/848 and the Architecture and Reference Framework against one request set takes longer than an hour the first time, and the reading has to happen again each time the request changes."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. EUDI Wallet Request Lint (OpenID4VP) checks the file you open against 9 rules, each naming its legal or official basis, from a rule set dated 2026-09-21, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:161547,slug:"eudi-rp-request-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/dsa-terms-lint"><span class="yj-rel-t">DSA Terms Lint - EU Digital Services Act</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/csaf-advisory-check-cra-2026"><span class="yj-rel-t">CSAF Advisory Check for CRA 2026</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/devicetree-config-language-pack"><span class="yj-rel-t">DeviceTree Config Audit: 23 dtc-Style Rules, 50 Snippets</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/en301549-lint"><span class="yj-rel-t">EN 301 549 Lint — European Accessibility Act rules, with the clause number, in</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>