@certrev/cert-block 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/README.md +135 -8
  3. package/dist/builder/card-contract.d.ts +143 -0
  4. package/dist/builder/card-contract.d.ts.map +1 -0
  5. package/dist/builder/card-contract.js +262 -0
  6. package/dist/builder/card-contract.js.map +1 -0
  7. package/dist/builder/card-report.d.ts +37 -0
  8. package/dist/builder/card-report.d.ts.map +1 -0
  9. package/dist/builder/card-report.js +43 -0
  10. package/dist/builder/card-report.js.map +1 -0
  11. package/dist/builder/cert-review-card.d.ts +7 -5
  12. package/dist/builder/cert-review-card.d.ts.map +1 -1
  13. package/dist/builder/cert-review-card.js +227 -18
  14. package/dist/builder/cert-review-card.js.map +1 -1
  15. package/dist/builder/contract-card.d.ts +53 -0
  16. package/dist/builder/contract-card.d.ts.map +1 -0
  17. package/dist/builder/contract-card.js +141 -0
  18. package/dist/builder/contract-card.js.map +1 -0
  19. package/dist/builder/delivered-face.d.ts +83 -0
  20. package/dist/builder/delivered-face.d.ts.map +1 -0
  21. package/dist/builder/delivered-face.js +135 -0
  22. package/dist/builder/delivered-face.js.map +1 -0
  23. package/dist/builder/delivery-runtime.d.ts +15 -1
  24. package/dist/builder/delivery-runtime.d.ts.map +1 -1
  25. package/dist/builder/delivery-runtime.js +26 -1
  26. package/dist/builder/delivery-runtime.js.map +1 -1
  27. package/dist/builder/index.d.ts +7 -1
  28. package/dist/builder/index.d.ts.map +1 -1
  29. package/dist/builder/index.js +8 -1
  30. package/dist/builder/index.js.map +1 -1
  31. package/dist/builder/ssr-seed.d.ts +31 -1
  32. package/dist/builder/ssr-seed.d.ts.map +1 -1
  33. package/dist/builder/ssr-seed.js +38 -4
  34. package/dist/builder/ssr-seed.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +3 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/reader/reader-css.generated.d.ts +1 -1
  40. package/dist/reader/reader-css.generated.d.ts.map +1 -1
  41. package/dist/reader/reader-css.generated.js +1 -1
  42. package/dist/reader/reader-css.generated.js.map +1 -1
  43. package/dist/reader/render.d.ts.map +1 -1
  44. package/dist/reader/render.js +71 -34
  45. package/dist/reader/render.js.map +1 -1
  46. package/dist/reader/tokens.d.ts +2 -0
  47. package/dist/reader/tokens.d.ts.map +1 -1
  48. package/dist/reader/tokens.js +28 -6
  49. package/dist/reader/tokens.js.map +1 -1
  50. package/dist/verify/get-verified-delivery.d.ts +56 -0
  51. package/dist/verify/get-verified-delivery.d.ts.map +1 -1
  52. package/dist/verify/get-verified-delivery.js +49 -4
  53. package/dist/verify/get-verified-delivery.js.map +1 -1
  54. package/dist/verify/key-set.d.ts +67 -0
  55. package/dist/verify/key-set.d.ts.map +1 -0
  56. package/dist/verify/key-set.js +190 -0
  57. package/dist/verify/key-set.js.map +1 -0
  58. package/package.json +115 -112
  59. package/src/builder/card-contract.ts +314 -0
  60. package/src/builder/card-report.ts +61 -0
  61. package/src/builder/cert-review-card.tsx +332 -26
  62. package/src/builder/contract-card.tsx +159 -0
  63. package/src/builder/delivered-face.ts +156 -0
  64. package/src/builder/delivery-runtime.ts +32 -1
  65. package/src/builder/index.tsx +16 -1
  66. package/src/builder/ssr-seed.ts +56 -4
  67. package/src/index.ts +12 -0
  68. package/src/reader/reader-css.generated.ts +1 -1
  69. package/src/reader/reader.css +297 -2
  70. package/src/reader/render.ts +87 -38
  71. package/src/reader/tokens.ts +29 -6
  72. package/src/verify/get-verified-delivery.ts +85 -4
  73. package/src/verify/key-set.ts +225 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,236 @@ change the rendered cert output and never break a compiling integration** (regis
5
5
  exports, types). Anything that would alter what a visitor sees, or require integration changes,
6
6
  is announced loudly here first.
7
7
 
8
+ ## 2.0.0 · 2026-10-21
9
+
10
+ Major: **the contract-v1 Builder client. Install it once; from then on every visual, copy, layout, schema, fold and
11
+ dialog change to the card ships from CertREV's server, with no package release and no install.** CertREV's server
12
+ (contract v1, portal ADR 0001) paints the card as a signed CARD DOCUMENT; this release prints that document verbatim
13
+ and computes nothing visual of its own.
14
+
15
+ **What a brand does, once:** bump `@certrev/cert-block` to `^2.0.0` and add four sources to the storefront's
16
+ Content-Security-Policy (README, "The contract-v1 card (2.0.0)"). Same import, same `certRevCertComponent`, same block
17
+ name (`CertREV Cert`), same inputs, no new prop, no config.
18
+
19
+ - **Which card a placement gets is decided by the SIGNED binding, per placement.** The Builder Delivery response may
20
+ carry the card document as an unsigned top-level sibling, `card`, beside `payload`, `signature`, `renderDef` and
21
+ `face`. The signed `payload.content.faceDigest` binds it with two keys 1.3.x ignores, `contract: 1` and `doc` (the
22
+ lowercase hex sha256 of the document's RFC 8785 bytes): `{ v: 1, defVersion, html, css, contract: 1, doc }`.
23
+ - **No `contract` key** (every brand until CertREV turns contract v1 on for it, from its side): the card paints
24
+ **exactly what 1.3.1 paints**, byte for byte (the delivered face, the reader face, the legacy face, the options
25
+ face, the records), whatever else the response carries.
26
+ - **`contract: 1`**: the block prints the document only when it is a well-formed contract-1 card document that hashes
27
+ (WebCrypto sha256 over cert-contract's `canonicalizeJson`) to the signed `doc` and names the signed design version,
28
+ the envelope's certificate and this Builder entry (`subject: { platform: 'builder', externalId }`). Otherwise it
29
+ prints **nothing** and reports why: `card_missing` (no card served), `card_verify_failed` (malformed, another
30
+ digest, design version, certificate or entry), `card_pin_mismatch` (CertREV's replica pinned another design
31
+ version). Never a broken card, and never a face of its own for such a placement.
32
+ - **Any other `contract` value, or a malformed contract-1 binding**: nothing, and `card_unsupported_contract`.
33
+ - **What a block prints:** the block's part hosts in page order and, in the block holding the document's first part
34
+ (`top`, else `rail`, else `pill`), the document's head fragment (`<style data-certrev-card-head>`: the pill's fixed box
35
+ and the fonts) before them, CertREV's two dialog hosts (the profile and the certificate: CertREV-styled, identical on
36
+ every brand and CMS, blind to the brand's design) and, once CertREV serves it, the hosted runtime's `<script src
37
+ integrity crossorigin="anonymous" defer>` after them, inside the card's wrapper
38
+ (`data-certrev-face="card"`, `data-certrev-contract="1"`, `data-certrev-def-version`, the seed). Every byte inside the
39
+ wrapper but the runtime tag is the document's own. Each part is a `<certrev-card>` host with a declarative shadow root:
40
+ the browser's parser attaches it, so the card is complete with JavaScript off and no theme rule reaches inside it. The
41
+ saved design decides the layout (the block's `mode` is not read): a banner block prints the card and its memo (`part:
42
+ 'header'` the card only, `part: 'memo'` the memo only); a sidebar or floating block prints its rail or pill, and a
43
+ `memo` block outside the banner prints nothing (a page that kept split blocks after the brand chose a sidebar shows one
44
+ card). A page prints the head, the dialogs and the runtime once (ADR 0001's head once per page, as the lead ruled): a
45
+ split banner's header block carries them, its memo block prints the memo only, and the runtime opens a memo control's
46
+ dialog from the header block (it finds a dialog anywhere on the page by the placement). No block coordinates with
47
+ another, so this holds with JavaScript off.
48
+ - **In the browser.** Hydration keeps the server's card as it stands (same node, no read). The seed (the 1.3.1 seed with
49
+ the card appended last) lets React's own re-renders (its recovery from a hydration error elsewhere on the page, a
50
+ record streamed in after the card rendered, a later mount of the same placement) print the card again with no read,
51
+ after the browser has re-checked the seed's card against the seed payload's binding; a seed card that fails is
52
+ never printed and is reported. A card React inserts (an in-app navigation, Builder's editor) gets its roots attached by
53
+ the card: `setHTMLUnsafe`, else `attachShadow` from each template with its mode and options. The runtime, when the
54
+ document names one, is injected once per page into `document.head` by the block that prints its tag (React does not
55
+ run a script it inserts), unless it already runs (`Symbol.for('certrev.card-runtime')` on `document`).
56
+ - **Builder's wrappers and the sidebar card.** Builder renders each block in a flex item of a flex column, which contains
57
+ the rail's float, so no copy could sit beside the sidebar card. `BUILDER_CARD_FLOW_CSS` (printed after the wrapper,
58
+ only with a rail, only above the card's 820px breakpoint, only with `:has()`) lets the float out of Builder's
59
+ wrappers, as 1.2.0-canary.6 did for the reader face.
60
+ - **`clientRead` now defaults to `true` (behaviour change, no new prop).** A card the client mounts with no record on the
61
+ page reads the Delivery API once from the browser: it is the only way the server's card exists after an in-app
62
+ navigation, and it is the read the CSP's `connect-src https://portal.certrev.com` allows. A read the policy blocks did
63
+ not reach CertREV, so the block paints its options face exactly as 1.3.1 painted it (and reports
64
+ `delivery_unreachable`). `clientRead={false}` restores 1.2.0 to 1.3.1's behaviour (no browser read). A card the client
65
+ renders while the document is still loading first waits for a record the server may still stream (1.2.1), with or
66
+ without `clientRead`, and reads only if the page loads without one; through 1.3.1 an explicit `clientRead` read at
67
+ once and could miss that record. Every read sends no cookie (`credentials: 'omit'`).
68
+ - **The trust root is a key SET, so a key rotation needs no reinstall.** A kid this package bakes (`cert-issuer-1`)
69
+ resolves as before. Any other kid makes one read of the delivery origin's published key set,
70
+ `GET <origin>/api/cert/v1/keys` (`credentials: 'omit'`; a browser at most once per page and origin, a server once per
71
+ origin per hour, a failure remembered for ten minutes; in a browser a failed read is not retried on that page), accepted only when a baked kid signs it (Ed25519 over the RFC
72
+ 8785 bytes of `{ kind: 'certrev.keyset', v: 1, issuedAt, keys: [{ kid, crv: 'Ed25519', x }] }`), it is not older
73
+ than the set already accepted from that origin, and it still lists, as the same key, the kid that signed it. Anything
74
+ else leaves the kid unknown: `unknown_key`, nothing painted, as in 1.3.x. Today every envelope is signed by the baked
75
+ kid, so nothing is fetched.
76
+ - **No silent fallback.** Each fallback is one structured report: in the browser
77
+ `CustomEvent('certrev:card-fallback', { detail: { reason, platform: 'builder', externalId, certId? } })` on
78
+ `document`, once per placement and reason per page; on a server one `console.warn` line, `[certrev] card_fallback
79
+ {json}`; and a contract-1 `'none'` record names its `reason`, so a browser hydrating it reports it too. Reported as
80
+ well, with nothing painted differently, where the read that met them ran (the server's log for a server render, the
81
+ event for a browser read): CertREV not reached (`delivery_unreachable`, whether the block then paints its options face
82
+ or has none to paint) and an unknown kid (`unknown_key`). The card sends nothing anywhere: no beacon, no storage, no
83
+ cookie.
84
+ - **React 19.** The card suspends a server render once, on its read: the read itself computes the card's digest (and,
85
+ for 1.3.0's delivered face, its hashes) before it is cached. Under React 19, `use` matches promises by their order in a
86
+ render, so a second suspension (on the digest, after the read) was handed the read's result: the card printed nothing
87
+ (`card_verify_failed`), and through 1.3.1 a delivered face silently fell back to the card's own face. The replica runs
88
+ on React 18 and on React 19 (`CERT_BLOCK_E2E_REACT`). React 19 also streams any Suspense boundary larger than its
89
+ `progressiveChunkSize` (12,800 bytes by default) after the shell; a host that wants the card complete for readers with
90
+ JavaScript off raises it (README, "React 19 hosts").
91
+ - **Found and fixed before release (a break-it pass).** A page with more cards than the digest memo held (32 documents;
92
+ 64 delivered faces in 1.3.0's memo, which had the same flaw) never finished rendering, because a retry found its
93
+ pending digest evicted: both memos now drop only settled entries. A split header and memo pair whose memo block had
94
+ nothing to print wrote a record that took the header block's card down on hydration: such a block now prints nothing
95
+ and writes no record. `Object.hasOwn` was replaced (Safari before 15.4). A runtime `src` must be on `certrev.com`.
96
+ - **New API, additive.** `./builder`: `acceptedCardBinding`, `parseCardDocument`, `cardDocumentDigest`, `cardPartsFor`,
97
+ `CardDocumentV1`, `CardBinding`, `CardPartName`, `CARD_FALLBACK_EVENT`, `CardFallbackDetail`, `CardFallbackReason`,
98
+ `BUILDER_CARD_FLOW_CSS`. Root: `keySetResolver`, `verifyKeySet`, `KEY_SET_PATH` and their types. `VerifiedDelivery` and
99
+ `CachedDelivery` gain an optional `card` (the sibling exactly as served, UNVERIFIED: check it as the card does).
100
+ - **Unchanged:** the registration, its inputs and the props (a test compares them with 1.3.1's own build, and the
101
+ compiler checks the props), every 1.3.1 face and record for a placement without a contract binding (every 1.3.1
102
+ golden and byte test passes unmodified; the six tests of the no-browser-read default now pass `clientRead={false}`),
103
+ the design pin's own rules on that path, the web component, the storefront modal bundle, and
104
+ `@certrev/cert-contract` `^0.6.0` (the binding is two keys in the already-signed `faceDigest`).
105
+ - **Tests.** `builder/__tests__/card-contract.test.ts` (the binding, the shape check, the digest held equal to the
106
+ portal's own on its five real documents, the parts, the printed bytes), `contract-card.test.tsx` (server: every
107
+ branch, the printed bytes, the reports), `contract-card-client.test.tsx` (jsdom: hydration, client mount, the seed's
108
+ re-check, root attachment, the runtime), `registration-1.x.test.ts` + `registration-props.test-d.ts` (the 1.x
109
+ registration and props), `verify/__tests__/key-set.test.ts` (every refusal, one read per origin, a rotation that
110
+ renders). `e2e/` (`pnpm test:replica`, Playwright Chromium): a Hydrogen-shaped Builder replica at 1280 and 393 px.
111
+
112
+ ## 1.3.1 · 2026-10-21
113
+
114
+ Patch: **a Builder card on the reader face now paints the same card as Shopify and WordPress.** The portal's reader
115
+ face moved on after 1.2.0 (the face this package copied was the portal's of 2026-10-06); 1.3.1 copies its current
116
+ state, so the card's design and behaviour are uniform across CMSs. The sentence at the top of this file says patches
117
+ never change the rendered cert output: **this one does, on purpose, for the reader face only** (a render def that
118
+ says `face: 'reader'`), by bringing it to the bytes the portal's Shopify snippets paint today. **No integration
119
+ change is needed** (same exports, types, registration, inputs and seed), and **the delivered-face path of 1.3.0 is
120
+ unchanged**: its gate, hashes, seed, wrapper and the 1.2.1 fallbacks work as before. The legacy face
121
+ (`renderCertBlock`), the options-only block, the web component and the storefront modal bundle are untouched; a
122
+ pinned card keeps its pin rules and paints the caught-up face for its pinned reader def. A card that paints a
123
+ delivered face (1.3.0) paints the server's markup and is not affected at all.
124
+
125
+ **This is the fallback face**, for sites that still read the render def themselves and paint the reader face from it
126
+ in their own page. It is a catch-up and nothing more: no new feature of the own face comes with it (the face's "View
127
+ Profile" and "View Certificate" controls are the same links carrying `data-certrev-modal-open` as before, opened by the
128
+ host's modal script as in 1.3.0). The next release is planned to be the contract-v1 client, which prints the server's
129
+ signed card document instead of painting a face of its own.
130
+
131
+ What a reader sees now, on a reader-face card:
132
+
133
+ - **The sidebar card folds on a phone.** At 640px and under, its bio and memo are hidden and shown again inside a
134
+ closed `<details class="certrev-r-more">` behind "Read the expert memo" ("About the expert" when there is a bio and
135
+ no memo; no fold with neither), exactly as the Shopify extension and the WordPress plugin (1.8.3) paint it; wider
136
+ than 640px the card is as before. The scope line, the compensation cue and both links stay outside the fold. A
137
+ 469-character memo no longer shows in full on a 375px phone. (`sidebar()` in `reader/render.ts`.)
138
+ - **The rung is honoured** (banner, memo card, sidebar): `credentialed` prints no memo, bio or photo (so no memo card;
139
+ the banner is the memo-less card with the scope line and the profile and certificate row), `named` also drops the
140
+ credential, `minimal` is the accent band and the links only, and any other value is the full card (the memo
141
+ rung), as before. The floating pill is the same on every rung. (`readFacts`, `bannerCard`, `bannerMemo`,
142
+ `sidebar`.)
143
+ - **A toned face.** A def with an ink (`tokens.inkColor`) or a surface that is not white (`tokens.surface`) writes
144
+ `--cr-sf` and `--cr-ink` on the card and adds the class `certrev-r-toned` to every section root; the stylesheet
145
+ paints the surface and ink from them. (`readerTokens`, `isTonedStyle` in `reader/tokens.ts`; `sectionOpen`.)
146
+ - **A def with no design paints the memo-rung defaults**: accent `#0a1b3f` and corner `12px` (it was `#0A1B3F` and
147
+ `14px`). A def that sets its own accent and corner is unchanged. (`reader/tokens.ts`.)
148
+ - **The banner's writer takes the same 42px face as the reviewer**, photo or initials, at every width (it was the
149
+ 36px `sm` face). A photo the browser cannot load is still repainted as its person's initials: the person is now
150
+ read from the byline ("Written by" or "Reviewed by"), not from the face's size class. (`bannerCard`;
151
+ `photoPerson` in `builder/cert-review-card.tsx`.)
152
+ - **The banner's credential date never breaks**: it sits in `<span class="certrev-r-nw">`, and the credential line
153
+ breaks only between its parts. (`verifiedLine` in `reader/render.ts`.)
154
+ - **The stylesheet is the portal's current one** (`reader/reader.css` and `READER_CSS`, 42.9 KB, from portal
155
+ `d62ae100b3`; was 31.6 KB): the banner card splits evenly between the writer and the reviewer on a shared
156
+ baseline (821px and up), the certified date and the cue sit on one full-width row, left-justified, under a hairline a
157
+ theme cannot hide, and the footer stacks at 560px and under; the expert memo card on a phone puts the avatar and
158
+ name on one row with a full-width quote and one links row, and a theme's `blockquote` rules cannot bleed into
159
+ it; the sidebar fold and its 44px toggle survive a theme's `details` rules; a toned face's border resets hold.
160
+ The sheet the card inlines (without the 40 remote font faces) grows from 14.8 KB to 26.2 KB before compression.
161
+ - **Unchanged:** the profile and certificate links (`rel` stays `CERTREV_LINK_REL`, the one byte the face does not
162
+ share with the Liquid), the floating pill's markup, the compensation cue and scope line wording, the avatars' 46px
163
+ reviewer face on the memo card and the sidebar, the seed and the wrapper, and every non-reader face.
164
+ - **Tests.** `scripts/refresh-reader.mjs --portal <portal> --oracle` re-copied the stylesheet, the 11 golden pages and
165
+ the Liquid harness's output; the golden, differential and link-rel tests pass byte for byte on all of it. The
166
+ harness now also paints 155 more edge cells (every rung, the sidebar fold's variants, toned faces, a face with no
167
+ design: 399 cells in all, was 244), and `reader/__tests__/reader-one-face.test.ts` spells those rules out.
168
+ Hand-written expectations that pinned the 2026-10-06 face were brought to the portal's current Liquid (the
169
+ writer's `sm` avatar, the credential date's span, the default accent and corner, the stylesheet's hash). The
170
+ reader entries of `__golden__/delivered-face-1.2.1.json` and `delivered-face-client-1.2.1.json` were re-recorded
171
+ from this version; everything in them outside the stylesheet and the reader sections (the wrapper and the seed) is
172
+ still 1.2.1's bytes, and the legacy entries are untouched. New: a failed writer photo repaints the writer, not the
173
+ reviewer (`delivery-card-hydrate.test.tsx`).
174
+
175
+ ## 1.3.0 · 2026-10-18
176
+
177
+ Minor: **a `delivery` card can paint the face CertREV rendered for the brand's design, when the signed envelope binds
178
+ it.** Backward compatible: **with no delivered face, or an envelope without `faceDigest`, the card renders exactly what
179
+ 1.2.1 renders, byte for byte** (the server's HTML and its seed, hydration, a streamed record, a fallback record, a
180
+ `clientRead` read). A block without the marker and a pinned card are untouched, and no integration needs a code
181
+ change. Like 1.1.0's marker, the new face is switched on from CertREV's side, never by the upgrade: it takes an
182
+ envelope minted with a `faceDigest` (`@certrev/cert-contract` 0.6.0) AND a Delivery response serving the face it hashes.
183
+
184
+ - **A delivered face, bound by a signed digest.** The Builder Delivery response may carry an unsigned top-level
185
+ `face: { html, css }` beside `payload`, `signature` and `renderDef`. The signature does not cover it; the signed
186
+ `payload.content.faceDigest` (`{ v: 1, defVersion, html, css }`, the lowercase hex sha256 of each half's UTF-8
187
+ bytes for one saved design version) binds it. The card paints the delivered face only when ALL of these hold: the
188
+ verdict is `render`; the card is not pinned; the block shows the whole card (below); the verified content carries a
189
+ `faceDigest` with `v: 1`; a well-formed face was served; a `renderDef` was served and its `v` is
190
+ `faceDigest.defVersion`; and sha256(html) and sha256(css), computed with WebCrypto (`crypto.subtle.digest`, the
191
+ global the kernel already verifies with), equal the signed values. It then paints `<style>` + css + `</style>` +
192
+ html inside the card's usual wrapper, which keeps `data-certrev-cert-chrome`, `data-certrev-delivery`,
193
+ `data-certrev-strings-version`, `data-certrev-def-version` and the seed, and gains
194
+ `data-certrev-face="delivered"`. It paints in place of the legacy or reader face, with no reader stylesheet and no
195
+ sidebar flow rules.
196
+ - **A split banner keeps its own halves.** The delivered face is a whole design, so only a block whose `part` is
197
+ absent or `'full'` (or null, or a value `renderCertBlock` does not recognise, which it paints as the full card)
198
+ paints it. A block with `part: 'header'` or `part: 'memo'` paints its own part as 1.2.1 does, and hashes nothing.
199
+ The block's `mode` is not consulted: the delivered face carries the design's own layout.
200
+ - **The browser re-checks the face before it paints it.** A server that painted the delivered face ends the seed with
201
+ the same `face`: `{ v: 1, baseUrl, externalId, payload, signature?, renderDef, face }`. A seed without one is the
202
+ 1.2.1 seed byte for byte. The browser repaints the face from the seed (hydration, a later client mount, a record
203
+ streamed in while the page loads) only after hashing it against the seed payload's own `faceDigest`, for the
204
+ seed's def; a `clientRead` read is judged as the server judges it. A seed with no `face`, a malformed one, or one
205
+ whose hashes fail paints what 1.2.1 paints for it. While a card hydrates over the server's delivered face and the
206
+ browser cannot confirm it (no WebCrypto, as on an insecure `http:` page), it adopts the server's element without
207
+ rendering that markup, and the render right after hydration replaces it with the face 1.2.1 paints, with no
208
+ hydration error. A card hydrating a delivered face suspends once while WebCrypto hashes it, and keeps the server's
209
+ HTML on screen meanwhile; the hashes are memoized by the face's content, so one face is hashed once per process.
210
+ - **Page weight.** The face is on the page twice: painted, and in the seed, where JSON and attribute escaping add
211
+ between a few percent and about 60% (more for quote- and markup-dense HTML). A face the size of the 1.2.1 reader
212
+ card (about 43 KB with its stylesheet) adds about 46 KB to the seed, uncompressed. The delivered face replaces the
213
+ card's own face, so the painted half costs about what the reader face did.
214
+ - **Fails closed as before.** A digest rewritten in transit fails the signature, so the card renders nothing and
215
+ writes a `"none"` record, as for any tampering. Markup whose bytes do not hash to the signed digest is never
216
+ painted, and a WebCrypto failure counts as a mismatch.
217
+ - **New API, all additive.** Root: `parseDeliveredFace` (the face sibling's shape check, as `parseRenderDef` is the
218
+ def's), the `DeliveredFace` type and the `CertFaceDigest` contract type. `VerifiedDelivery` and `CachedDelivery`
219
+ gain an optional `face`, present only on a `render` verdict whose response carried a well-formed face, so a read
220
+ with no face is the 1.2.1 object exactly. **`face` is carried unverified:** a `getVerifiedDelivery` caller that
221
+ paints it must check it against the digest first, as the card does.
222
+ - **Dependency.** `@certrev/cert-contract` is now `^0.6.0` (was `^0.5.0`), for `CertFaceDigest`. The `^0.5.0` range
223
+ did not match the workspace's 0.6.0, so `pnpm -r build` on a cold tree built cert-block before cert-contract and
224
+ failed; it now builds in order.
225
+ - **Unchanged:** every face 1.2.1 paints, the seed and fallback records without a face, the stale window and the
226
+ deadline, the pinned card, the options path (no marker), the registration and inputs, the web component, and the
227
+ storefront modal bundle (`src/modal/*` and `dist/modal/certrev-cert.js`: nothing it bundles changed).
228
+ - **Tests.** `builder/__tests__/delivered-face.test.tsx` (server) and `delivered-face-client.test.tsx` (jsdom):
229
+ the delivered face on the server for the legacy, the legacy-without-hidden-fields and the reader defs (exact bytes,
230
+ the seed the 1.2.1 seed plus the face); every other case of the matrix (17 failing deliveries per def, the two
231
+ split-banner parts, the pinned card, a tombstone) against `__golden__/delivered-face-1.2.1.json`, rendered by the
232
+ unmodified 1.2.1 card; the browser's hydration (same node, no read, no error), client mount, streamed record and
233
+ `clientRead` with a face, and with a face the browser cannot confirm, against `__golden__/delivered-face-client-1.2.1.json`;
234
+ 1.2.1's server HTML hydrating as served; tampering; and the seed's `face` parse and write. `delivered-face-use.test.tsx`
235
+ pins the one promise per face a React 19 `use` replays. `get-verified-delivery.test.ts` gains the face sibling's
236
+ parse, cache and settle. No existing expectation changed.
237
+
8
238
  ## 1.2.1 · 2026-10-09
9
239
 
10
240
  Patch, stable (dist-tag `latest`): **1.2.1-canary.0, below, as a stable release.** A card the browser renders while
package/README.md CHANGED
@@ -290,6 +290,85 @@ actually makes (`mode`, `part`). The component enforces the fused-credential rul
290
290
  construction (a credential is unrenderable without its dated verification) and themes to the
291
291
  brand's render-def (accent / surface / corners / font + v2 bar-ink / body-ink).
292
292
 
293
+ ### The contract-v1 card (2.0.0)
294
+
295
+ **Install once.** From 2.0.0 the block prints the card CertREV's server renders (contract v1), so every later change to
296
+ the card's design, copy, layout, schema, memo fold or dialogs ships from CertREV with no package release and no install
297
+ on your side. The upgrade is two edits, made once:
298
+
299
+ 1. `@certrev/cert-block` `^2.0.0`. Keep the registration line as it is: same import, same `certRevCertComponent`,
300
+ same block name (`CertREV Cert`), same inputs, no new prop, no config.
301
+ 2. Four sources in the storefront's Content-Security-Policy, and nothing else from CertREV:
302
+
303
+ ```
304
+ script-src https://portal.certrev.com (the hosted card runtime: dialogs, revalidation)
305
+ connect-src https://portal.certrev.com (the card's read after an in-app navigation; the runtime's revalidation)
306
+ font-src https://assets.certrev.com (the card's fonts)
307
+ img-src https://assets.certrev.com (reviewer photos)
308
+ ```
309
+
310
+ The card also uses inline `<style>` elements and `style` attributes, as every cert-block release has, so the policy's
311
+ `style-src` must keep allowing inline styles.
312
+ 3. On a React 19 storefront only: pass `progressiveChunkSize: 1_000_000` to `renderToReadableStream` in the server entry
313
+ (Hydrogen: `app/entry.server.tsx`), once, so a reader with JavaScript off gets the card in the page (below).
314
+
315
+ **Turned on from CertREV's side, per brand.** Nothing changes on the page until CertREV signs a placement's envelope for
316
+ contract v1: until then the block paints exactly what 1.3.1 paints, byte for byte. The switch is the signed
317
+ `payload.content.faceDigest`, which then carries `contract: 1` and `doc`, the sha256 of the card document's RFC 8785
318
+ bytes; the Delivery response carries the document as its unsigned `card` sibling.
319
+
320
+ | The signed binding | What the block prints |
321
+ |---|---|
322
+ | no `contract` key | exactly what 1.3.1 prints for the same response |
323
+ | `contract: 1`, and the served `card` hashes to `doc` and names the signed design version, the certificate and this Builder entry | the card, verbatim |
324
+ | `contract: 1`, no card served | nothing; reported `card_missing` |
325
+ | `contract: 1`, any mismatch or a malformed document | nothing; reported `card_verify_failed` |
326
+ | any other `contract`, or a malformed binding | nothing; reported `card_unsupported_contract` |
327
+ | CertREV's replica pins another design version | nothing; reported `card_pin_mismatch` |
328
+
329
+ **What a block prints.** Inside the card's wrapper (`data-certrev-face="card"`): the block's part hosts in page order
330
+ and, in the block that holds the document's first part (the card on a banner, else the rail, else the pill), the head
331
+ fragment (`<style data-certrev-card-head>`) before them, CertREV's profile and certificate dialog hosts and, once CertREV
332
+ serves it, the runtime's `<script src integrity crossorigin="anonymous" defer>` after them. So a page prints each of
333
+ those once: a split banner's header block carries them and its memo block prints the memo only, whose controls open the
334
+ page's one dialog (the runtime finds it by the placement). Keep the header block on a page that has a memo block. Each part is a
335
+ `<certrev-card>` with a declarative shadow root, attached by the HTML parser: the card is complete with JavaScript off
336
+ and no theme rule reaches inside it. The saved design decides the layout, not the block's `mode`; `part` splits a banner
337
+ only (`'header'` the card, `'memo'` the memo), and a `'memo'` block on a sidebar or floating design prints nothing, so a
338
+ page keeps one card when a brand changes its design. A sidebar card's float is let out of Builder's block wrappers
339
+ (`BUILDER_CARD_FLOW_CSS`) so the article's copy flows beside it above 820px, as on every other CMS.
340
+
341
+ **In the browser.** Hydration keeps the server's card as it stands, with no read. React's own later renders of the
342
+ card print it again from the seed after re-checking it. A card the client mounts with no record on the page (an in-app
343
+ navigation, Builder's editor) reads the Delivery API once (`clientRead`, on by default from 2.0.0; `clientRead={false}`
344
+ restores 1.3.1's no-read behaviour), prints the card and attaches its roots (`setHTMLUnsafe`, else from each template),
345
+ and loads the runtime once. A read your policy blocks paints the block's options face, as 1.3.1 did.
346
+
347
+ **React 19 hosts and readers with JavaScript off.** React 19 streams a Suspense boundary larger than its
348
+ `progressiveChunkSize` (12,800 bytes by default) after the page's shell, even when it is ready at once, and only a script
349
+ moves it into place; the card (its parts, dialogs and seed) is larger than that, so a reader with JavaScript off on a
350
+ React 19 page sees no card unless the host passes a larger `progressiveChunkSize` (for example `1_000_000`) to
351
+ `renderToReadableStream`. React 18 inlines it. Readers with JavaScript and crawlers that run it see the card either way.
352
+
353
+ **Every fallback is reported**, never silent: in the browser,
354
+
355
+ ```ts
356
+ import { CARD_FALLBACK_EVENT, type CardFallbackDetail } from '@certrev/cert-block/builder'
357
+
358
+ document.addEventListener(CARD_FALLBACK_EVENT, (e) => {
359
+ const { reason, externalId, certId } = (e as CustomEvent<CardFallbackDetail>).detail
360
+ // reason: card_missing | card_verify_failed | card_unsupported_contract | card_pin_mismatch | unknown_key | delivery_unreachable
361
+ })
362
+ ```
363
+
364
+ once per placement and reason per page; on a server, one `console.warn` line `[certrev] card_fallback {json}`. The card
365
+ itself sends nothing anywhere (no beacon, no storage, no cookie: its reads use `credentials: 'omit'`).
366
+
367
+ **Key rotation needs no reinstall.** The baked kid (`cert-issuer-1`) verifies as before. An envelope signed by any
368
+ other kid makes one read of `https://portal.certrev.com/api/cert/v1/keys`, a key set the card accepts only when the baked
369
+ kid signs it (and it is not older than one already accepted, and still lists its signer). Otherwise the kid is unknown
370
+ and the card renders nothing, as in 1.3.x.
371
+
293
372
  ### The reader face (1.2.0, opt-in)
294
373
 
295
374
  certrev.com's "reader" design (a banner card plus the expert memo, a sidebar card, a floating pill) is a
@@ -303,7 +382,13 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
303
382
 
304
383
  - **Same bytes as the Liquid.** The markup is the Shopify `certrev-reader-*` snippets', and the portal's
305
384
  golden pages are copied into `src/reader/__tests__/fixtures/golden/` with their sha256, so the two
306
- cannot drift without a test saying so.
385
+ cannot drift without a test saying so. **From 1.3.1 it is the portal's one face**, the card the Shopify
386
+ extension and the WordPress plugin (1.8.3 and later) paint, so a Builder page, a Shopify page and a WordPress
387
+ page show the same card (`src/reader/__tests__/fixtures/SOURCES.json` names the portal commit the copies came
388
+ from; `pnpm refresh:reader` re-copies them and the golden and differential tests then say what the renderer must
389
+ follow). 1.2.0 to 1.3.0 painted the portal's face of 2026-10-06; the differences are listed in the CHANGELOG.
390
+ It is the **fallback face** for sites that read the def themselves: 1.3.1 only brings it level with the portal,
391
+ and adds no feature of its own.
307
392
  - **The stylesheet** is the generated reader section of `certrev-cert.css`
308
393
  (`/* CERTREV-READER-GEN:BEGIN */ ... END`), copied byte for byte as `src/reader/reader.css` (`./reader.css`)
309
394
  and as `READER_CSS`. The card inlines it once, in a `<style data-certrev-reader-css>`, inside the face's
@@ -349,12 +434,23 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
349
434
  - **Brand tokens** are an allow-list, as the Shopify and WordPress twins' are. The accent and bar ink must
350
435
  be a `#RGB` or `#RRGGBB` string; the corner radius a bare `0` or one non-negative `px`, `rem`, `em` or `%`
351
436
  length. Anything else (a named colour, `rgb()`, a negative or unitless radius, a value with a quote, a
352
- comment opener or a bracket) paints the default, navy `#0A1B3F` and `14px`, and never reaches the
353
- `style` attribute. The font is one of five stacks (`sans`, `serif`, `grotesk`, `mono`, `system`); an
354
- unknown slot paints sans.
437
+ comment opener or a bracket) paints the default, navy `#0a1b3f` and `12px` (the design guide's memo-rung defaults,
438
+ also what a face with no def paints; 1.3.0 painted `#0A1B3F` and `14px`), and never reaches the `style`
439
+ attribute. The font is one of five stacks (`sans`, `serif`, `grotesk`, `mono`, `system`); an unknown slot paints
440
+ sans. A def with an ink (`inkColor`), or a surface (`surface`) that is not white, is a **toned** face: each
441
+ valid colour is written as `--cr-sf` / `--cr-ink` on the root, which gains the class `certrev-r-toned`.
442
+ - **The rung.** The def's `rung` decides how much the card prints: `memo` (and any value the face does not know,
443
+ or none) is the whole card; `credentialed` prints no memo, bio or photo (so no memo card, and the banner is the
444
+ memo-less card with the scope line and the profile and certificate row); `named` also drops the credential;
445
+ `minimal` is the accent band and the links only. The floating pill is the same on every rung.
446
+ - **The sidebar folds on a phone.** The card's bio and memo are printed twice: as the card's own pair, and again
447
+ inside a closed `<details class="certrev-r-more">` whose toggle reads "Read the expert memo" ("About the
448
+ expert" when there is a bio and no memo; no fold with neither). At 640px and under the stylesheet hides the
449
+ pair and shows the fold; wider, the fold is hidden. The scope line, the compensation cue and both links stay
450
+ outside the fold. It is the same card as Shopify's and WordPress's (owner decision 3d0838a4).
355
451
  - **Avatars.** Every byline paints a round avatar: the person's photo when there is one to paint, else their
356
- initials, else the silhouette (a name with no initials). It is 36 px for the writer, 42 px for the banner
357
- reviewer, 46 px for the memo and sidebar reviewer. The initials and the silhouette are an inline `<svg>`, not an
452
+ initials, else the silhouette (a name with no initials). It is 42 px for the writer and the banner reviewer
453
+ alike (1.3.0 and earlier: 36 px for the writer), 46 px for the memo and sidebar reviewer. The initials and the silhouette are an inline `<svg>`, not an
358
454
  `<img>`, so a page with no photos holds no avatar image. The initials are the same bytes the portal's Liquid
359
455
  snippets and WordPress renderer print: the name trimmed and split on single spaces (empty parts skipped, so
360
456
  "Jane Doe" is "JD"), a leading dr, mr, mrs, ms, prof or mx (with or without the dot) dropped, the first character (by code point) of each of the first two parts left,
@@ -366,7 +462,8 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
366
462
  setting.
367
463
  - **Credential line.** The banner prints the reviewer's credential, a middle dot and the attribution as
368
464
  three leaves of one `<em>` (`RDN, LD, PhD · Credential verified by CertREV on May 1, 2026`); a reviewer
369
- with no displayed credential makes no verification claim. The writer's `<em>` under the name is the
465
+ with no displayed credential makes no verification claim. The date, when there is one, is wrapped in
466
+ `<span class="certrev-r-nw">` so it never breaks across lines. The writer's `<em>` under the name is the
370
467
  envelope's `author.title`, printed only when it is non-empty.
371
468
 
372
469
  ### The delivery card in the browser (1.2.0)
@@ -419,6 +516,17 @@ browser hydrates from it:
419
516
  `connect-src` for the Delivery origin; where it is blocked or fails, the card **renders nothing and logs
420
517
  nothing**, never a broken card and never the host's tree unmounted. It is not a Builder input: a brand that wants
421
518
  it sets it in code, on a wrapper of the registered component.
519
+ - **The delivered face (1.3.0).** The Delivery API may serve, beside the envelope, a face it rendered for the
520
+ brand's design: an unsigned `face: { html, css }`. The envelope's signed `content.faceDigest`
521
+ (`@certrev/cert-contract` 0.6.0) holds the sha256 of each half's UTF-8 bytes and the def version they were
522
+ rendered for. The card paints that face (`<style>` + css + `</style>` + html, in its usual wrapper, marked
523
+ `data-certrev-face="delivered"`) only for an unpinned block that shows the whole card (not a split banner's
524
+ `part: 'header'` or `'memo'`), when the def served is that version and both hashes, computed with WebCrypto,
525
+ equal the signed values. The seed then ends with the same `face`, and the browser hashes it again against the
526
+ seed payload's digest before it paints it; with no WebCrypto (an insecure `http:` page) or bytes that do not
527
+ hash, the card paints what 1.2.1 paints. Every other delivery paints exactly what 1.2.1 paints, seed included,
528
+ except that a reader-face def paints the reader face of 1.3.1 (below), not 1.2.1's.
529
+ The face is on the page twice (painted, and in the seed).
422
530
  - **The server's read** waits up to 8 s (the verifier's own default stays 3 s). Its cache keeps a certificate for
423
531
  60 s, then serves it for up to 5 minutes more while one background refresh runs or while CertREV cannot be
424
532
  reached; a refusal (revoked, expired) lands as soon as its refresh does. A cold placement waits once; a warm one
@@ -850,7 +958,26 @@ alongside each modal module. The load-bearing ones:
850
958
  - **`__tests__/webcomponent.test.tsx`**: the shared `renderBadgeHtml` string renderer (hand-escaping, unsafe-URL/color dropping, compact style) and the `<certrev-badge>` custom element (idempotent registration, SSR light-DOM preservation, client-mode fail-closed without a resolver).
851
959
  - **`builder/__tests__/builder.test.tsx`** covers the PUSH card and its registration: the wire-type ⇄ inputs parity lock, the fused-credential gate, and the mistyped-option degradation.
852
960
  - **`builder/__tests__/delivery-card-hydrate.test.tsx`** + **`builder/__tests__/ssr-seed.test.tsx`** (1.2.0): a server-rendered delivery card hydrates on both faces with zero browser reads, no mismatch and the same nodes; its seed is the served envelope and still verifies; revoked and browser-clock-expired cards come down; a later client mount reuses the page's seed; the server's options and none records are kept through hydration and through React's client re-render after a hydration error, with no read; a client mount with no record makes no read and paints the options face or nothing, and only with `clientRead` reads once, or renders nothing and logs nothing when the read is refused; a reviewer photo that fails to load repaints as initials; the seed and fallback-record parsers refuse malformed records and each other's; the design pin is part of the seed's key; a hidden `reviewerPhoto` or `memo`, or `display.showExpertPhoto: false` (the photo URLs), is absent from the whole server HTML and the seed carries no signature; every server render in these files runs under jsdom with no server switch; and the card's inlined reader stylesheet names no font URL. **`builder/__tests__/delivery-card.test.tsx`** pins the unreachable cases to the 1.0.3 options face byte for byte, and the 8 s deadline and the stale window; **`verify/__tests__/cache.test.ts`** the cache's stale-while-revalidate, stale-if-error and in-flight limit. **`reader/__tests__/reader-initials.test.ts`** pins the initials bytes (honorifics, escaping, hidden and missing photos, `showExpertPhoto`, the empty name).
961
+ - **`builder/__tests__/delivered-face.test.tsx`** + **`builder/__tests__/delivered-face-client.test.tsx`** + **`builder/__tests__/delivered-face-use.test.tsx`** (1.3.0): the delivered face paints, server and browser (hydration, a client mount over the page's record, a streamed record, `clientRead`), only when its hashes match the signed digest for the def served, on an unpinned whole-card block; every other delivery, and a browser that cannot hash, renders what the unmodified 1.2.1 card rendered for the same input, byte for byte, seed included (`__golden__/delivered-face-1.2.1.json`, `__golden__/delivered-face-client-1.2.1.json`); a tampered digest suppresses.
853
962
  - **`builder/__tests__/streamed-record-card.test.tsx`** (1.2.1): a card the client renders while the page loads paints nothing, then the record the stream adds, to the document (#421) or to the server's `<html>` after React replaced it (#423, with and without `MutationObserver`), and what a client mount paints once the page loads with none; pinned, options, none and expired records; no read; hydration, `clientRead` and a loaded page unchanged.
963
+ - **`reader/__tests__/reader-one-face.test.ts`** + **`reader-golden.test.ts`** + **`reader-differential.test.ts`** (1.3.1): the reader face is byte-equal to the portal's own Liquid harness on its 11 golden pages and 399 edge cells, among them every rung, the sidebar's phone fold, toned faces and a face with no design (`pnpm refresh:reader` re-copies them from a portal checkout).
964
+ - **`builder/__tests__/card-contract.test.ts`**, **`contract-card.test.tsx`**, **`contract-card-client.test.tsx`** (2.0.0):
965
+ the contract-1 card on the portal's own card documents (`__fixtures__/contract-v1/`, painted by its
966
+ `buildCardDocument`; `scripts/card-fixtures.portal.ts` regenerates them): the binding, the shape check, the digest held
967
+ equal to the portal's own, the printed bytes (the document's own, verbatim), every fallback and its report, hydration,
968
+ a client mount's read, the seed's re-check, root attachment and the runtime; and every 1.3.1 path unchanged.
969
+ **`registration-1.x.test.ts`** + **`registration-props.test-d.ts`** hold the registration to 1.3.1's own build and the
970
+ props to 1.3.1's (compiler-checked). **`verify/__tests__/key-set.test.ts`**: the key set's refusal rules and reads.
971
+ **`preview-parity.test.tsx`**: against the portal's own Appearance preview page for a contract-1 brand, the block
972
+ prints the preview's head byte for byte and each host's shadow tree byte for byte but the certificate id.
973
+ **`contract-wire.test.tsx`**: a Builder Delivery 200 exactly as the portal's route answered it.
974
+ **`break-it-regressions*.test.tsx`**: the pre-release break-it findings, each failing on the code before its fix.
975
+ - **`e2e/`** (`pnpm test:replica`, Playwright 1.59.1 Chromium, not part of `pnpm test`): a Hydrogen-shaped Builder replica
976
+ (streamed SSR under a strict CSP with exactly the four sources above, document hydration, an in-app navigation, and
977
+ CertREV's hosted runtime as built, `e2e/replica/card-runtime.*.js.txt`) at 1280 and 393 px: every part's shadow tree
978
+ and pixels equal the same part printed alone and the Appearance preview's own part, the dialogs (focus trapped, Escape,
979
+ focus returned), the sidebar's memo fold, a revoked placement removed and kept removed, JS off, and a CSP positive
980
+ control for each source. `CERT_BLOCK_E2E_REACT=<a node_modules with react and react-dom 19>` runs it on React 19.
854
981
  - **`modal/*.test.ts`**, per module: the modal element (tombstone → `markSuppressed`), the dialog view, interactions, placement, badge revalidation, and the Delivery-API verify client. `modal/own-face-modal.test.ts` puts two faces on one page and proves each control opens its own face's modal; `modal/closed-container-face.test.ts` proves a face that loads inside a closed `<details>`, tab or accordion is left alone and renders when opened (there is no runtime disclosure guard).
855
982
  - **`__tests__/readme-version.test.ts`** + **`__tests__/readme-contract.test.ts`** pin the claims in this file that rot silently: the version stamp against `package.json`, the key id, the route-example heading, and every `exports` subpath appearing under Public API.
856
983
 
@@ -859,7 +986,7 @@ published at `@certrev/cert-block/fixtures`).
859
986
 
860
987
  ## Version
861
988
 
862
- `1.2.1`: see [CHANGELOG.md](./CHANGELOG.md) for the release history and the semver contract
989
+ `2.0.0`: see [CHANGELOG.md](./CHANGELOG.md) for the release history and the semver contract
863
990
  (patch/minor never change the rendered cert output or break a compiling integration). Publishes
864
991
  **publicly** to npm as `@certrev/cert-block` (`publishConfig.access: public`) via GitHub Actions
865
992
  trusted publishing (OIDC); the internal `@certrev` GitHub-Packages channel mirrors it.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * The contract-1 card a Builder block prints (2.0.0): the server's card document, bound by the signed digest
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * CertREV's contract v1 (portal ADR 0001) moves the card to the server: one renderer paints every part of a placement
7
+ * into a CARD DOCUMENT, and every CMS client prints those parts verbatim. Nothing visual, no string a reader sees and
8
+ * no layout is computed here. The Builder Delivery response carries the document as an UNSIGNED top-level sibling,
9
+ * `card`, beside `payload`, `signature`, `renderDef` and `face`. What binds it is the signed
10
+ * `payload.content.faceDigest`, which for a contract-1 Builder placement is
11
+ *
12
+ * { v: 1, defVersion, html, css, contract: 1, doc }
13
+ *
14
+ * where `doc` is the lowercase hex sha256 of the document's RFC 8785 bytes (cert-contract's `canonicalizeJson`) and
15
+ * `html`/`css` bind 1.3.x's delivered face, which this client no longer reads for such a placement.
16
+ *
17
+ * THE SCHEMA IS THE PORTAL'S (`src/lib/cert-delivery/card-document.ts`, `CardDocumentV1`; source read at portal
18
+ * v1-core d5e459b685). This module mirrors only the fields a client reads, read-only, and never redefines them: the
19
+ * digest covers the document's exact bytes, so `parseCardDocument` returns the object it was given, never a copy.
20
+ *
21
+ * Which path a verified delivery takes is decided by the SIGNED binding alone (`acceptedCardBinding`):
22
+ * - no `contract` key in the digest (every brand without CertREV's contract-1 entitlement): the 1.3.1 path, unchanged;
23
+ * - a well-formed contract-1 binding: the card, only when the served document hashes to `doc` and names the same
24
+ * design version, certificate and placement (`cardBinds`); anything else prints nothing and is reported;
25
+ * - a `contract` key that is not a well-formed contract-1 binding (a later contract, a malformed digest): nothing, and
26
+ * reported. A client refuses a contract it does not support (ADR 0001, section 2).
27
+ *
28
+ * React-free. Hashing uses WebCrypto (`crypto.subtle.digest`), as the kernel and the 1.3.0 face gate do, so it runs
29
+ * wherever the card verifies (Node SSR, edge runtimes, a browser in a secure context); the card owns the suspension.
30
+ */
31
+ import { type CertContent, type CertPayload } from '../contract/kernel.js';
32
+ /** The contract this client prints. */
33
+ export declare const CARD_CONTRACT_VERSION = 1;
34
+ /** The part names a document can hold, by where they go: banner card and memo, the sidebar rail, the floating pill. */
35
+ export declare const CARD_PART_NAMES: readonly ["top", "bottom", "rail", "pill"];
36
+ export type CardPartName = (typeof CARD_PART_NAMES)[number];
37
+ /** The layouts a document paints. */
38
+ export type CardLayout = 'banner' | 'sidebar' | 'floating';
39
+ /**
40
+ * The fields of the portal's `CardDocumentV1` this client reads. The document carries more (`renderer`, `placement`,
41
+ * `jsonld`, `lifecycle`, `cache`, `illustrative`); they are part of the digest and printed nowhere by Builder.
42
+ */
43
+ export interface CardDocumentV1 {
44
+ readonly contract: 1;
45
+ readonly kind: 'certrev.card';
46
+ readonly certId: string;
47
+ readonly subject: {
48
+ readonly platform: string;
49
+ readonly externalId: string;
50
+ };
51
+ readonly defVersion: number;
52
+ readonly layout: CardLayout;
53
+ /** Each part is one complete `<certrev-card>` host holding a declarative shadow root. */
54
+ readonly parts: Partial<Record<CardPartName, string>>;
55
+ /** CertREV's own certificate and expert-profile dialogs: complete hosts, identical on every brand and CMS. */
56
+ readonly dialogs: {
57
+ readonly expert?: string;
58
+ readonly certificate?: string;
59
+ };
60
+ readonly assets: {
61
+ /** A complete `<style data-certrev-card-head>` fragment: the pill's fixed box and the fonts. */
62
+ readonly head: string;
63
+ /** The hosted runtime (dialogs, revalidate, polyfill), present once CertREV serves it. */
64
+ readonly runtime?: {
65
+ readonly src: string;
66
+ readonly integrity: string;
67
+ };
68
+ };
69
+ }
70
+ /** The signed binding of a contract-1 card, as the verified `content.faceDigest` carries it. */
71
+ export interface CardBinding {
72
+ readonly contract: 1;
73
+ readonly defVersion: number;
74
+ readonly doc: string;
75
+ }
76
+ /**
77
+ * The signed binding, judged:
78
+ * - `null`: the verified content's digest has no `contract` key (or there is no digest): not a contract-1 placement,
79
+ * so the card paints what 1.3.1 paints;
80
+ * - a `CardBinding`: `{ v: 1, contract: 1 }` with a non-negative integer `defVersion` and a 64-char lowercase hex `doc`
81
+ * (`html` and `css` may be there or not; this client does not read them);
82
+ * - `'unsupported'`: a `contract` key with anything else (another contract, a malformed binding). Fails closed.
83
+ */
84
+ export declare function acceptedCardBinding(content: CertContent | null | undefined): CardBinding | 'unsupported' | null;
85
+ /**
86
+ * The served document, shape-checked for what this client reads: the object itself when it is a contract-1 card
87
+ * document, else null. Nothing is copied or normalised (the digest covers the object as served). Parts it does not
88
+ * know are left in place (they are part of the digest) and never printed.
89
+ */
90
+ export declare function parseCardDocument(value: unknown): CardDocumentV1 | null;
91
+ /** Lowercase hex sha256 of a document's RFC 8785 bytes; null when it cannot be canonicalized or hashed. */
92
+ export declare function cardDocumentDigest(card: unknown): Promise<string | null>;
93
+ /** One document's digest: the pending hash, and its answer once settled (null never matches). */
94
+ export interface CardDigestMemo {
95
+ readonly pending: Promise<string | null>;
96
+ settled?: {
97
+ readonly digest: string | null;
98
+ };
99
+ }
100
+ /**
101
+ * The memoized digest of one document, keyed by its canonical bytes (a seed parsed afresh from the page and the
102
+ * server's cached read of the same document share one entry). A document that cannot be canonicalized settles to null
103
+ * at once, without a key.
104
+ */
105
+ export declare function cardDigestFor(card: unknown): CardDigestMemo;
106
+ /** Test seam: forget every document's digest (a new process). */
107
+ export declare function forgetCardDigestsForTests(): void;
108
+ /**
109
+ * The synchronous half of the check: the document binds this binding, payload and placement in everything but its
110
+ * bytes (contract, design version, certificate, and the Builder placement it was painted for).
111
+ */
112
+ export declare function cardMatchesPlacement(card: CardDocumentV1, binding: CardBinding, payload: CertPayload, externalId: string): boolean;
113
+ /** The asynchronous half's verdict: the document's own digest is the signed one. */
114
+ export declare function cardDigestMatches(digest: string | null, binding: CardBinding): boolean;
115
+ /** Both halves at once (for callers outside React): the served document is the one the verified envelope signs. */
116
+ export declare function cardBinds(card: CardDocumentV1, binding: CardBinding, payload: CertPayload, externalId: string): Promise<boolean>;
117
+ /**
118
+ * The parts one Builder block prints, in page order. The saved design decides the layout (the block's `mode` is not
119
+ * read). A banner block prints its card (`top`) and its memo (`bottom`); a block whose `part` is `'header'` prints the
120
+ * card only, and one whose `part` is `'memo'` the memo only, so a split banner prints each part once. Outside the
121
+ * banner a `'memo'` block prints nothing and any other block the design's one part (the rail, the pill): a page that
122
+ * kept its split header and memo blocks after the brand chose a sidebar design shows one sidebar card, not two.
123
+ */
124
+ export declare function cardPartsFor(card: CardDocumentV1, part: unknown): CardPartName[];
125
+ /** The runtime's tag, or '' when the document names none. */
126
+ export declare function cardRuntimeTag(card: CardDocumentV1): string;
127
+ /**
128
+ * The document's first part: `top`, else `rail`, else `pill`, from the document's own parts (the lead's ruling on ADR
129
+ * 0001's head-once-per-page, relay 074dbd2c8ef3); `bottom` only for a document with none of those, so its head and
130
+ * dialogs always have a block to print them. Null for a document with no part.
131
+ */
132
+ export declare function cardFirstPart(card: CardDocumentV1): CardPartName | null;
133
+ /** Whether the block printing `names` prints the page's head fragment, dialogs and runtime: it holds the first part. */
134
+ export declare function cardCarriesAssets(card: CardDocumentV1, names: readonly CardPartName[]): boolean;
135
+ /**
136
+ * Exactly what a block prints inside its wrapper: its parts in page order and, only in the block holding the document's
137
+ * first part (`cardCarriesAssets`), the head fragment before them and both dialogs (expert, then certificate) and the
138
+ * runtime's tag after. So a split banner (a header block and a memo block) prints the head, the dialogs and the runtime
139
+ * once, in the header block, with no coordination between the blocks and with JavaScript off; the runtime finds a
140
+ * control's dialog anywhere on the page by its placement. Every byte but the runtime tag is the document's own.
141
+ */
142
+ export declare function cardHtml(card: CardDocumentV1, names: readonly CardPartName[]): string;
143
+ //# sourceMappingURL=card-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-contract.d.ts","sourceRoot":"","sources":["../../src/builder/card-contract.ts"],"names":[],"mappings":"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"}