Documentation · Markdown source · Edit on GitHub · CC0 1.0

Implementation report for VRP 1.0 (Core + Well-Known URI)

Status: Draft, measured 2026-11-05; the verifier rows for V1 and V2 re-read 2026-11-08 after the verifier correction (hemmabo-mcp-server 7.0.13, vrp-verify 0.3.0; the notes say what changed); V1's §2:39 and §7:512 set to partial on 2026-11-09 after the independent reviewer reproduced two discovery-document gaps (see the notes). The correction is described in two published advisories: GHSA-4phj-whvc-8frh (hemmabo-mcp-server 7.0.12 → 7.0.13, published 2026-11-08 21:01 UTC) and GHSA-6q7g-4q6r-w7g7 (@vacationrentalprotocol/vrp-verify 0.2.0 → 0.3.0, published 2026-11-08 21:05 UTC); only those versions are confirmed affected, earlier ones were not assessed. One row per normative MUST in spec/v0.1.md (55 lines carrying MUST at e815600) and spec/well-known-uri-v0.1.md (3). It is the evidence base for the 1.0 decision record (ADR 0013, revised): a rule with fewer than two independent implementations on its side (node side or verifier side) is marked at risk there, as W3C Process §6.3.2 and §6.3.7 do. This report is data; it decides nothing.

How to read it. ✔ the implementation does what the rule says, with the evidence in the notes (a live measurement with UTC, or a file and line at a commit); ✗ it does not; – the rule does not address that implementation's role; ? not checked. Every claim below is one of those four.

Implementations

IdImplementationRoleIndependent of the reference implementer?Evidence read at
Avillaakerlyckan.se (HemmaBo, key_custody: platform)nodenolive 2026-11-05T14:46:42–14:46:57Z; hemmabo-smart-stays 879ff1e
Blakeviewhome.se (HemmaBo, key_custody: platform)nodeno (same software as A)live 2026-11-05T14:47:48–14:48:03Z
Capartmamatevz.si (CM PRO, key_custody: self)nodeyeslive 2026-11-05T14:48:04–14:48:15Z; viljemd-hub/channel-manager public/api/vrp_offer.php read 2026-11-05T14:46:59Z
V1hemmabo-mcp-server reference verifier, 7.0.13verifiernolib/vrp.ts and lib/vrp-offer-rules.ts at 32cb82f (2026-11-08; the 2026-11-05 reading was ed4cc25, 7.0.12)
V2@vacationrentalprotocol/vrp-verify 0.3.0verifier library (no network)no (written by the reference implementer's editors)src/*.js at db7b905 (2026-11-08; the 2026-11-05 reading was 0.2.0 at bd4f92e)
V3this repository's scripts (conformance.mjs, verify-offer-rules.mjs, verify-host-domain.mjs, verify-node-page.mjs)test suiteno (the standard's own)e815600
V4verify.html on vacationrentalprotocol.comverifier (browser)no (the standard's own)e815600
V5vrp-independent-verifier (Swarnabha Nandi, MIT)verifieryessrc/verifier.mjs at bc959ba (last commit 2026-08-22)

Node side: two software products (HemmaBo, CM PRO) → two independent implementations where both have ✔. Verifier side: the reference verifier and V5 are the two organizations; V2–V4 are the standard's own artifacts and do not count toward independence (W3C §6.3.2: implementations by others than the authors).

Node measurements (live, GET only)

CheckABC
discovery protocol, protocol_versionvacation-rental-protocol, 0.1samesame
canonical_domain equals the fetched host✔✔✔
jwks_url, verified_stay_offer_endpoint are node URLs✔✔✔
canonical_domain host-owned (§2 steps 1–3, vrp-verify 0.2.0 lists)✔ (2026-11-04 08:48Z)✔✔
JWKS key kty OKP, crv Ed25519, alg EdDSA, kid present, no private part✔✔✔
JWKS Cache-Controlpublic, max-age=300samesame
offer: 12 required payload fields✔✔✔
offer: signed payload equals returned offer✔✔✔
offer: valid_until − generated_at900 s900 s600 s
offer: request echo limited to the four fields (+ computed weekday/nights)✔✔✔ (no language echo)
offer: language=not-a-tag-xx is left out of the signed payload✔✔✔ (never echoed)
offer: check_in in the past → 400 checkin_in_past✔✔✔
offer: check_out before check_in → 400✔ checkout_must_be_after_checkin✔✔ invalid_range
direct_booking_url absolute https node URL of canonical_domain✔✔✔
booking.payment_options absent✔✔✔
rules.refund_schedule1 row2 rowsabsent
availability.available, price.exact, may_quote_as_official_direct_offertrue, true, truesamesame
http:// discovery → redirect to the https node URL308 ✔308 ✔301 ✔
www. discovery308 → apex ✔308 → apex ✔200 served (www is a node URL) ✔
§2.1 (a) <link rel="protocol">, (b) visible rel="protocol" link with «VRP», (d) additionalType, (e) term in own language✔ (2026-11-05T05:42:48Z)✔ (05:52:26Z)✔ (2026-11-04T08:49:14Z)

Rules addressed to nodes

Core lineRule (short)ABCIndependent (≥ 2 products)Notes
§2:37protocol MUST be vacation-rental-protocol✔✔✔yesmeasured
§2:38protocol_version MUST be 0.1✔✔✔yesmeasured; 1.0 changes this rule (ADR 0013 rev.)
§2:39canonical_domain MUST match the fetched domain✔✔✔yesmeasured
§2:41jwks_url MUST be the host-domain JWKS and a node URL✔✔✔yesmeasured
§2:42–43offer endpoint MUST be a node URL✔✔✔yesmeasured
§2:46canonical_domain MUST be host-owned (steps 1–3)✔✔✔yesmeasured with vrp-verify 0.2.0 and the 53 vectors
§2.1:121rel="protocol" href MUST NOT point to an implementation or operator✔✔✔yesmeasured: all three link vacationrentalprotocol.com
§3:142signing keys MUST use OKP/Ed25519✔✔✔yesmeasured
§3.1:165rotation: MUST publish the new key alongside the old✔ (read)✔ (read)?noA/B: api/_lib/vrp-node-key.js:93 serves active, next, retired; never exercised live (1 key each). C: not read
§5:250payload MUST include the 12 fields✔✔✔yesmeasured
§5:269–271dates MUST exist, check_out later than check_in✔✔✔yesmeasured (backwards → 400)
§5:272MUST NOT sign past or invalid dates✔✔✔yesmeasured (past → 400); C: vrp_offer.php property-date check read
§5:278MUST NOT copy other request values into the payload✔✔✔yesmeasured: bad language dropped; request keys limited
§5:280request.language MUST be a BCP 47 tag of the given form✔✔–yes (A/B) / n.a. (C)A/B: api/verified-stay-offer.ts:82–86; C never echoes language
§5:288–289generated_at MUST be signing time; valid_until MUST be later and ≤ 900 s✔✔✔yesmeasured 900 s / 900 s / 600 s; A/B verified-stay-offer.ts:133; C vrp_offer.php +10 minutes
§5.1:324booking MUST include direct_booking_url✔✔✔yesmeasured
§5.1:334MUST be an absolute https URL✔✔✔yesmeasured
§5.1:335–337MUST be a node URL of canonical_domain, MUST NOT point elsewhere✔✔✔yesmeasured
§5.2:418rails MUST NOT gate discovery, verification or offer retrieval✔✔✔yesno payment_options; all three serve offers without payment context
§5.3:448nodes MUST relay refund rows verbatim (not tiers)✔✔–no (one product)A/B: rows signed in the offer; C publishes no schedule. At risk (optional field, one product)
WK §5:89discovery MUST NOT contain secrets✔✔✔yesread: public metadata only; JWKS carries no private part

Rules addressed to verifiers and agents

Core lineRule (short)V1V2V3V4V5Independent (V1 + V5)Notes
§1:14verification MUST NOT require HemmaBo, a registry or an index✔✔✔✔✔yesV1 fetches only the node's domain; V2/V3/V5 run offline
§2:39discovery canonical_domain MUST match the fetched domainpartial lib/vrp.ts:262–267: a present canonical_domain must equal the fetched domain, but an absent one falls back to domain and then to the fetched domain itself, so a document without the field passes– (caller)✔ verify-host-domain.mjs:109 (an absent field fails)✔ verify.html:572✗noV1 gap reproduced by the independent reviewer on 2026-11-09 (Node 22, signed test offers): safe_to_quote_as_official_direct_offer: true without canonical_domain. Correction handled separately, with a regression test
§2:46/70host-owned check; no lists → unknown✔ :240–249✔ quote.js:77 (hostDomainFailures)✔✔ verify.html:465–474✗no
§2:82MUST NOT follow a redirect off node URLs✔ :193–224 (fetchJson, redirect: "manual", :215)– (caller)✔ checkRedirects (vectors)✔ verify.html:508–513 (no redirect followed)✗no
§2.1:121checker for (a)/(b) href––✔ verify-node-page.mjs:52––n.a.a page checker, not a verifier rule
§3.1:170kid MUST be opaque; MUST NOT infer from it✔✔✔✔ :395–403✔yesall select the key by kid and read nothing else into it
§3.1:174refresh-on-miss✔ (no cache: :200 no-store)––✔ (no-store)–yes (trivially)neither caches
§3.1:202JWKS cache MUST NOT exceed 5 min✔ (no cache)––✔ (no cache)–yes (trivially)
§5.1:334–341booking URL MUST be https node URL; else unknown, MUST NOT route✔ isNodeUrl :797✔ quote.js:104 (bookingUrlFailure)✔ checkNodeUrl✔ :497✗no
§5.1:351–352reference id MUST NOT be required nor substitute the signature✔✔✔✔✔yesnone reads offer_id
§5.1:355MUST ignore unknown query parameters✔✔✔✔✔yesnone parses them
§5.1:362–364MUST NOT accept a URL outside, or modified after, the JWS✔ :783 (from payload)✔ (payload)✔✔ (payload)–yes (V5 n.a.)
§5.1:371–372stale → unknown, MUST NOT present its price✔ lib/vrp-offer-rules.ts:121–136, refused in lib/vrp.ts:1047–1051✔ Status.EXPIRED (offer.js:143–144)✔ verify-offer-rules.mjs:84✔✔ fresh:falseyes
§5.2:402MUST NOT treat payment_options as a payment path✔ (routes only direct_booking_url)––––n.a. for libraries
§5.3:432refund rounding: MUST NOT substitute other rounding– (passes rows through, computes nothing)––––n.a.no implementation computes H
§5.3:442MUST NOT invent a refund promise when absent✔ :654–666 (null = unknown)––––no (one)
§5.3:448agents MUST relay rows verbatim✔ :896––––no (one)
§5.4:455/481/494class rules (verifiable / attested / reputational)✔ :930 rule text––––no (one)a renderer rule; only V1 renders
§6:500non-quoteable if valid_until missing/malformed/expired✔ lib/vrp-offer-rules.ts:121–136 (UTC form with Z), lib/vrp.ts:1047–1051✔ VALID_UNTIL_UNUSABLE, offer.js:143–144✔ :76,84✔✔ (missing → not fresh)yes
§6:502non-quoteable if generated_at missing or > 60 s ahead✔ lib/vrp-offer-rules.ts:121–136, refused in lib/vrp.ts:1047–1051 (7.0.13)✔ src/offer-rules.js:104–119, src/offer.js:143–144 (0.3.0)✔ :82✔ (:553–561)✗noV1 and V2 apply it since 2026-11-08 (releases v7.0.13 and v0.3.0); until then neither read generated_at. V5 does not; V1 and V2 are one organization
§5:269–289, §7:512lifetime ≤ 900 s, dates exist and are not past, language form, request match✔ lib/vrp-offer-rules.ts:121–172 (the stay asked for = the tool's arguments, lib/vrp.ts:1047–1051)✔ src/offer-rules.js:104–156, src/quote.js:138–143 (the stay asked for = context.request; without it asked_stay_unknown)✔ verify-offer-rules.mjs✔✗nothe offer-rules vectors (44) pass in V1 (src/vrp-offer-rules.unit.test.ts, and through the tool handler in src/vrp-offer-rules.contract.test.ts), V2 (test/offer-rules.test.mjs: parity, and through verifyOffer and quoteChecks), V3 and V4; since 2026-11-08
§7:512MUST NOT quote unless all sixteen conditions holdpartial (7.0.13): it fetches the discovery document, the JWKS and the offer itself and applies the other conditions, but two pass when the field is absent: §7:514 (canonical_domain is the fetched domain) and §7:516 (protocol_version: "0.1" declared). lib/vrp.ts:257–265 accepts the pre-0.1 field version in place of protocol_version and takes the fetched domain in place of a missing canonical_domain✔ for the library's part (key, JWS, payload match, §5/§6 lifetime, §5/§7 dates and the stay asked for from context.request, availability, exact price, booking URL, permission, §2/§7 domain from context.domain); fetching, the discovery document (protocol, protocol_version, canonical_domain of the fetched host) and transport remain the caller's, as its README says✔ (across scripts)✔✗ (signature, key, payload match, valid_until only)noV1: both gaps reproduced by the independent reviewer on 2026-11-09 (safe_to_quote_as_official_direct_offer: true with version: "0.1" and no protocol_version, and without canonical_domain); correction handled separately, with a regression test for each case. V2 alone never decides all sixteen: three conditions need the network it does not have
§7:545MUST NOT invent discounts, say bookable unless available, quote non-exact, route elsewhere✔ :31–41, :813–835; price.exact === true explicitly, :384–394 (7.0.13; 7.0.12 derived exactness from an amount)✔ quote.js:175–187✔✔✗no
§8:555MUST fail closed✔ (throws; a §5–§7 violation is OfferRuleViolation, lib/vrp.ts:125–133, answered with signature_valid: true, safe_to_quote: false)✔ (unverifiable/invalid)✔✔✔ (verified:false)yes
§9:559/563MUST distinguish affirmed / negated / unknown; MUST NOT cite unknown✔✔ Status.UNVERIFIABLE ≠ INVALID✔✔ (pass/fail/unknown)✗ (binary)no
WK §3:71clients MUST ignore unknown fields✔✔✔✔✔yesall read named fields only
WK §4:83MUST NOT treat the discovery document as proof✔–✔✔–yes (V5 n.a.)

Lines not carrying a rule: §1.1:20 (BCP 14 boilerplate), §2.1:102 (a statement about the next MAJOR), §3.1:226 (restates :202). §2:92 is MAY.

Summary for the 1.0 record

Reproduce

Nodes: GET the three documents and four offer requests listed under «Node measurements» (check_in=2027-03-10&check_out=2027-03-13&guests=2, a past check_in, a check_out before check_in, and language=not-a-tag-xx), one request per second per host. Verifiers: the file and line references above at the commits given; npm run verify-offer-rules, verify-host-domain, verify-offer-vectors, verify-node-page in this repository. V1: npm test in hemmabo-mcp-server at 32cb82f (src/vrp-offer-rules.unit.test.ts, src/vrp-offer-rules.contract.test.ts); V2: node --test in vrp-verify at db7b905 (test/offer-rules.test.mjs).