@certrev/cert-block 1.3.0 → 2.0.1
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 +235 -0
- package/README.md +162 -9
- package/dist/builder/card-beacon.d.ts +73 -0
- package/dist/builder/card-beacon.d.ts.map +1 -0
- package/dist/builder/card-beacon.js +178 -0
- package/dist/builder/card-beacon.js.map +1 -0
- package/dist/builder/card-contract.d.ts +151 -0
- package/dist/builder/card-contract.d.ts.map +1 -0
- package/dist/builder/card-contract.js +286 -0
- package/dist/builder/card-contract.js.map +1 -0
- package/dist/builder/card-placement.d.ts +45 -0
- package/dist/builder/card-placement.d.ts.map +1 -0
- package/dist/builder/card-placement.js +193 -0
- package/dist/builder/card-placement.js.map +1 -0
- package/dist/builder/card-report.d.ts +49 -0
- package/dist/builder/card-report.d.ts.map +1 -0
- package/dist/builder/card-report.js +97 -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 +202 -170
- package/dist/builder/cert-review-card.js.map +1 -1
- package/dist/builder/contract-card.d.ts +70 -0
- package/dist/builder/contract-card.d.ts.map +1 -0
- package/dist/builder/contract-card.js +191 -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 +27 -3
- package/dist/builder/delivery-runtime.d.ts.map +1 -1
- package/dist/builder/delivery-runtime.js +40 -4
- package/dist/builder/delivery-runtime.js.map +1 -1
- package/dist/builder/index.d.ts +7 -4
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +8 -4
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/ssr-seed.d.ts +23 -4
- package/dist/builder/ssr-seed.d.ts.map +1 -1
- package/dist/builder/ssr-seed.js +29 -6
- package/dist/builder/ssr-seed.js.map +1 -1
- package/dist/builder/version.d.ts +3 -0
- package/dist/builder/version.d.ts.map +1 -0
- package/dist/builder/version.js +3 -0
- package/dist/builder/version.js.map +1 -0
- 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 +32 -0
- package/dist/verify/get-verified-delivery.d.ts.map +1 -1
- package/dist/verify/get-verified-delivery.js +30 -5
- 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-beacon.ts +201 -0
- package/src/builder/card-contract.ts +336 -0
- package/src/builder/card-placement.ts +208 -0
- package/src/builder/card-report.ts +126 -0
- package/src/builder/cert-review-card.tsx +285 -201
- package/src/builder/contract-card.tsx +216 -0
- package/src/builder/delivered-face.ts +18 -4
- package/src/builder/delivery-runtime.ts +46 -4
- package/src/builder/index.tsx +16 -4
- package/src/builder/ssr-seed.ts +37 -7
- package/src/builder/version.ts +2 -0
- 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 +54 -5
- package/src/verify/key-set.ts +225 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,241 @@ 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.1 · 2026-10-25
|
|
9
|
+
|
|
10
|
+
**e.l.f.'s one install.** A drop-in on 2.0.0: same package, same `certRevCertComponent` and registration (`CertREV
|
|
11
|
+
Cert`, the same 22 inputs), no new prop, no new peer dependency, and the same four Content-Security-Policy sources (the
|
|
12
|
+
beacon below goes to the delivery origin `connect-src` already allows). Everything that can change later comes from
|
|
13
|
+
CertREV's server.
|
|
14
|
+
|
|
15
|
+
- **Nothing, never unsigned facts.** Every path that painted the block's export-time options (a reviewer name, a
|
|
16
|
+
credential, a memo, a verify link nobody signed) now prints **nothing** and reports why: a block with no delivery
|
|
17
|
+
marker (`no_marker`), a marker this client does not accept or a block with no Builder entry (`marker_invalid`),
|
|
18
|
+
CertREV not reached with no verified answer inside the stale window (`delivery_unreachable`, also for a page whose
|
|
19
|
+
2.0.0 server wrote an `'options'` record), and a client mount with `clientRead={false}` and no server answer
|
|
20
|
+
(`client_read_off`). The signed paths are unchanged: a contract-1 card prints CertREV's document; an envelope with
|
|
21
|
+
no `contract` key paints the 1.3.x face from its SIGNED facts, as 2.0.0 did. The options-face renderer is gone; the
|
|
22
|
+
inputs stay registered (an entry that carries them still loads) and are not read.
|
|
23
|
+
- **A stale window inside the revocation SLA.** A verified answer is fresh for 30 s and may still paint for 30 s more
|
|
24
|
+
while it is refreshed or CertREV is down (2.0.0: 60 s and 300 s), so the card adds at most a minute to the Delivery
|
|
25
|
+
API's own cache and a revoked card is gone within ADR 0001 §7's five to six minutes (2.0.0 stacked to about twelve).
|
|
26
|
+
Past the window a read that cannot reach CertREV prints nothing; stale is never served indefinitely.
|
|
27
|
+
- **A beacon to CertREV** (`card-beacon.ts`). Each fallback, and one health event on one page view in a hundred, is a
|
|
28
|
+
`POST <delivery origin>/api/cert/v1/beacon` with a `text/plain` JSON body: `{v:1, client:'cert-block',
|
|
29
|
+
clientVersion, contract, platform:'builder', externalId, family, events:[{kind, reason, outcome}]}`. No cookies
|
|
30
|
+
(`credentials: 'omit'`), no page address or text, no user agent beyond a family (chromium, webkit, gecko, other,
|
|
31
|
+
server). At most three per page in the browser (`fetch`, `no-cors`, `keepalive`); on a server, each placement and
|
|
32
|
+
reason at most once an hour per process, with a 1.5 s deadline, never awaited. A beacon that fails is dropped; it
|
|
33
|
+
never changes what prints. The same shape serves the WordPress plugin 2.0.1 and the hosted runtime.
|
|
34
|
+
- **Forward compatible.** Unknown parts, unknown fields and new keys in known objects (how CertREV ships a minor change
|
|
35
|
+
under contract 1) are ignored and the card prints as before; a newer major (`contract` 2 or later in the signed
|
|
36
|
+
binding) prints nothing and reports `card_unsupported_contract` (beacon outcome `unsupported`); the page around it
|
|
37
|
+
always renders.
|
|
38
|
+
- **Placement hints** (`card-placement.ts`). After the page has loaded, the block moves each banner part to the place
|
|
39
|
+
`card.placement` names among the Builder entry's top-level blocks: `content-start` (after a leading image when
|
|
40
|
+
`after: 'leading-media'`), `after-byline` (an element marked `data-certrev-anchor="byline"`, else the first
|
|
41
|
+
`[rel~="author"]`, `[itemprop="author"]`, `.byline`, `[class*="byline"]` or `address`), `before-references` (else
|
|
42
|
+
`content-end`), `content-end`. An unknown anchor, `pinned`, or an anchor the page does not have leaves the part where
|
|
43
|
+
the editor put it; with JavaScript off every part is where the editor put it, and in Builder's visual editor and
|
|
44
|
+
preview nothing moves. A sidebar's rail and a floating pill never move. So CertREV can put the reviewer right after the byline and the memo at the end of the article with no
|
|
45
|
+
install.
|
|
46
|
+
- **The page's CertREV graph.** When the document carries `jsonld.graph` (a complete JSON-LD object CertREV's server
|
|
47
|
+
resolved for the placement: the Certification, its reviewer and CertREV, and the page's WebPage and Article; an
|
|
48
|
+
additive key under contract 1), the block that carries the page's assets prints it verbatim, once per page, as
|
|
49
|
+
`<script type="application/ld+json" data-certrev-graph data-certrev-external-id="<entry>">` (every `<` written as
|
|
50
|
+
`\u003c`). A document without it prints no graph. The block never builds or edits the graph; schema changes are the
|
|
51
|
+
server's. (1.x's rule that a page-builder block owns no structured data held while the block had to build the graph
|
|
52
|
+
itself; now it only prints the server's.)
|
|
53
|
+
- Dialogs are the document's own (`card.dialogs`): nothing in the package draws, styles or names them, so a redesign
|
|
54
|
+
of CertREV's profile and certificate dialogs needs no client change.
|
|
55
|
+
- **Nothing in the console.** Through 2.0.0, React's development build reported a hydration mismatch on every page with
|
|
56
|
+
a contract-1 card (the browser had already attached the card's shadow roots, which React expected as templates), and
|
|
57
|
+
in production every card React wrote itself (an in-app navigation, a re-render after the page's own hydration error)
|
|
58
|
+
drew Chrome's "Found declarative shadowrootmode attribute on a template, but declarative Shadow DOM is not being
|
|
59
|
+
parsed" warning once per template. The wrapper now skips React's hydration comparison (`suppressHydrationWarning`,
|
|
60
|
+
never printed), and in a browser that parses declarative roots the HTML React writes holds each template's mode
|
|
61
|
+
under `data-certrev-shadowrootmode` until the block re-parses the card's exact bytes, as before. The server's bytes
|
|
62
|
+
are unchanged.
|
|
63
|
+
- **A failed envelope is reported too.** An envelope that does not verify (a bad signature, an algorithm the client
|
|
64
|
+
does not accept, another platform, content that drifted from what was signed, a payload version it cannot read)
|
|
65
|
+
prints nothing, as before, and is now reported as `envelope_verify_failed` (beacon outcome `verify_failed`). Revoked,
|
|
66
|
+
expired, not found and another entry's envelope stay silent. When CertREV answers with an HTTP error, the
|
|
67
|
+
`delivery_unreachable` report carries `status` and the beacon says `http_5xx` or `http_4xx`. A beacon never sends the
|
|
68
|
+
page address (`referrerPolicy: 'no-referrer'`). Its three-per-page budget belongs to a document address, so an in-app
|
|
69
|
+
navigation starts a new one.
|
|
70
|
+
- **React 19 no longer rewrites the card.** React 19 rewrites an element's `innerHTML` whenever its
|
|
71
|
+
`dangerouslySetInnerHTML` is a new object (React 18 compared the strings), so on React 19 every re-render of a
|
|
72
|
+
contract-1 block, including the one right after hydration, replaced the card the browser had built (and left it to
|
|
73
|
+
the hosted runtime to attach the roots again). The block now keeps one object per printed string, so the server's
|
|
74
|
+
card nodes are the ones on the page after hydration, and it re-attaches any root after every commit before paint.
|
|
75
|
+
|
|
76
|
+
## 2.0.0 · 2026-10-21
|
|
77
|
+
|
|
78
|
+
Major: **the contract-v1 Builder client. Install it once; from then on every visual, copy, layout, schema, fold and
|
|
79
|
+
dialog change to the card ships from CertREV's server, with no package release and no install.** CertREV's server
|
|
80
|
+
(contract v1, portal ADR 0001) paints the card as a signed CARD DOCUMENT; this release prints that document verbatim
|
|
81
|
+
and computes nothing visual of its own.
|
|
82
|
+
|
|
83
|
+
**What a brand does, once:** bump `@certrev/cert-block` to `^2.0.0` and add four sources to the storefront's
|
|
84
|
+
Content-Security-Policy (README, "The contract-v1 card (2.0.0)"). Same import, same `certRevCertComponent`, same block
|
|
85
|
+
name (`CertREV Cert`), same inputs, no new prop, no config.
|
|
86
|
+
|
|
87
|
+
- **Which card a placement gets is decided by the SIGNED binding, per placement.** The Builder Delivery response may
|
|
88
|
+
carry the card document as an unsigned top-level sibling, `card`, beside `payload`, `signature`, `renderDef` and
|
|
89
|
+
`face`. The signed `payload.content.faceDigest` binds it with two keys 1.3.x ignores, `contract: 1` and `doc` (the
|
|
90
|
+
lowercase hex sha256 of the document's RFC 8785 bytes): `{ v: 1, defVersion, html, css, contract: 1, doc }`.
|
|
91
|
+
- **No `contract` key** (every brand until CertREV turns contract v1 on for it, from its side): the card paints
|
|
92
|
+
**exactly what 1.3.1 paints**, byte for byte (the delivered face, the reader face, the legacy face, the options
|
|
93
|
+
face, the records), whatever else the response carries.
|
|
94
|
+
- **`contract: 1`**: the block prints the document only when it is a well-formed contract-1 card document that hashes
|
|
95
|
+
(WebCrypto sha256 over cert-contract's `canonicalizeJson`) to the signed `doc` and names the signed design version,
|
|
96
|
+
the envelope's certificate and this Builder entry (`subject: { platform: 'builder', externalId }`). Otherwise it
|
|
97
|
+
prints **nothing** and reports why: `card_missing` (no card served), `card_verify_failed` (malformed, another
|
|
98
|
+
digest, design version, certificate or entry), `card_pin_mismatch` (CertREV's replica pinned another design
|
|
99
|
+
version). Never a broken card, and never a face of its own for such a placement.
|
|
100
|
+
- **Any other `contract` value, or a malformed contract-1 binding**: nothing, and `card_unsupported_contract`.
|
|
101
|
+
- **What a block prints:** the block's part hosts in page order and, in the block holding the document's first part
|
|
102
|
+
(`top`, else `rail`, else `pill`), the document's head fragment (`<style data-certrev-card-head>`: the pill's fixed box
|
|
103
|
+
and the fonts) before them, CertREV's two dialog hosts (the profile and the certificate: CertREV-styled, identical on
|
|
104
|
+
every brand and CMS, blind to the brand's design) and, once CertREV serves it, the hosted runtime's `<script src
|
|
105
|
+
integrity crossorigin="anonymous" defer>` after them, inside the card's wrapper
|
|
106
|
+
(`data-certrev-face="card"`, `data-certrev-contract="1"`, `data-certrev-def-version`, the seed). Every byte inside the
|
|
107
|
+
wrapper but the runtime tag is the document's own. Each part is a `<certrev-card>` host with a declarative shadow root:
|
|
108
|
+
the browser's parser attaches it, so the card is complete with JavaScript off and no theme rule reaches inside it. The
|
|
109
|
+
saved design decides the layout (the block's `mode` is not read): a banner block prints the card and its memo (`part:
|
|
110
|
+
'header'` the card only, `part: 'memo'` the memo only); a sidebar or floating block prints its rail or pill, and a
|
|
111
|
+
`memo` block outside the banner prints nothing (a page that kept split blocks after the brand chose a sidebar shows one
|
|
112
|
+
card). A page prints the head, the dialogs and the runtime once (ADR 0001's head once per page, as the lead ruled): a
|
|
113
|
+
split banner's header block carries them, its memo block prints the memo only, and the runtime opens a memo control's
|
|
114
|
+
dialog from the header block (it finds a dialog anywhere on the page by the placement). No block coordinates with
|
|
115
|
+
another, so this holds with JavaScript off.
|
|
116
|
+
- **In the browser.** Hydration keeps the server's card as it stands (same node, no read). The seed (the 1.3.1 seed with
|
|
117
|
+
the card appended last) lets React's own re-renders (its recovery from a hydration error elsewhere on the page, a
|
|
118
|
+
record streamed in after the card rendered, a later mount of the same placement) print the card again with no read,
|
|
119
|
+
after the browser has re-checked the seed's card against the seed payload's binding; a seed card that fails is
|
|
120
|
+
never printed and is reported. A card React inserts (an in-app navigation, Builder's editor) gets its roots attached by
|
|
121
|
+
the card: `setHTMLUnsafe`, else `attachShadow` from each template with its mode and options. The runtime, when the
|
|
122
|
+
document names one, is injected once per page into `document.head` by the block that prints its tag (React does not
|
|
123
|
+
run a script it inserts), unless it already runs (`Symbol.for('certrev.card-runtime')` on `document`).
|
|
124
|
+
- **Builder's wrappers and the sidebar card.** Builder renders each block in a flex item of a flex column, which contains
|
|
125
|
+
the rail's float, so no copy could sit beside the sidebar card. `BUILDER_CARD_FLOW_CSS` (printed after the wrapper,
|
|
126
|
+
only with a rail, only above the card's 820px breakpoint, only with `:has()`) lets the float out of Builder's
|
|
127
|
+
wrappers, as 1.2.0-canary.6 did for the reader face.
|
|
128
|
+
- **`clientRead` now defaults to `true` (behaviour change, no new prop).** A card the client mounts with no record on the
|
|
129
|
+
page reads the Delivery API once from the browser: it is the only way the server's card exists after an in-app
|
|
130
|
+
navigation, and it is the read the CSP's `connect-src https://portal.certrev.com` allows. A read the policy blocks did
|
|
131
|
+
not reach CertREV, so the block paints its options face exactly as 1.3.1 painted it (and reports
|
|
132
|
+
`delivery_unreachable`). `clientRead={false}` restores 1.2.0 to 1.3.1's behaviour (no browser read). A card the client
|
|
133
|
+
renders while the document is still loading first waits for a record the server may still stream (1.2.1), with or
|
|
134
|
+
without `clientRead`, and reads only if the page loads without one; through 1.3.1 an explicit `clientRead` read at
|
|
135
|
+
once and could miss that record. Every read sends no cookie (`credentials: 'omit'`).
|
|
136
|
+
- **The trust root is a key SET, so a key rotation needs no reinstall.** A kid this package bakes (`cert-issuer-1`)
|
|
137
|
+
resolves as before. Any other kid makes one read of the delivery origin's published key set,
|
|
138
|
+
`GET <origin>/api/cert/v1/keys` (`credentials: 'omit'`; a browser at most once per page and origin, a server once per
|
|
139
|
+
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
|
|
140
|
+
8785 bytes of `{ kind: 'certrev.keyset', v: 1, issuedAt, keys: [{ kid, crv: 'Ed25519', x }] }`), it is not older
|
|
141
|
+
than the set already accepted from that origin, and it still lists, as the same key, the kid that signed it. Anything
|
|
142
|
+
else leaves the kid unknown: `unknown_key`, nothing painted, as in 1.3.x. Today every envelope is signed by the baked
|
|
143
|
+
kid, so nothing is fetched.
|
|
144
|
+
- **No silent fallback.** Each fallback is one structured report: in the browser
|
|
145
|
+
`CustomEvent('certrev:card-fallback', { detail: { reason, platform: 'builder', externalId, certId? } })` on
|
|
146
|
+
`document`, once per placement and reason per page; on a server one `console.warn` line, `[certrev] card_fallback
|
|
147
|
+
{json}`; and a contract-1 `'none'` record names its `reason`, so a browser hydrating it reports it too. Reported as
|
|
148
|
+
well, with nothing painted differently, where the read that met them ran (the server's log for a server render, the
|
|
149
|
+
event for a browser read): CertREV not reached (`delivery_unreachable`, whether the block then paints its options face
|
|
150
|
+
or has none to paint) and an unknown kid (`unknown_key`). The card sends nothing anywhere: no beacon, no storage, no
|
|
151
|
+
cookie.
|
|
152
|
+
- **React 19.** The card suspends a server render once, on its read: the read itself computes the card's digest (and,
|
|
153
|
+
for 1.3.0's delivered face, its hashes) before it is cached. Under React 19, `use` matches promises by their order in a
|
|
154
|
+
render, so a second suspension (on the digest, after the read) was handed the read's result: the card printed nothing
|
|
155
|
+
(`card_verify_failed`), and through 1.3.1 a delivered face silently fell back to the card's own face. The replica runs
|
|
156
|
+
on React 18 and on React 19 (`CERT_BLOCK_E2E_REACT`). React 19 also streams any Suspense boundary larger than its
|
|
157
|
+
`progressiveChunkSize` (12,800 bytes by default) after the shell; a host that wants the card complete for readers with
|
|
158
|
+
JavaScript off raises it (README, "React 19 hosts").
|
|
159
|
+
- **Found and fixed before release (a break-it pass).** A page with more cards than the digest memo held (32 documents;
|
|
160
|
+
64 delivered faces in 1.3.0's memo, which had the same flaw) never finished rendering, because a retry found its
|
|
161
|
+
pending digest evicted: both memos now drop only settled entries. A split header and memo pair whose memo block had
|
|
162
|
+
nothing to print wrote a record that took the header block's card down on hydration: such a block now prints nothing
|
|
163
|
+
and writes no record. `Object.hasOwn` was replaced (Safari before 15.4). A runtime `src` must be on `certrev.com`.
|
|
164
|
+
- **New API, additive.** `./builder`: `acceptedCardBinding`, `parseCardDocument`, `cardDocumentDigest`, `cardPartsFor`,
|
|
165
|
+
`CardDocumentV1`, `CardBinding`, `CardPartName`, `CARD_FALLBACK_EVENT`, `CardFallbackDetail`, `CardFallbackReason`,
|
|
166
|
+
`BUILDER_CARD_FLOW_CSS`. Root: `keySetResolver`, `verifyKeySet`, `KEY_SET_PATH` and their types. `VerifiedDelivery` and
|
|
167
|
+
`CachedDelivery` gain an optional `card` (the sibling exactly as served, UNVERIFIED: check it as the card does).
|
|
168
|
+
- **Unchanged:** the registration, its inputs and the props (a test compares them with 1.3.1's own build, and the
|
|
169
|
+
compiler checks the props), every 1.3.1 face and record for a placement without a contract binding (every 1.3.1
|
|
170
|
+
golden and byte test passes unmodified; the six tests of the no-browser-read default now pass `clientRead={false}`),
|
|
171
|
+
the design pin's own rules on that path, the web component, the storefront modal bundle, and
|
|
172
|
+
`@certrev/cert-contract` `^0.6.0` (the binding is two keys in the already-signed `faceDigest`).
|
|
173
|
+
- **Tests.** `builder/__tests__/card-contract.test.ts` (the binding, the shape check, the digest held equal to the
|
|
174
|
+
portal's own on its five real documents, the parts, the printed bytes), `contract-card.test.tsx` (server: every
|
|
175
|
+
branch, the printed bytes, the reports), `contract-card-client.test.tsx` (jsdom: hydration, client mount, the seed's
|
|
176
|
+
re-check, root attachment, the runtime), `registration-1.x.test.ts` + `registration-props.test-d.ts` (the 1.x
|
|
177
|
+
registration and props), `verify/__tests__/key-set.test.ts` (every refusal, one read per origin, a rotation that
|
|
178
|
+
renders). `e2e/` (`pnpm test:replica`, Playwright Chromium): a Hydrogen-shaped Builder replica at 1280 and 393 px.
|
|
179
|
+
|
|
180
|
+
## 1.3.1 · 2026-10-21
|
|
181
|
+
|
|
182
|
+
Patch: **a Builder card on the reader face now paints the same card as Shopify and WordPress.** The portal's reader
|
|
183
|
+
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
|
|
184
|
+
state, so the card's design and behaviour are uniform across CMSs. The sentence at the top of this file says patches
|
|
185
|
+
never change the rendered cert output: **this one does, on purpose, for the reader face only** (a render def that
|
|
186
|
+
says `face: 'reader'`), by bringing it to the bytes the portal's Shopify snippets paint today. **No integration
|
|
187
|
+
change is needed** (same exports, types, registration, inputs and seed), and **the delivered-face path of 1.3.0 is
|
|
188
|
+
unchanged**: its gate, hashes, seed, wrapper and the 1.2.1 fallbacks work as before. The legacy face
|
|
189
|
+
(`renderCertBlock`), the options-only block, the web component and the storefront modal bundle are untouched; a
|
|
190
|
+
pinned card keeps its pin rules and paints the caught-up face for its pinned reader def. A card that paints a
|
|
191
|
+
delivered face (1.3.0) paints the server's markup and is not affected at all.
|
|
192
|
+
|
|
193
|
+
**This is the fallback face**, for sites that still read the render def themselves and paint the reader face from it
|
|
194
|
+
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
|
|
195
|
+
Profile" and "View Certificate" controls are the same links carrying `data-certrev-modal-open` as before, opened by the
|
|
196
|
+
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
|
|
197
|
+
signed card document instead of painting a face of its own.
|
|
198
|
+
|
|
199
|
+
What a reader sees now, on a reader-face card:
|
|
200
|
+
|
|
201
|
+
- **The sidebar card folds on a phone.** At 640px and under, its bio and memo are hidden and shown again inside a
|
|
202
|
+
closed `<details class="certrev-r-more">` behind "Read the expert memo" ("About the expert" when there is a bio and
|
|
203
|
+
no memo; no fold with neither), exactly as the Shopify extension and the WordPress plugin (1.8.3) paint it; wider
|
|
204
|
+
than 640px the card is as before. The scope line, the compensation cue and both links stay outside the fold. A
|
|
205
|
+
469-character memo no longer shows in full on a 375px phone. (`sidebar()` in `reader/render.ts`.)
|
|
206
|
+
- **The rung is honoured** (banner, memo card, sidebar): `credentialed` prints no memo, bio or photo (so no memo card;
|
|
207
|
+
the banner is the memo-less card with the scope line and the profile and certificate row), `named` also drops the
|
|
208
|
+
credential, `minimal` is the accent band and the links only, and any other value is the full card (the memo
|
|
209
|
+
rung), as before. The floating pill is the same on every rung. (`readFacts`, `bannerCard`, `bannerMemo`,
|
|
210
|
+
`sidebar`.)
|
|
211
|
+
- **A toned face.** A def with an ink (`tokens.inkColor`) or a surface that is not white (`tokens.surface`) writes
|
|
212
|
+
`--cr-sf` and `--cr-ink` on the card and adds the class `certrev-r-toned` to every section root; the stylesheet
|
|
213
|
+
paints the surface and ink from them. (`readerTokens`, `isTonedStyle` in `reader/tokens.ts`; `sectionOpen`.)
|
|
214
|
+
- **A def with no design paints the memo-rung defaults**: accent `#0a1b3f` and corner `12px` (it was `#0A1B3F` and
|
|
215
|
+
`14px`). A def that sets its own accent and corner is unchanged. (`reader/tokens.ts`.)
|
|
216
|
+
- **The banner's writer takes the same 42px face as the reviewer**, photo or initials, at every width (it was the
|
|
217
|
+
36px `sm` face). A photo the browser cannot load is still repainted as its person's initials: the person is now
|
|
218
|
+
read from the byline ("Written by" or "Reviewed by"), not from the face's size class. (`bannerCard`;
|
|
219
|
+
`photoPerson` in `builder/cert-review-card.tsx`.)
|
|
220
|
+
- **The banner's credential date never breaks**: it sits in `<span class="certrev-r-nw">`, and the credential line
|
|
221
|
+
breaks only between its parts. (`verifiedLine` in `reader/render.ts`.)
|
|
222
|
+
- **The stylesheet is the portal's current one** (`reader/reader.css` and `READER_CSS`, 42.9 KB, from portal
|
|
223
|
+
`d62ae100b3`; was 31.6 KB): the banner card splits evenly between the writer and the reviewer on a shared
|
|
224
|
+
baseline (821px and up), the certified date and the cue sit on one full-width row, left-justified, under a hairline a
|
|
225
|
+
theme cannot hide, and the footer stacks at 560px and under; the expert memo card on a phone puts the avatar and
|
|
226
|
+
name on one row with a full-width quote and one links row, and a theme's `blockquote` rules cannot bleed into
|
|
227
|
+
it; the sidebar fold and its 44px toggle survive a theme's `details` rules; a toned face's border resets hold.
|
|
228
|
+
The sheet the card inlines (without the 40 remote font faces) grows from 14.8 KB to 26.2 KB before compression.
|
|
229
|
+
- **Unchanged:** the profile and certificate links (`rel` stays `CERTREV_LINK_REL`, the one byte the face does not
|
|
230
|
+
share with the Liquid), the floating pill's markup, the compensation cue and scope line wording, the avatars' 46px
|
|
231
|
+
reviewer face on the memo card and the sidebar, the seed and the wrapper, and every non-reader face.
|
|
232
|
+
- **Tests.** `scripts/refresh-reader.mjs --portal <portal> --oracle` re-copied the stylesheet, the 11 golden pages and
|
|
233
|
+
the Liquid harness's output; the golden, differential and link-rel tests pass byte for byte on all of it. The
|
|
234
|
+
harness now also paints 155 more edge cells (every rung, the sidebar fold's variants, toned faces, a face with no
|
|
235
|
+
design: 399 cells in all, was 244), and `reader/__tests__/reader-one-face.test.ts` spells those rules out.
|
|
236
|
+
Hand-written expectations that pinned the 2026-10-06 face were brought to the portal's current Liquid (the
|
|
237
|
+
writer's `sm` avatar, the credential date's span, the default accent and corner, the stylesheet's hash). The
|
|
238
|
+
reader entries of `__golden__/delivered-face-1.2.1.json` and `delivered-face-client-1.2.1.json` were re-recorded
|
|
239
|
+
from this version; everything in them outside the stylesheet and the reader sections (the wrapper and the seed) is
|
|
240
|
+
still 1.2.1's bytes, and the legacy entries are untouched. New: a failed writer photo repaints the writer, not the
|
|
241
|
+
reviewer (`delivery-card-hydrate.test.tsx`).
|
|
242
|
+
|
|
8
243
|
## 1.3.0 · 2026-10-18
|
|
9
244
|
|
|
10
245
|
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,122 @@ 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.1` (2.0.1 is a drop-in on 2.0.0: see "2.0.1" below). Keep the registration line as it
|
|
300
|
+
is: same import, same `certRevCertComponent`, 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
|
+
2.0.1: the card's beacon)
|
|
307
|
+
font-src https://assets.certrev.com (the card's fonts)
|
|
308
|
+
img-src https://assets.certrev.com (reviewer photos)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The card also uses inline `<style>` elements and `style` attributes, as every cert-block release has, so the policy's
|
|
312
|
+
`style-src` must keep allowing inline styles.
|
|
313
|
+
3. On a React 19 storefront only: pass `progressiveChunkSize: 1_000_000` to `renderToReadableStream` in the server entry
|
|
314
|
+
(Hydrogen: `app/entry.server.tsx`), once, so a reader with JavaScript off gets the card in the page (below).
|
|
315
|
+
|
|
316
|
+
**Turned on from CertREV's side, per brand.** Nothing changes on the page until CertREV signs a placement's envelope for
|
|
317
|
+
contract v1: until then the block paints exactly what 1.3.1 paints, byte for byte. The switch is the signed
|
|
318
|
+
`payload.content.faceDigest`, which then carries `contract: 1` and `doc`, the sha256 of the card document's RFC 8785
|
|
319
|
+
bytes; the Delivery response carries the document as its unsigned `card` sibling.
|
|
320
|
+
|
|
321
|
+
| The signed binding | What the block prints |
|
|
322
|
+
|---|---|
|
|
323
|
+
| no `contract` key | exactly what 1.3.1 prints for the same response |
|
|
324
|
+
| `contract: 1`, and the served `card` hashes to `doc` and names the signed design version, the certificate and this Builder entry | the card, verbatim |
|
|
325
|
+
| `contract: 1`, no card served | nothing; reported `card_missing` |
|
|
326
|
+
| `contract: 1`, any mismatch or a malformed document | nothing; reported `card_verify_failed` |
|
|
327
|
+
| any other `contract`, or a malformed binding | nothing; reported `card_unsupported_contract` |
|
|
328
|
+
| CertREV's replica pins another design version | nothing; reported `card_pin_mismatch` |
|
|
329
|
+
|
|
330
|
+
**What a block prints.** Inside the card's wrapper (`data-certrev-face="card"`): the block's part hosts in page order
|
|
331
|
+
and, in the block that holds the document's first part (the card on a banner, else the rail, else the pill), the head
|
|
332
|
+
fragment (`<style data-certrev-card-head>`) before them, CertREV's profile and certificate dialog hosts and, once CertREV
|
|
333
|
+
serves it, the runtime's `<script src integrity crossorigin="anonymous" defer>` after them. So a page prints each of
|
|
334
|
+
those once: a split banner's header block carries them and its memo block prints the memo only, whose controls open the
|
|
335
|
+
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
|
|
336
|
+
`<certrev-card>` with a declarative shadow root, attached by the HTML parser: the card is complete with JavaScript off
|
|
337
|
+
and no theme rule reaches inside it. The saved design decides the layout, not the block's `mode`; `part` splits a banner
|
|
338
|
+
only (`'header'` the card, `'memo'` the memo), and a `'memo'` block on a sidebar or floating design prints nothing, so a
|
|
339
|
+
page keeps one card when a brand changes its design. A sidebar card's float is let out of Builder's block wrappers
|
|
340
|
+
(`BUILDER_CARD_FLOW_CSS`) so the article's copy flows beside it above 820px, as on every other CMS.
|
|
341
|
+
|
|
342
|
+
**In the browser.** Hydration keeps the server's card as it stands, with no read. React's own later renders of the
|
|
343
|
+
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
|
|
344
|
+
navigation, Builder's editor) reads the Delivery API once (`clientRead`, on by default from 2.0.0; `clientRead={false}`
|
|
345
|
+
restores 1.3.1's no-read behaviour), prints the card and attaches its roots (`setHTMLUnsafe`, else from each template),
|
|
346
|
+
and loads the runtime once. A read your policy blocks prints nothing and reports `delivery_unreachable` (2.0.1; 2.0.0
|
|
347
|
+
painted the block's options face, as 1.3.1 did).
|
|
348
|
+
|
|
349
|
+
**React 19 hosts and readers with JavaScript off.** React 19 streams a Suspense boundary larger than its
|
|
350
|
+
`progressiveChunkSize` (12,800 bytes by default) after the page's shell, even when it is ready at once, and only a script
|
|
351
|
+
moves it into place; the card (its parts, dialogs and seed) is larger than that, so a reader with JavaScript off on a
|
|
352
|
+
React 19 page sees no card unless the host passes a larger `progressiveChunkSize` (for example `1_000_000`) to
|
|
353
|
+
`renderToReadableStream`. React 18 inlines it. Readers with JavaScript and crawlers that run it see the card either way.
|
|
354
|
+
|
|
355
|
+
**Every fallback is reported**, never silent: in the browser,
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { CARD_FALLBACK_EVENT, type CardFallbackDetail } from '@certrev/cert-block/builder'
|
|
359
|
+
|
|
360
|
+
document.addEventListener(CARD_FALLBACK_EVENT, (e) => {
|
|
361
|
+
const { reason, externalId, certId } = (e as CustomEvent<CardFallbackDetail>).detail
|
|
362
|
+
// reason: card_missing | card_verify_failed | card_unsupported_contract | card_pin_mismatch | unknown_key
|
|
363
|
+
// | delivery_unreachable | no_marker | marker_invalid | client_read_off (the last three: 2.0.1)
|
|
364
|
+
})
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
once per placement and reason per page; on a server, one `console.warn` line `[certrev] card_fallback {json}`. 2.0.1:
|
|
368
|
+
each report is also a beacon to CertREV (below); the card stores nothing and sends no cookie (`credentials: 'omit'`).
|
|
369
|
+
|
|
370
|
+
**Key rotation needs no reinstall.** The baked kid (`cert-issuer-1`) verifies as before. An envelope signed by any
|
|
371
|
+
other kid makes one read of `https://portal.certrev.com/api/cert/v1/keys`, a key set the card accepts only when the baked
|
|
372
|
+
kid signs it (and it is not older than one already accepted, and still lists its signer). Otherwise the kid is unknown
|
|
373
|
+
and the card renders nothing, as in 1.3.x.
|
|
374
|
+
|
|
375
|
+
### 2.0.1: e.l.f.'s one install
|
|
376
|
+
|
|
377
|
+
A drop-in on 2.0.0 (same package, export, registration, inputs and CSP). What changes on a page:
|
|
378
|
+
|
|
379
|
+
- **Nothing, never unsigned facts.** A block with no delivery marker (`no_marker`), a marker this client does not
|
|
380
|
+
accept (`marker_invalid`), CertREV not reached with no verified answer inside the stale window
|
|
381
|
+
(`delivery_unreachable`), and a client mount with `clientRead={false}` (`client_read_off`) print **nothing** and
|
|
382
|
+
report it. Through 2.0.0 those painted the block's export-time options (a reviewer name, a credential, a memo, a
|
|
383
|
+
verify link that nobody signed); that face is gone. Every section below that still describes "the options face"
|
|
384
|
+
describes 1.x to 2.0.0; 2.0.1 prints nothing there. The signed paths are unchanged.
|
|
385
|
+
- **Stale window: 30 s fresh, 30 s stale** (2.0.0: 60 s and 300 s): with the Delivery API's own cache a revoked card is
|
|
386
|
+
gone within five to six minutes (ADR 0001 §7); past the window a read that cannot reach CertREV prints nothing.
|
|
387
|
+
- **A beacon to CertREV**: each fallback, and one health event on one page view in a hundred, as a `POST <delivery
|
|
388
|
+
origin>/api/cert/v1/beacon` `text/plain` JSON body `{v:1, client:'cert-block', clientVersion, contract,
|
|
389
|
+
platform:'builder', externalId, family, events:[{kind, reason, outcome}]}`. No cookies, no page address or text, no
|
|
390
|
+
user agent beyond a family. At most three per page; on a server, each placement and reason once an hour per process.
|
|
391
|
+
It goes to the delivery origin, already in `connect-src`: no new CSP source.
|
|
392
|
+
- **Forward compatible**: unknown parts, unknown fields and new keys under contract 1 are ignored; a newer major prints
|
|
393
|
+
nothing (`card_unsupported_contract`) and the page renders around it.
|
|
394
|
+
- **Placement hints**: once the page has loaded, each banner part moves to the place the document's `placement` names
|
|
395
|
+
among the Builder entry's top-level blocks: `content-start` (after a leading image with `after: 'leading-media'`),
|
|
396
|
+
`after-byline`, `before-references` (else `content-end`), `content-end`. Mark your byline with
|
|
397
|
+
`data-certrev-anchor="byline"` to make `after-byline` exact; otherwise the first `[rel~="author"]`,
|
|
398
|
+
`[itemprop="author"]`, `.byline`, `[class*="byline"]` or `address` in the entry is taken. An anchor the page lacks,
|
|
399
|
+
or one this client does not know, leaves the part where the editor put it, as JavaScript off and Builder's visual
|
|
400
|
+
editor and preview do. Rails and pills never move.
|
|
401
|
+
- **Nothing in the console**: no hydration mismatch in React's development build, and no "declarative Shadow DOM is
|
|
402
|
+
not being parsed" warning when React writes the card itself (an in-app navigation). The server's bytes are
|
|
403
|
+
unchanged.
|
|
404
|
+
- **The page's CertREV graph**: when the document carries `jsonld.graph` (resolved by CertREV's server for the
|
|
405
|
+
placement), the block that carries the page's assets prints it once as `<script type="application/ld+json"
|
|
406
|
+
data-certrev-graph>`; without it, no graph. The block never builds the graph (the "card emits no structured data" rule
|
|
407
|
+
below describes 1.x's own face).
|
|
408
|
+
|
|
293
409
|
### The reader face (1.2.0, opt-in)
|
|
294
410
|
|
|
295
411
|
certrev.com's "reader" design (a banner card plus the expert memo, a sidebar card, a floating pill) is a
|
|
@@ -303,7 +419,13 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
|
|
|
303
419
|
|
|
304
420
|
- **Same bytes as the Liquid.** The markup is the Shopify `certrev-reader-*` snippets', and the portal's
|
|
305
421
|
golden pages are copied into `src/reader/__tests__/fixtures/golden/` with their sha256, so the two
|
|
306
|
-
cannot drift without a test saying so.
|
|
422
|
+
cannot drift without a test saying so. **From 1.3.1 it is the portal's one face**, the card the Shopify
|
|
423
|
+
extension and the WordPress plugin (1.8.3 and later) paint, so a Builder page, a Shopify page and a WordPress
|
|
424
|
+
page show the same card (`src/reader/__tests__/fixtures/SOURCES.json` names the portal commit the copies came
|
|
425
|
+
from; `pnpm refresh:reader` re-copies them and the golden and differential tests then say what the renderer must
|
|
426
|
+
follow). 1.2.0 to 1.3.0 painted the portal's face of 2026-10-06; the differences are listed in the CHANGELOG.
|
|
427
|
+
It is the **fallback face** for sites that read the def themselves: 1.3.1 only brings it level with the portal,
|
|
428
|
+
and adds no feature of its own.
|
|
307
429
|
- **The stylesheet** is the generated reader section of `certrev-cert.css`
|
|
308
430
|
(`/* CERTREV-READER-GEN:BEGIN */ ... END`), copied byte for byte as `src/reader/reader.css` (`./reader.css`)
|
|
309
431
|
and as `READER_CSS`. The card inlines it once, in a `<style data-certrev-reader-css>`, inside the face's
|
|
@@ -349,12 +471,23 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
|
|
|
349
471
|
- **Brand tokens** are an allow-list, as the Shopify and WordPress twins' are. The accent and bar ink must
|
|
350
472
|
be a `#RGB` or `#RRGGBB` string; the corner radius a bare `0` or one non-negative `px`, `rem`, `em` or `%`
|
|
351
473
|
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
|
|
474
|
+
comment opener or a bracket) paints the default, navy `#0a1b3f` and `12px` (the design guide's memo-rung defaults,
|
|
475
|
+
also what a face with no def paints; 1.3.0 painted `#0A1B3F` and `14px`), and never reaches the `style`
|
|
476
|
+
attribute. The font is one of five stacks (`sans`, `serif`, `grotesk`, `mono`, `system`); an unknown slot paints
|
|
477
|
+
sans. A def with an ink (`inkColor`), or a surface (`surface`) that is not white, is a **toned** face: each
|
|
478
|
+
valid colour is written as `--cr-sf` / `--cr-ink` on the root, which gains the class `certrev-r-toned`.
|
|
479
|
+
- **The rung.** The def's `rung` decides how much the card prints: `memo` (and any value the face does not know,
|
|
480
|
+
or none) is the whole card; `credentialed` prints no memo, bio or photo (so no memo card, and the banner is the
|
|
481
|
+
memo-less card with the scope line and the profile and certificate row); `named` also drops the credential;
|
|
482
|
+
`minimal` is the accent band and the links only. The floating pill is the same on every rung.
|
|
483
|
+
- **The sidebar folds on a phone.** The card's bio and memo are printed twice: as the card's own pair, and again
|
|
484
|
+
inside a closed `<details class="certrev-r-more">` whose toggle reads "Read the expert memo" ("About the
|
|
485
|
+
expert" when there is a bio and no memo; no fold with neither). At 640px and under the stylesheet hides the
|
|
486
|
+
pair and shows the fold; wider, the fold is hidden. The scope line, the compensation cue and both links stay
|
|
487
|
+
outside the fold. It is the same card as Shopify's and WordPress's (owner decision 3d0838a4).
|
|
355
488
|
- **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
|
-
|
|
489
|
+
initials, else the silhouette (a name with no initials). It is 42 px for the writer and the banner reviewer
|
|
490
|
+
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
491
|
`<img>`, so a page with no photos holds no avatar image. The initials are the same bytes the portal's Liquid
|
|
359
492
|
snippets and WordPress renderer print: the name trimmed and split on single spaces (empty parts skipped, so
|
|
360
493
|
"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 +499,8 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
|
|
|
366
499
|
setting.
|
|
367
500
|
- **Credential line.** The banner prints the reviewer's credential, a middle dot and the attribution as
|
|
368
501
|
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
|
|
502
|
+
with no displayed credential makes no verification claim. The date, when there is one, is wrapped in
|
|
503
|
+
`<span class="certrev-r-nw">` so it never breaks across lines. The writer's `<em>` under the name is the
|
|
370
504
|
envelope's `author.title`, printed only when it is non-empty.
|
|
371
505
|
|
|
372
506
|
### The delivery card in the browser (1.2.0)
|
|
@@ -427,7 +561,8 @@ browser hydrates from it:
|
|
|
427
561
|
`part: 'header'` or `'memo'`), when the def served is that version and both hashes, computed with WebCrypto,
|
|
428
562
|
equal the signed values. The seed then ends with the same `face`, and the browser hashes it again against the
|
|
429
563
|
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
|
|
564
|
+
hash, the card paints what 1.2.1 paints. Every other delivery paints exactly what 1.2.1 paints, seed included,
|
|
565
|
+
except that a reader-face def paints the reader face of 1.3.1 (below), not 1.2.1's.
|
|
431
566
|
The face is on the page twice (painted, and in the seed).
|
|
432
567
|
- **The server's read** waits up to 8 s (the verifier's own default stays 3 s). Its cache keeps a certificate for
|
|
433
568
|
60 s, then serves it for up to 5 minutes more while one background refresh runs or while CertREV cannot be
|
|
@@ -862,6 +997,24 @@ alongside each modal module. The load-bearing ones:
|
|
|
862
997
|
- **`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
998
|
- **`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
999
|
- **`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.
|
|
1000
|
+
- **`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).
|
|
1001
|
+
- **`builder/__tests__/card-contract.test.ts`**, **`contract-card.test.tsx`**, **`contract-card-client.test.tsx`** (2.0.0):
|
|
1002
|
+
the contract-1 card on the portal's own card documents (`__fixtures__/contract-v1/`, painted by its
|
|
1003
|
+
`buildCardDocument`; `scripts/card-fixtures.portal.ts` regenerates them): the binding, the shape check, the digest held
|
|
1004
|
+
equal to the portal's own, the printed bytes (the document's own, verbatim), every fallback and its report, hydration,
|
|
1005
|
+
a client mount's read, the seed's re-check, root attachment and the runtime; and every 1.3.1 path unchanged.
|
|
1006
|
+
**`registration-1.x.test.ts`** + **`registration-props.test-d.ts`** hold the registration to 1.3.1's own build and the
|
|
1007
|
+
props to 1.3.1's (compiler-checked). **`verify/__tests__/key-set.test.ts`**: the key set's refusal rules and reads.
|
|
1008
|
+
**`preview-parity.test.tsx`**: against the portal's own Appearance preview page for a contract-1 brand, the block
|
|
1009
|
+
prints the preview's head byte for byte and each host's shadow tree byte for byte but the certificate id.
|
|
1010
|
+
**`contract-wire.test.tsx`**: a Builder Delivery 200 exactly as the portal's route answered it.
|
|
1011
|
+
**`break-it-regressions*.test.tsx`**: the pre-release break-it findings, each failing on the code before its fix.
|
|
1012
|
+
- **`e2e/`** (`pnpm test:replica`, Playwright 1.59.1 Chromium, not part of `pnpm test`): a Hydrogen-shaped Builder replica
|
|
1013
|
+
(streamed SSR under a strict CSP with exactly the four sources above, document hydration, an in-app navigation, and
|
|
1014
|
+
CertREV's hosted runtime as built, `e2e/replica/card-runtime.*.js.txt`) at 1280 and 393 px: every part's shadow tree
|
|
1015
|
+
and pixels equal the same part printed alone and the Appearance preview's own part, the dialogs (focus trapped, Escape,
|
|
1016
|
+
focus returned), the sidebar's memo fold, a revoked placement removed and kept removed, JS off, and a CSP positive
|
|
1017
|
+
control for each source. `CERT_BLOCK_E2E_REACT=<a node_modules with react and react-dom 19>` runs it on React 19.
|
|
865
1018
|
- **`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
1019
|
- **`__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
1020
|
|
|
@@ -870,7 +1023,7 @@ published at `@certrev/cert-block/fixtures`).
|
|
|
870
1023
|
|
|
871
1024
|
## Version
|
|
872
1025
|
|
|
873
|
-
`
|
|
1026
|
+
`2.0.1`: see [CHANGELOG.md](./CHANGELOG.md) for the release history and the semver contract
|
|
874
1027
|
(patch/minor never change the rendered cert output or break a compiling integration). Publishes
|
|
875
1028
|
**publicly** to npm as `@certrev/cert-block` (`publishConfig.access: public`) via GitHub Actions
|
|
876
1029
|
trusted publishing (OIDC); the internal `@certrev` GitHub-Packages channel mirrors it.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Builder card's beacon (2.0.1): what the card tells CertREV about itself, so a card that does not print on a brand's
|
|
3
|
+
* page is seen by CertREV and not only by the brand's console.
|
|
4
|
+
*
|
|
5
|
+
* One `POST <delivery origin>/api/cert/v1/beacon` per event, a `text/plain` JSON body (a CORS simple request, so no
|
|
6
|
+
* preflight; the origin is the delivery origin the page's `connect-src` already allows, so no new CSP source):
|
|
7
|
+
*
|
|
8
|
+
* { v: 1, client: 'cert-block', clientVersion, contract, platform: 'builder', externalId, family,
|
|
9
|
+
* events: [{ kind: 'fallback' | 'health', reason, outcome, status? }] }
|
|
10
|
+
*
|
|
11
|
+
* What it never carries: cookies (`credentials: 'omit'`), the page's address (`referrerPolicy: 'no-referrer'`) or text, the reader's user agent (only a
|
|
12
|
+
* coarse family: chromium, webkit, gecko, other, or server), or anything the reader typed. `externalId` is the Builder
|
|
13
|
+
* entry the block renders in, which the brand's page already sends to CertREV when it reads the card.
|
|
14
|
+
*
|
|
15
|
+
* How much: in a browser, every fallback (each placement and reason once per page, `card-report.ts`) and one health
|
|
16
|
+
* event per page in a hundred, at most three beacons per page (a page is a document address: an in-app navigation to
|
|
17
|
+
* another article starts a new budget); on a server, each placement, reason and origin at most
|
|
18
|
+
* once an hour per process, with a short deadline, never awaited. A beacon that fails is dropped: it never retries,
|
|
19
|
+
* never stores anything and never changes what the card prints.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BEACON_PATH = "/api/cert/v1/beacon";
|
|
22
|
+
/** Where a block with no accepted delivery marker reports: CertREV's production delivery origin. */
|
|
23
|
+
export declare const DEFAULT_BEACON_ORIGIN = "https://portal.certrev.com";
|
|
24
|
+
/** The most beacons one page sends. */
|
|
25
|
+
export declare const BEACON_MAX_PER_PAGE = 3;
|
|
26
|
+
/** The share of page views that send one health event. */
|
|
27
|
+
export declare const BEACON_HEALTH_SAMPLE = 0.01;
|
|
28
|
+
/** A server sends one beacon per placement, reason and origin at most this often. */
|
|
29
|
+
export declare const BEACON_SERVER_INTERVAL_MS = 3600000;
|
|
30
|
+
export type BeaconOutcome = 'ok' | 'unreachable' | 'http_4xx' | 'http_5xx' | 'verify_failed' | 'unsupported' | 'other';
|
|
31
|
+
export type BeaconFamily = 'chromium' | 'webkit' | 'gecko' | 'server' | 'other';
|
|
32
|
+
export interface BeaconEvent {
|
|
33
|
+
readonly kind: 'fallback' | 'health';
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
readonly outcome: BeaconOutcome;
|
|
36
|
+
/** The HTTP status CertREV answered with, when a read failed on one (outcome `http_4xx` or `http_5xx`). */
|
|
37
|
+
readonly status?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface CardBeacon {
|
|
40
|
+
readonly v: 1;
|
|
41
|
+
readonly client: 'cert-block';
|
|
42
|
+
readonly clientVersion: string;
|
|
43
|
+
readonly contract: number;
|
|
44
|
+
readonly platform: 'builder';
|
|
45
|
+
readonly externalId?: string;
|
|
46
|
+
readonly family: BeaconFamily;
|
|
47
|
+
readonly events: readonly BeaconEvent[];
|
|
48
|
+
}
|
|
49
|
+
/** How a beacon leaves: `url`, the JSON body, and where it is sent from. */
|
|
50
|
+
export type BeaconTransport = (url: string, body: string, where: 'browser' | 'server') => void;
|
|
51
|
+
/** The transport the card ships with: `fetch`, never awaited, never throwing (exported for CertREV's replica harness). */
|
|
52
|
+
export declare const fetchBeaconTransport: BeaconTransport;
|
|
53
|
+
/** The coarse browser family, from the user agent; nothing else of it is kept. */
|
|
54
|
+
export declare function browserFamily(userAgent: string): BeaconFamily;
|
|
55
|
+
export declare function beaconBody(externalId: string, events: readonly BeaconEvent[], family: BeaconFamily): CardBeacon;
|
|
56
|
+
/** Send one event from the browser, within the page's budget. */
|
|
57
|
+
export declare function sendBrowserBeacon(baseUrl: string | undefined, externalId: string, event: BeaconEvent): void;
|
|
58
|
+
/** Send one health event from the browser on one page view in a hundred (drawn once per page). */
|
|
59
|
+
export declare function maybeSendBrowserHealth(baseUrl: string | undefined, externalId: string): void;
|
|
60
|
+
/** Send one event from a server: each placement, reason and origin at most once an hour per process. */
|
|
61
|
+
export declare function sendServerBeacon(baseUrl: string | undefined, externalId: string, event: BeaconEvent): void;
|
|
62
|
+
/**
|
|
63
|
+
* Test seam: replace the transport (and the sampling draw and clock) and forget the page's and the process's budget.
|
|
64
|
+
* Returns a restore.
|
|
65
|
+
*/
|
|
66
|
+
export declare function setCardBeaconForTests(overrides?: {
|
|
67
|
+
transport?: BeaconTransport;
|
|
68
|
+
random?: () => number;
|
|
69
|
+
now?: () => number;
|
|
70
|
+
}): () => void;
|
|
71
|
+
/** Test seam: a new page and a new process. */
|
|
72
|
+
export declare function forgetCardBeaconsForTests(): void;
|
|
73
|
+
//# sourceMappingURL=card-beacon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card-beacon.d.ts","sourceRoot":"","sources":["../../src/builder/card-beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,eAAO,MAAM,WAAW,wBAAwB,CAAA;AAChD,oGAAoG;AACpG,eAAO,MAAM,qBAAqB,+BAA+B,CAAA;AACjE,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,IAAI,CAAA;AACpC,0DAA0D;AAC1D,eAAO,MAAM,oBAAoB,OAAO,CAAA;AACxC,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,UAAY,CAAA;AAIlD,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,eAAe,GAAG,aAAa,GAAG,OAAO,CAAA;AACtH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE/E,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,2GAA2G;IAC3G,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAA;IACb,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;CACvC;AAED,4EAA4E;AAC5E,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,QAAQ,KAAK,IAAI,CAAA;AAE9F,0HAA0H;AAC1H,eAAO,MAAM,oBAAoB,EAAE,eAsBlC,CAAA;AAoBD,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,CAK7D;AAeD,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,WAAW,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,UAAU,CAW/G;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQ3G;AAED,kGAAkG;AAClG,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAO5F;AAED,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAa1G;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACpC,SAAS,GAAE;IAAE,SAAS,CAAC,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GACxF,MAAM,IAAI,CAWZ;AAED,+CAA+C;AAC/C,wBAAgB,yBAAyB,IAAI,IAAI,CAKhD"}
|