@certrev/cert-block 1.2.0-canary.6 → 1.2.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 +120 -35
- package/README.md +116 -21
- package/dist/builder/card-reader-css.d.ts +14 -0
- package/dist/builder/card-reader-css.d.ts.map +1 -0
- package/dist/builder/card-reader-css.js +17 -0
- package/dist/builder/card-reader-css.js.map +1 -0
- package/dist/builder/cert-review-card.d.ts +8 -0
- package/dist/builder/cert-review-card.d.ts.map +1 -1
- package/dist/builder/cert-review-card.js +234 -41
- package/dist/builder/cert-review-card.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 +31 -3
- package/dist/builder/delivery-runtime.js.map +1 -1
- package/dist/builder/ssr-seed.d.ts +155 -0
- package/dist/builder/ssr-seed.d.ts.map +1 -0
- package/dist/builder/ssr-seed.js +278 -0
- package/dist/builder/ssr-seed.js.map +1 -0
- package/dist/reader/face.d.ts +6 -2
- package/dist/reader/face.d.ts.map +1 -1
- package/dist/reader/face.js +53 -5
- package/dist/reader/face.js.map +1 -1
- package/dist/reader/render.d.ts.map +1 -1
- package/dist/reader/render.js +10 -10
- package/dist/reader/render.js.map +1 -1
- package/dist/verify/cache.d.ts +39 -2
- package/dist/verify/cache.d.ts.map +1 -1
- package/dist/verify/cache.js +61 -12
- package/dist/verify/cache.js.map +1 -1
- package/dist/verify/get-verified-delivery.d.ts +28 -3
- package/dist/verify/get-verified-delivery.d.ts.map +1 -1
- package/dist/verify/get-verified-delivery.js +46 -9
- package/dist/verify/get-verified-delivery.js.map +1 -1
- package/package.json +112 -112
- package/src/builder/card-reader-css.ts +19 -0
- package/src/builder/cert-review-card.tsx +308 -56
- package/src/builder/delivery-runtime.ts +33 -3
- package/src/builder/ssr-seed.ts +343 -0
- package/src/reader/face.ts +59 -6
- package/src/reader/render.ts +12 -12
- package/src/verify/cache.ts +87 -11
- package/src/verify/get-verified-delivery.ts +77 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,41 +5,126 @@ 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
|
-
## 1.2.0
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
8
|
+
## 1.2.0 · 2026-10-06
|
|
9
|
+
|
|
10
|
+
Minor, the first stable release with the `delivery` card's reader face (dist-tag `latest`). It follows
|
|
11
|
+
`1.2.0-canary.5` (the design pin, below, is part of it) and adds these changes: **the delivery card never reads
|
|
12
|
+
from the browser unless the brand opts in, so no storefront needs a Content-Security-Policy change**, **a card whose
|
|
13
|
+
server could not reach CertREV paints the block's own options face, never nothing**, **the server's read waits 8 s
|
|
14
|
+
and serves a certificate stale while it refreshes**, **a field the def hides is absent from the page**, **the reader
|
|
15
|
+
face's avatar shows the person's initials where it has no photo to paint, or a photo fails to load**, and **the
|
|
16
|
+
reader sidebar card's float leaves its Builder block** (canary.6). No integration needs a code change, and an
|
|
17
|
+
options-only block is untouched.
|
|
18
|
+
|
|
19
|
+
- **No browser read by default; the options face when CertREV is not reached (the 1.2.0 re-cut).** A proof run on a
|
|
20
|
+
cold staging read found the failure this release exists to remove: the server's read missed the verifier's 3 s
|
|
21
|
+
deadline, so the server painted no card, and the browser's own read was then blocked by the storefront's CSP. Now:
|
|
22
|
+
- The card **never fetches from the browser** unless the block opts in with the new `clientRead` prop (code only,
|
|
23
|
+
not a Builder input). A card the browser renders with no server render behind it paints the block's options face,
|
|
24
|
+
or nothing (a block with no options).
|
|
25
|
+
- Where the server has no envelope it writes a **fallback record** in the seed attribute,
|
|
26
|
+
`{ v: 1, baseUrl, externalId, design?, face }` with `face` `"options"` or `"none"` and nothing of a certificate.
|
|
27
|
+
CertREV not reached (a 5xx, a body that is not an envelope, a network error, the deadline) paints the block's
|
|
28
|
+
own **options face**, the 1.0.3 bytes, in the block's `mode`, else its def's `layout`, else the banner; a block
|
|
29
|
+
whose options cannot paint writes `"none"`. A pinned card (canary.5's design pin, CertREV's replica) paints the
|
|
30
|
+
options face too, with a record naming its pin, so the replica shows what a brand's page shows: of canary.5's
|
|
31
|
+
fail-closed list below, only the 503 changes. A definitive no (revoked, expired, unverifiable,
|
|
32
|
+
another entry, a 404 or 410) still paints nothing, with `"none"`, so the options face never paints over a
|
|
33
|
+
revocation. The browser keeps the recorded face through hydration and through React's client re-render after a
|
|
34
|
+
hydration error, with no read.
|
|
35
|
+
- The card runtime's server read waits **8 s** (`CARD_DELIVERY_TIMEOUT_MS`; `getVerifiedEnvelope` and
|
|
36
|
+
`getVerifiedDelivery` keep their 3 s default). Its cache gains `staleMs` (5 minutes, `CARD_DELIVERY_STALE_MS`):
|
|
37
|
+
past the 60 s TTL a certificate is served at once while ONE background refresh runs (stale-while-revalidate),
|
|
38
|
+
and kept while CertREV cannot be reached (stale-if-error); a definitive refusal replaces it as soon as its
|
|
39
|
+
refresh lands, and a negative entry is never served stale. `inflightMaxMs` (deadline + 2 s) lets the next render
|
|
40
|
+
start a new load instead of joining one the host runtime abandoned. `TtlCache` without the new options behaves
|
|
41
|
+
exactly as before.
|
|
42
|
+
- A reviewer photo that **fails to load** (a CSP `img-src` block, a 404) is repainted as the person's initials,
|
|
43
|
+
whether it failed before hydration or after; the record is unchanged and nothing is requested.
|
|
44
|
+
|
|
45
|
+
- **The browser hydrates from the server's read (no browser fetch).** Through canary.5 a `delivery`-marker
|
|
46
|
+
card read the Delivery API on the server and again in the browser on hydration; a storefront whose CSP
|
|
47
|
+
`connect-src` names no CertREV origin blocked the second read, the read settled as a suppress and the
|
|
48
|
+
server's card vanished. The face's wrapper now carries the envelope it was painted from,
|
|
49
|
+
`data-certrev-envelope` = `{ v: 1, baseUrl, externalId, design?, payload, signature?, renderDef }` (the payload
|
|
50
|
+
as served, less hidden fields, below; `src/builder/ssr-seed.ts`), and the browser hydrates from it with zero
|
|
51
|
+
requests. After hydration the card re-runs the lifecycle and subject policy at the browser's clock, so an
|
|
52
|
+
expiry that passes while the page is open still takes it down. A card the browser mounts later paints
|
|
53
|
+
from any seed the page carried for the same placement. A card with no server render behind it (a
|
|
54
|
+
client-only page, Builder's editor preview) makes no read unless the block opts in with `clientRead`
|
|
55
|
+
(above); where an opted-in read is blocked or fails it renders nothing and logs nothing, and it never
|
|
56
|
+
unmounts the host's tree. A pinned card
|
|
57
|
+
(canary.5's `certrevDesignPin`) takes only a seed carrying the same pin, and the seed records it.
|
|
58
|
+
- **Server render or browser hydration is decided from the page, with no switch.** The card used to ask the
|
|
59
|
+
delivery runtime's `isServer()`, which defaulted to `typeof document === 'undefined'`; a `react-dom/server` render
|
|
60
|
+
under jsdom (or any DOM shim) therefore took the browser path and painted `<!--$--><!--/$-->`. The seam is gone.
|
|
61
|
+
A card with no `document` is a server; a card past hydration or mounted on the client is a browser; a card that is
|
|
62
|
+
hydrating is a browser only when the live document already holds server-rendered HTML in front of it (this
|
|
63
|
+
placement's seed, or any React Suspense boundary marker, which the card's own `<Suspense>` always leaves) and is a
|
|
64
|
+
server render otherwise. One case is not seen: a server render run while the document still holds server-rendered
|
|
65
|
+
React HTML is taken for a hydration, so a harness that hydrates one page and renders another into the same document
|
|
66
|
+
clears it between them.
|
|
67
|
+
- **A hidden field is absent from the page.** The seed (and so the page source) used to carry the reviewer's photo
|
|
68
|
+
URL even where the def hides `reviewerPhoto` or the payload's `display.showExpertPhoto` is `false`, and the memo text
|
|
69
|
+
where the def hides `memo`. Now the payload is stripped of what is hidden before the card paints and before it writes
|
|
70
|
+
the seed: `reviewerPhoto` removes `content.expert.photoUrl` and `content.author.photoUrl`, `display.showExpertPhoto:
|
|
71
|
+
false` removes the same two (whatever the def hides; only a literal `false` counts, as the reader face reads it), and
|
|
72
|
+
`memo` removes `content.memo`. The face is painted from the
|
|
73
|
+
stripped payload, on the server and in the browser, so the two stay identical. The seed then **carries no
|
|
74
|
+
signature**: it signs the full payload, a stripped one no longer verifies, the browser never re-checks a seed's
|
|
75
|
+
signature (it re-runs the lifecycle and subject policy only, which read nothing that is stripped), and a pair that
|
|
76
|
+
fails to verify would only mislead. A seed that stripped nothing keeps the signature. `content.expert.compensationCue`
|
|
77
|
+
is never stripped: it is a locked disclosure, and the banner, the memo and the sidebar paint "Compensated expert" from it (the floating pill's own markup carries no
|
|
78
|
+
disclosure line, as in the portal's snippets; the modal it opens paints the cue). One visible
|
|
79
|
+
consequence: a reader def that hides `memo` now paints no memo on the Builder card (the reader face called directly,
|
|
80
|
+
and the portal's snippets, still paint the memo they are given).
|
|
81
|
+
- **The reader face's avatar is the person's initials where it has no photo to paint.** The silhouette is now the
|
|
82
|
+
fallback for a name with no initials, not for a missing photo. A photo that is missing or unsafe, `display.showExpertPhoto:
|
|
83
|
+
false`, and a def that hides `reviewerPhoto` (which used to remove the avatar altogether, and now removes the photo:
|
|
84
|
+
its URL is never written) all paint the initials. The initials are fixed bytes the portal's Liquid and PHP twins
|
|
85
|
+
print too: trim the name, split on single spaces and skip the empty parts (so "Jane Doe", two spaces, is "JD"), drop a
|
|
86
|
+
leading dr, mr, mrs, ms, prof or mx (with or without the dot), take the first character of each of the first two
|
|
87
|
+
parts left, uppercase, and paint exactly
|
|
88
|
+
`<svg viewBox="0 0 40 40" aria-hidden="true" focusable="false"><text x="20" y="20" text-anchor="middle"
|
|
89
|
+
dominant-baseline="central" font-size="15" font-weight="700">JD</text></svg>`, escaped. It applies to every avatar of the face
|
|
90
|
+
(the sidebar reviewer, the banner and memo reviewer, and the banner's writer under the writer's own name). A photo that is
|
|
91
|
+
painted is the same `<img>` as before.
|
|
92
|
+
- **The reader card loads no font file.** Its inlined `<style>` is `READER_CSS` without the 40 remote
|
|
93
|
+
`@font-face` rules (`https://assets.certrev.com/fonts/…`), so the card needs no `font-src` and logs no
|
|
94
|
+
violation on a strict CSP. The four metric Fallback faces stay, so text paints in Arial and Times New Roman
|
|
95
|
+
sized to the demo's fonts and the layout holds. `READER_CSS` and `./reader.css` are unchanged and still
|
|
96
|
+
carry all 44 rules: a host that wants certrev.com's faces links `./reader.css` and allows the origin.
|
|
97
|
+
- **The reader sidebar card's float leaves its Builder block (canary.6).** Builder serves a page's blocks as a flex
|
|
98
|
+
column (`.builder-blocks`), and each block (`.builder-block`) is a flex item, which is a formatting context of its
|
|
99
|
+
own and so contains a float. The block around a reader sidebar card therefore spanned the whole column at the
|
|
100
|
+
aside's height (at 1280: 1208 px wide), and no copy could sit beside the aside. A reader face now paints one more
|
|
101
|
+
element right after its wrapper: `<style data-certrev-builder-flow="">` holding `BUILDER_SIDEBAR_FLOW_CSS`
|
|
102
|
+
(exported from `./builder/cert-review-card`). Its three rules apply only above the reader CSS's own stacking
|
|
103
|
+
breakpoint (`@media not all and (max-width: 820px)`), and only in a Builder column holding a reader SIDEBAR card
|
|
104
|
+
as `.builder-blocks > .builder-block > [data-certrev-face="reader"] > .certrev-reader > .certrev-r-side`:
|
|
105
|
+
- that column becomes `display: flow-root`;
|
|
106
|
+
- the card's block and the face's wrapper become `display: block`, so they take no height and the aside is the
|
|
107
|
+
box the copy flows around.
|
|
108
|
+
|
|
109
|
+
Any other column, a card that is not a direct child of a Builder block, the stacked widths and a browser with no
|
|
110
|
+
`:has()` keep their layout. In a column the rules do apply, its blocks lay out as blocks: `align-self` has no
|
|
111
|
+
effect there, and vertical margins between blocks collapse. `reader-face-sidebar-flow.test.tsx` pins the element,
|
|
112
|
+
the breakpoint read from the reader CSS, and what the rules select in a Builder-shaped page.
|
|
113
|
+
- **Bytes.** The legacy face's HTML is unchanged. The reader face's is unchanged except the avatars above and the
|
|
114
|
+
flow rules' `<style>` after the wrapper. The wrapper of either gains the one attribute, and the card's inlined
|
|
115
|
+
stylesheet loses the remote font faces. The cache
|
|
116
|
+
keeps the envelope's signature beside the verdict so the seed can carry it.
|
|
117
|
+
- **Tests.** `delivery-card-hydrate.test.tsx` (rewritten: zero browser reads on hydration for both faces, the
|
|
118
|
+
same DOM nodes, the seed verifying against the issuer key, revoked and browser-clock-expired cards, a later
|
|
119
|
+
client mount, the options and none records kept through hydration and through a client re-render after a
|
|
120
|
+
hydration error with no read, the no-record client mount making no read without `clientRead` and with it reading
|
|
121
|
+
once or failing closed silently, a broken or pre-failed photo repainted as initials, every server render under
|
|
122
|
+
jsdom with no switch), `delivery-card.test.tsx` (each unreachable case paints the 1.0.3 options face byte for
|
|
123
|
+
byte with an options record; the def layout and an editor's mode; a pinned card's options record; the 8 s deadline; a
|
|
124
|
+
read slower than 3 s still painting; the stale window over a CertREV outage), `cache.test.ts` (new: the stale
|
|
125
|
+
window, stale-if-error and the in-flight limit, and the cache unchanged without them) and `ssr-seed.test.tsx` (the seed and fallback-record parsers' refusals, `withoutPhotos`, the design pin in the seed key, the hidden-field
|
|
126
|
+
strip and the signature decision, and a card stylesheet with no font URL). The reader tests gain the initials
|
|
127
|
+
cases (`reader-initials.test.ts`).
|
|
43
128
|
|
|
44
129
|
## 1.2.0-canary.5 · 2026-10-06
|
|
45
130
|
|
package/README.md
CHANGED
|
@@ -295,8 +295,10 @@ brand's render-def (accent / surface / corners / font + v2 bar-ink / body-ink).
|
|
|
295
295
|
certrev.com's "reader" design (a banner card plus the expert memo, a sidebar card, a floating pill) is a
|
|
296
296
|
**second face** beside the one every earlier version paints. A `delivery`-marker card paints it only when
|
|
297
297
|
the Delivery API's render def says `face: 'reader'`, spelled exactly. Any other def (no `face`, `legacy`, an
|
|
298
|
-
unknown name, a non-string) paints exactly the bytes 1.1.1 painted: the byte tests in
|
|
299
|
-
`src/builder/__tests__/legacy-face-1.1.1.test.ts` compare it to the 1.1.1 tag's own output.
|
|
298
|
+
unknown name, a non-string) paints exactly the face bytes 1.1.1 painted: the byte tests in
|
|
299
|
+
`src/builder/__tests__/legacy-face-1.1.1.test.ts` compare it to the 1.1.1 tag's own output. (From 1.2.0
|
|
300
|
+
the face's wrapper also carries its `data-certrev-envelope` seed; see
|
|
301
|
+
[The delivery card in the browser](#the-delivery-card-in-the-browser-120).) A block without
|
|
300
302
|
the `delivery` marker (an options-only block) never reads the envelope, so it stays on the legacy face.
|
|
301
303
|
|
|
302
304
|
- **Same bytes as the Liquid.** The markup is the Shopify `certrev-reader-*` snippets', and the portal's
|
|
@@ -312,10 +314,13 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
|
|
|
312
314
|
and the four metric "* Fallback" faces (`src: local(Arial)`, so no fetch). They are declared at
|
|
313
315
|
`font-stretch: 101%` and the reader root asks for 101%, so they apply to reader text only and never to a
|
|
314
316
|
legacy face of the same name. The brand font stacks are the demo's own (`'DM Sans', ui-sans-serif, ...`);
|
|
315
|
-
there are no "CertREV Reader" aliases. The
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
there are no "CertREV Reader" aliases. **The card's own `<style>` (1.2.0) is `READER_CSS` without the 40
|
|
318
|
+
remote faces**: it keeps the four Fallback faces and names no font URL, so a card loads no font file and
|
|
319
|
+
needs no `font-src`, and a storefront whose CSP allows no CertREV origin logs no violation. Its text paints
|
|
320
|
+
in the Fallback metric faces (Arial and Times New Roman sized to the demo's fonts, so the layout matches)
|
|
321
|
+
unless the page already declares the demo's families. A host that wants certrev.com's own faces links
|
|
322
|
+
`@certrev/cert-block/reader.css` (which keeps all 44 rules) from its own stylesheet and allows
|
|
323
|
+
`font-src https://assets.certrev.com`; the card then paints in them with no change of its own.
|
|
319
324
|
Every declaration on the `.certrev-reader` root is `!important`, a guard rule makes descendant text inherit
|
|
320
325
|
the reader's font, colour and spacing over a host theme's element rules, and an image armor keeps `img`
|
|
321
326
|
corners square (`border-radius: 0 !important`; Hydrogen's starter `app.css` rounds every `img` 4px).
|
|
@@ -347,17 +352,99 @@ the `delivery` marker (an options-only block) never reads the envelope, so it st
|
|
|
347
352
|
comment opener or a bracket) paints the default, navy `#0A1B3F` and `14px`, and never reaches the
|
|
348
353
|
`style` attribute. The font is one of five stacks (`sans`, `serif`, `grotesk`, `mono`, `system`); an
|
|
349
354
|
unknown slot paints sans.
|
|
350
|
-
- **Avatars.** Every byline paints a round avatar: the
|
|
351
|
-
|
|
352
|
-
reviewer
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
the
|
|
355
|
+
- **Avatars.** Every byline paints a round avatar: the person's photo when there is one to paint, else their
|
|
356
|
+
initials, else the silhouette (a name with no initials). It is 36 px for the writer, 42 px for the banner
|
|
357
|
+
reviewer, 46 px for the memo and sidebar reviewer. The initials and the silhouette are an inline `<svg>`, not an
|
|
358
|
+
`<img>`, so a page with no photos holds no avatar image. The initials are the same bytes the portal's Liquid
|
|
359
|
+
snippets and WordPress renderer print: the name trimmed and split on single spaces (empty parts skipped, so
|
|
360
|
+
"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,
|
|
361
|
+
uppercased, in
|
|
362
|
+
`<svg viewBox="0 0 40 40" aria-hidden="true" focusable="false"><text x="20" y="20" text-anchor="middle" dominant-baseline="central" font-size="15" font-weight="700">JD</text></svg>`
|
|
363
|
+
(the initials escaped); an empty result paints the silhouette. A def that hides `reviewerPhoto`, and
|
|
364
|
+
`display.showExpertPhoto: false`, never read a photo URL and paint the initials: the hidden photo is removed, not
|
|
365
|
+
the avatar. `reviewerPhoto` takes the reviewer's and the writer's photo alike, which is data on the brand, not a face
|
|
366
|
+
setting.
|
|
356
367
|
- **Credential line.** The banner prints the reviewer's credential, a middle dot and the attribution as
|
|
357
368
|
three leaves of one `<em>` (`RDN, LD, PhD · Credential verified by CertREV on May 1, 2026`); a reviewer
|
|
358
369
|
with no displayed credential makes no verification claim. The writer's `<em>` under the name is the
|
|
359
370
|
envelope's `author.title`, printed only when it is non-empty.
|
|
360
371
|
|
|
372
|
+
### The delivery card in the browser (1.2.0)
|
|
373
|
+
|
|
374
|
+
A `delivery`-marker card reads the signed envelope (and the brand's render def) from the Delivery API **on
|
|
375
|
+
the server**, and the face it paints carries that read as one attribute on its wrapper:
|
|
376
|
+
`data-certrev-envelope` = `{ v: 1, baseUrl, externalId, design?, payload, signature?, renderDef }`. The payload is
|
|
377
|
+
the served one less the fields the def hides (below), and `design` is the design pin the read was made with. The
|
|
378
|
+
browser hydrates from it:
|
|
379
|
+
|
|
380
|
+
- **A server-rendered card makes no browser read.** It hydrates from its own `data-certrev-envelope`,
|
|
381
|
+
byte for byte the server's face, so a storefront whose CSP `connect-src` names no CertREV origin keeps
|
|
382
|
+
the card painted and logs nothing. Through 1.1.1 the browser re-read the Delivery API on hydration, and a
|
|
383
|
+
blocked read took the server's card down.
|
|
384
|
+
- **Where the server had no envelope to paint, it writes what it painted instead**, in the same attribute: a
|
|
385
|
+
record `{ v: 1, baseUrl, externalId, design?, face }` with no payload, no signature and no def. The browser keeps
|
|
386
|
+
that face through hydration and any later render of the same placement, and reads nothing:
|
|
387
|
+
- **CertREV not reached** (a 5xx, a body that is not an envelope, a network error, the 8 s deadline): the
|
|
388
|
+
block's own options face, the card 0.5.x and 1.0.x painted from the same fields, with `face: "options"`. Its
|
|
389
|
+
placement is the block's `mode`, else its def's `layout`, else the banner. A page never loses its card to an
|
|
390
|
+
outage. A pinned card (below) paints it too, with a record naming its pin. A block whose options cannot
|
|
391
|
+
paint (no reviewer) paints nothing, with `face: "none"`.
|
|
392
|
+
- **A definitive no** (revoked, expired, unverifiable, another entry's envelope, a 404 or 410): nothing, with
|
|
393
|
+
`face: "none"`, so a revocation is never painted over by the options face.
|
|
394
|
+
- **After hydration** the card re-runs the lifecycle and subject policy at the browser's clock: a cert
|
|
395
|
+
whose `expiresAt` passed while the page was open comes down. The signature is not re-checked in the
|
|
396
|
+
browser (whoever can write the page's HTML can already write any card), and a revocation shows on the
|
|
397
|
+
next page load, which is when the server reads again.
|
|
398
|
+
- **A card the browser mounts later** (an in-app navigation, a re-render) paints from any seed the page's
|
|
399
|
+
server HTML carried for the same placement, so it makes no read either. A pinned card (the design pin,
|
|
400
|
+
below) takes only a seed carrying the same pin.
|
|
401
|
+
- **A card with no server render behind it** (a client-only page, Builder's visual editor preview) **makes no
|
|
402
|
+
read**: it paints the block's own options face, as the unreachable case does, pinned or not, or nothing (a
|
|
403
|
+
block with no options). The browser never fetches by default, so no brand's CSP needs a CertREV origin.
|
|
404
|
+
- **`clientRead` (a prop, off by default) opts in to that read.** A card with `clientRead` and no server render
|
|
405
|
+
behind it reads the Delivery API once from the browser and settles exactly as a server does. That read needs
|
|
406
|
+
`connect-src` for the Delivery origin; where it is blocked or fails, the card **renders nothing and logs
|
|
407
|
+
nothing**, never a broken card and never the host's tree unmounted. It is not a Builder input: a brand that wants
|
|
408
|
+
it sets it in code, on a wrapper of the registered component.
|
|
409
|
+
- **The server's read** waits up to 8 s (the verifier's own default stays 3 s). Its cache keeps a certificate for
|
|
410
|
+
60 s, then serves it for up to 5 minutes more while one background refresh runs or while CertREV cannot be
|
|
411
|
+
reached; a refusal (revoked, expired) lands as soon as its refresh does. A cold placement waits once; a warm one
|
|
412
|
+
never waits.
|
|
413
|
+
- **A reviewer photo the browser cannot load** (a CSP `img-src` without the photo's origin, a 404) is repainted as
|
|
414
|
+
the person's initials, the same bytes the face paints for a hidden photo (below), with no request and no change to
|
|
415
|
+
the record.
|
|
416
|
+
- **Server render or hydration is decided from the page, with no switch.** React tells a component nothing
|
|
417
|
+
about which it is in, and under jsdom (or a DOM shim) a `react-dom/server` render sees a `document` too.
|
|
418
|
+
So the card reads what a hydration has and a server render cannot: server-rendered HTML in the document. No
|
|
419
|
+
`document` is a server; a card mounted on the client, or past its hydration, is a browser; a card that is
|
|
420
|
+
hydrating is a browser only if the live document holds this placement's seed or a React Suspense boundary marker
|
|
421
|
+
(the card's own `<Suspense>` leaves one even where the server painted no card), and otherwise is a server render,
|
|
422
|
+
which reads and seeds as a server does. The one case that is not seen is a server render run while the document
|
|
423
|
+
still holds server-rendered React HTML (a test or shim that hydrates one page and renders another into the same
|
|
424
|
+
document): it is taken for a hydration, so clear the document between pages in such a harness.
|
|
425
|
+
|
|
426
|
+
#### Hidden fields are absent from the page
|
|
427
|
+
|
|
428
|
+
A def's `hidden` set (`reviewerPhoto`, `memo`) and the payload's `display.showExpertPhoto: false` are the brand's
|
|
429
|
+
statement that the page does not show those fields, so the seed does not carry them either. Before the card paints and
|
|
430
|
+
before it writes the seed, the payload is stripped:
|
|
431
|
+
|
|
432
|
+
| hidden by | removed from the payload |
|
|
433
|
+
| --- | --- |
|
|
434
|
+
| the def's `reviewerPhoto` | `content.expert.photoUrl` and `content.author.photoUrl` (the reader face paints the writer's photo under the same switch) |
|
|
435
|
+
| `display.showExpertPhoto: false` (a literal `false`; absent or `null` shows the photo) | the same two photo URLs, whatever the def hides |
|
|
436
|
+
| the def's `memo` | `content.memo` |
|
|
437
|
+
|
|
438
|
+
The face is painted from the stripped payload on the server and in the browser, so the two are identical and the page
|
|
439
|
+
source holds neither the photo URL nor the memo text. `content.expert.compensationCue` is never stripped: it is a locked
|
|
440
|
+
disclosure no def can hide, and the banner, the memo and the sidebar paint "Compensated expert" from it (the floating pill's own markup carries no
|
|
441
|
+
disclosure line, as in the portal's snippets; the modal it opens paints the cue). **When anything was stripped the seed
|
|
442
|
+
carries no `signature`.** The signature signs the whole payload, so a stripped payload no longer verifies against it;
|
|
443
|
+
the browser never re-checks a seed's signature, only the lifecycle and subject policy at its own clock, and neither reads
|
|
444
|
+
a stripped field. A seed that stripped nothing keeps the signature, so its payload can still be checked against the issuer
|
|
445
|
+
key. A reader def that hides `memo` paints no memo card on the Builder card (the reader face called directly, like the
|
|
446
|
+
portal's Shopify snippets, paints the memo it is given).
|
|
447
|
+
|
|
361
448
|
### Design pin (staging replica)
|
|
362
449
|
|
|
363
450
|
1.2.0-canary.5. **This is for CertREV's own canary replica only; production brands never send it.** The
|
|
@@ -390,8 +477,12 @@ the same URL, under the same cache key, and paints the same bytes as before.
|
|
|
390
477
|
fetched.
|
|
391
478
|
- a pin on a block without the `delivery` marker. The options face is never the pinned design, and nothing
|
|
392
479
|
is fetched.
|
|
393
|
-
- a 404 (`no_design`)
|
|
480
|
+
- a 404 (`no_design`) from the API, and every verdict that already suppresses an unpinned card
|
|
394
481
|
(revoked, expired, unverifiable, another entry).
|
|
482
|
+
|
|
483
|
+
CertREV not reached (a 503, a timeout, a network error) is not a fail-closed case since 1.2.0: a pinned card
|
|
484
|
+
paints the block's own options face, as an unpinned card does, with a record that names the pin and says
|
|
485
|
+
`options`. That is never the live design, and the record tells a capture it is not the pinned one either.
|
|
395
486
|
- a def that is anything other than a **reader** def (`face: 'reader'`) at **exactly** the pinned version.
|
|
396
487
|
That includes no def, a malformed def, a legacy or unknown face, and another version.
|
|
397
488
|
|
|
@@ -466,12 +557,15 @@ import certScriptUrl from '@certrev/cert-block/modal/certrev-cert.js?url'
|
|
|
466
557
|
<script src={certScriptUrl} defer />
|
|
467
558
|
```
|
|
468
559
|
|
|
469
|
-
CSP allowances for the headless host. The **registered card** needs none
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
`
|
|
474
|
-
|
|
560
|
+
CSP allowances for the headless host. The **registered card** needs none when the server renders it
|
|
561
|
+
(1.2.0): it hydrates from the envelope in its own server HTML with no browser read (see
|
|
562
|
+
[The delivery card in the browser](#the-delivery-card-in-the-browser-120)), fetches no font (the reader
|
|
563
|
+
face's inlined stylesheet names no font URL), and paints its logo and silhouette avatars inline. Two
|
|
564
|
+
things a host may still want: `img-src` for the reviewer's photo when the brand's def shows it (a reader
|
|
565
|
+
face `<img>` of the envelope's `photoUrl`, usually `https://assets.certrev.com`; a def that hides
|
|
566
|
+
`reviewerPhoto` loads no image, and a photo the policy blocks is repainted as the person's initials), and
|
|
567
|
+
`connect-src` only for a block that opts in to a browser read with `clientRead`. These are what the **modal**
|
|
568
|
+
needs:
|
|
475
569
|
|
|
476
570
|
- `connect-src` → the CertREV Delivery origin the element fetches the signed envelope from
|
|
477
571
|
(production `https://portal.certrev.com`; staging `https://staging.portal.certrev.com`).
|
|
@@ -731,7 +825,7 @@ notification channel, because for those consumers it is the only one.
|
|
|
731
825
|
|
|
732
826
|
## Tests
|
|
733
827
|
|
|
734
|
-
`pnpm test` (Vitest) runs
|
|
828
|
+
`pnpm test` (Vitest) runs every test file. They sit next to what they cover: `src/__tests__/`
|
|
735
829
|
for the cross-cutting suites, `src/builder/__tests__/` for the Builder kit, and `src/modal/*.test.ts`
|
|
736
830
|
alongside each modal module. The load-bearing ones:
|
|
737
831
|
|
|
@@ -742,6 +836,7 @@ alongside each modal module. The load-bearing ones:
|
|
|
742
836
|
- **`__tests__/escape-family.test.ts`** puts both escape families (`components/` and `modal/`) against wrong-typed input, not just null: a half-filled CMS object must degrade, never throw.
|
|
743
837
|
- **`__tests__/webcomponent.test.tsx`**: the shared `renderBadgeHtml` string renderer (hand-escaping, unsafe-URL/color dropping, compact style) and the `<certrev-badge>` custom element (idempotent registration, SSR light-DOM preservation, client-mode fail-closed without a resolver).
|
|
744
838
|
- **`builder/__tests__/builder.test.tsx`** covers the PUSH card and its registration: the wire-type ⇄ inputs parity lock, the fused-credential gate, and the mistyped-option degradation.
|
|
839
|
+
- **`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).
|
|
745
840
|
- **`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).
|
|
746
841
|
- **`__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.
|
|
747
842
|
|
|
@@ -750,7 +845,7 @@ published at `@certrev/cert-block/fixtures`).
|
|
|
750
845
|
|
|
751
846
|
## Version
|
|
752
847
|
|
|
753
|
-
`1.2.0
|
|
848
|
+
`1.2.0`: see [CHANGELOG.md](./CHANGELOG.md) for the release history and the semver contract
|
|
754
849
|
(patch/minor never change the rendered cert output or break a compiling integration). Publishes
|
|
755
850
|
**publicly** to npm as `@certrev/cert-block` (`publishConfig.access: public`) via GitHub Actions
|
|
756
851
|
trusted publishing (OIDC); the internal `@certrev` GitHub-Packages channel mirrors it.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader stylesheet as the Builder card inlines it (1.2.0): `READER_CSS` without its remote `@font-face` rules.
|
|
3
|
+
*
|
|
4
|
+
* The portal's reader CSS loads its faces from `https://assets.certrev.com/fonts/…`. A storefront whose CSP
|
|
5
|
+
* `font-src` does not name that origin (e.l.f.'s live policy names `'self' data:` and a list of font CDNs, no
|
|
6
|
+
* CertREV origin) refuses each load and logs a violation, and the text falls back anyway. So the card ships only the
|
|
7
|
+
* `local()` fallback faces (Arial or Times New Roman under each family's metric overrides), which need no CSP at all:
|
|
8
|
+
* the face looks the same as it does on a page that blocks the remote fonts, with nothing refused. The portal and the
|
|
9
|
+
* Shopify reader keep `READER_CSS` unchanged.
|
|
10
|
+
*
|
|
11
|
+
* INTERNAL: not on the package `exports` map.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CARD_READER_CSS: string;
|
|
14
|
+
//# sourceMappingURL=card-reader-css.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card-reader-css.d.ts","sourceRoot":"","sources":["../../src/builder/card-reader-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,eAAO,MAAM,eAAe,QAA2C,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader stylesheet as the Builder card inlines it (1.2.0): `READER_CSS` without its remote `@font-face` rules.
|
|
3
|
+
*
|
|
4
|
+
* The portal's reader CSS loads its faces from `https://assets.certrev.com/fonts/…`. A storefront whose CSP
|
|
5
|
+
* `font-src` does not name that origin (e.l.f.'s live policy names `'self' data:` and a list of font CDNs, no
|
|
6
|
+
* CertREV origin) refuses each load and logs a violation, and the text falls back anyway. So the card ships only the
|
|
7
|
+
* `local()` fallback faces (Arial or Times New Roman under each family's metric overrides), which need no CSP at all:
|
|
8
|
+
* the face looks the same as it does on a page that blocks the remote fonts, with nothing refused. The portal and the
|
|
9
|
+
* Shopify reader keep `READER_CSS` unchanged.
|
|
10
|
+
*
|
|
11
|
+
* INTERNAL: not on the package `exports` map.
|
|
12
|
+
*/
|
|
13
|
+
import { READER_CSS } from '../reader/reader-css.generated.js';
|
|
14
|
+
/** One `@font-face { … }` rule whose body has a `url(`; the reader CSS has no nested braces inside a face rule. */
|
|
15
|
+
const REMOTE_FONT_FACE = /@font-face\s*\{[^{}]*\burl\([^{}]*\}\s*/g;
|
|
16
|
+
export const CARD_READER_CSS = READER_CSS.replace(REMOTE_FONT_FACE, '');
|
|
17
|
+
//# sourceMappingURL=card-reader-css.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card-reader-css.js","sourceRoot":"","sources":["../../src/builder/card-reader-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAA;AAE9D,mHAAmH;AACnH,MAAM,gBAAgB,GAAG,0CAA0C,CAAA;AAEnE,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAA"}
|
|
@@ -62,6 +62,14 @@ export interface CertReviewCardProps extends Partial<BuilderCertChromeData> {
|
|
|
62
62
|
readonly certrevDesignPin?: unknown;
|
|
63
63
|
} | null;
|
|
64
64
|
} | null;
|
|
65
|
+
/**
|
|
66
|
+
* 1.2.0: opt in to a Delivery read FROM THE BROWSER for a card the server did not render (a client-side navigation,
|
|
67
|
+
* Builder's editor). Off by default: the card then never makes a request from the browser, so a brand's
|
|
68
|
+
* Content-Security-Policy needs no CertREV origin, and such a card paints the block's own options face. Turning it
|
|
69
|
+
* on needs the Delivery API's origin in the page's `connect-src`. Not a Builder input: a brand sets it in code, on
|
|
70
|
+
* a wrapper of this component.
|
|
71
|
+
*/
|
|
72
|
+
clientRead?: boolean;
|
|
65
73
|
}
|
|
66
74
|
/**
|
|
67
75
|
* The delivery marker, accepted only as `{ v: 1, baseUrl }` with `baseUrl` an `https:` ORIGIN on
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cert-review-card.d.ts","sourceRoot":"","sources":["../../src/builder/cert-review-card.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAE,KAAK,aAAa,EAAmB,MAAM,oCAAoC,CAAA;
|
|
1
|
+
{"version":3,"file":"cert-review-card.d.ts","sourceRoot":"","sources":["../../src/builder/cert-review-card.tsx"],"names":[],"mappings":"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"}
|