@certrev/cert-block 1.3.0 → 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.
- package/CHANGELOG.md +167 -0
- package/README.md +125 -9
- package/dist/builder/card-contract.d.ts +143 -0
- package/dist/builder/card-contract.d.ts.map +1 -0
- package/dist/builder/card-contract.js +262 -0
- package/dist/builder/card-contract.js.map +1 -0
- package/dist/builder/card-report.d.ts +37 -0
- package/dist/builder/card-report.d.ts.map +1 -0
- package/dist/builder/card-report.js +43 -0
- package/dist/builder/card-report.js.map +1 -0
- package/dist/builder/cert-review-card.d.ts +7 -5
- package/dist/builder/cert-review-card.d.ts.map +1 -1
- package/dist/builder/cert-review-card.js +163 -19
- package/dist/builder/cert-review-card.js.map +1 -1
- package/dist/builder/contract-card.d.ts +53 -0
- package/dist/builder/contract-card.d.ts.map +1 -0
- package/dist/builder/contract-card.js +141 -0
- package/dist/builder/contract-card.js.map +1 -0
- package/dist/builder/delivered-face.d.ts +9 -0
- package/dist/builder/delivered-face.d.ts.map +1 -1
- package/dist/builder/delivered-face.js +18 -5
- package/dist/builder/delivered-face.js.map +1 -1
- package/dist/builder/delivery-runtime.d.ts +15 -1
- package/dist/builder/delivery-runtime.d.ts.map +1 -1
- package/dist/builder/delivery-runtime.js +24 -1
- package/dist/builder/delivery-runtime.js.map +1 -1
- package/dist/builder/index.d.ts +3 -0
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +4 -0
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/ssr-seed.d.ts +18 -0
- package/dist/builder/ssr-seed.d.ts.map +1 -1
- package/dist/builder/ssr-seed.js +25 -3
- package/dist/builder/ssr-seed.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/reader/reader-css.generated.d.ts +1 -1
- package/dist/reader/reader-css.generated.d.ts.map +1 -1
- package/dist/reader/reader-css.generated.js +1 -1
- package/dist/reader/reader-css.generated.js.map +1 -1
- package/dist/reader/render.d.ts.map +1 -1
- package/dist/reader/render.js +71 -34
- package/dist/reader/render.js.map +1 -1
- package/dist/reader/tokens.d.ts +2 -0
- package/dist/reader/tokens.d.ts.map +1 -1
- package/dist/reader/tokens.js +28 -6
- package/dist/reader/tokens.js.map +1 -1
- package/dist/verify/get-verified-delivery.d.ts +28 -0
- package/dist/verify/get-verified-delivery.d.ts.map +1 -1
- package/dist/verify/get-verified-delivery.js +21 -2
- package/dist/verify/get-verified-delivery.js.map +1 -1
- package/dist/verify/key-set.d.ts +67 -0
- package/dist/verify/key-set.d.ts.map +1 -0
- package/dist/verify/key-set.js +190 -0
- package/dist/verify/key-set.js.map +1 -0
- package/package.json +115 -112
- package/src/builder/card-contract.ts +314 -0
- package/src/builder/card-report.ts +61 -0
- package/src/builder/cert-review-card.tsx +239 -25
- package/src/builder/contract-card.tsx +159 -0
- package/src/builder/delivered-face.ts +18 -4
- package/src/builder/delivery-runtime.ts +30 -1
- package/src/builder/index.tsx +12 -0
- package/src/builder/ssr-seed.ts +32 -3
- package/src/index.ts +9 -0
- package/src/reader/reader-css.generated.ts +1 -1
- package/src/reader/reader.css +297 -2
- package/src/reader/render.ts +87 -38
- package/src/reader/tokens.ts +29 -6
- package/src/verify/get-verified-delivery.ts +41 -2
- package/src/verify/key-set.ts +225 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,173 @@ 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
|
+
|
|
8
175
|
## 1.3.0 · 2026-10-18
|
|
9
176
|
|
|
10
177
|
Minor: **a `delivery` card can paint the face CertREV rendered for the brand's design, when the signed envelope binds
|
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 `#
|
|
353
|
-
|
|
354
|
-
unknown slot paints
|
|
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
|
|
357
|
-
|
|
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
|
|
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)
|
|
@@ -427,7 +524,8 @@ browser hydrates from it:
|
|
|
427
524
|
`part: 'header'` or `'memo'`), when the def served is that version and both hashes, computed with WebCrypto,
|
|
428
525
|
equal the signed values. The seed then ends with the same `face`, and the browser hashes it again against the
|
|
429
526
|
seed payload's digest before it paints it; with no WebCrypto (an insecure `http:` page) or bytes that do not
|
|
430
|
-
hash, the card paints what 1.2.1 paints. Every other delivery paints exactly what 1.2.1 paints, seed included
|
|
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.
|
|
431
529
|
The face is on the page twice (painted, and in the seed).
|
|
432
530
|
- **The server's read** waits up to 8 s (the verifier's own default stays 3 s). Its cache keeps a certificate for
|
|
433
531
|
60 s, then serves it for up to 5 minutes more while one background refresh runs or while CertREV cannot be
|
|
@@ -862,6 +960,24 @@ alongside each modal module. The load-bearing ones:
|
|
|
862
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).
|
|
863
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.
|
|
864
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.
|
|
865
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).
|
|
866
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.
|
|
867
983
|
|
|
@@ -870,7 +986,7 @@ published at `@certrev/cert-block/fixtures`).
|
|
|
870
986
|
|
|
871
987
|
## Version
|
|
872
988
|
|
|
873
|
-
`
|
|
989
|
+
`2.0.0`: see [CHANGELOG.md](./CHANGELOG.md) for the release history and the semver contract
|
|
874
990
|
(patch/minor never change the rendered cert output or break a compiling integration). Publishes
|
|
875
991
|
**publicly** to npm as `@certrev/cert-block` (`publishConfig.access: public`) via GitHub Actions
|
|
876
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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"}
|