@certrev/cert-block 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +50 -10
  3. package/dist/builder/card-beacon.d.ts +73 -0
  4. package/dist/builder/card-beacon.d.ts.map +1 -0
  5. package/dist/builder/card-beacon.js +178 -0
  6. package/dist/builder/card-beacon.js.map +1 -0
  7. package/dist/builder/card-contract.d.ts +10 -2
  8. package/dist/builder/card-contract.d.ts.map +1 -1
  9. package/dist/builder/card-contract.js +27 -3
  10. package/dist/builder/card-contract.js.map +1 -1
  11. package/dist/builder/card-placement.d.ts +45 -0
  12. package/dist/builder/card-placement.d.ts.map +1 -0
  13. package/dist/builder/card-placement.js +193 -0
  14. package/dist/builder/card-placement.js.map +1 -0
  15. package/dist/builder/card-report.d.ts +26 -14
  16. package/dist/builder/card-report.d.ts.map +1 -1
  17. package/dist/builder/card-report.js +68 -14
  18. package/dist/builder/card-report.js.map +1 -1
  19. package/dist/builder/cert-review-card.d.ts +6 -4
  20. package/dist/builder/cert-review-card.d.ts.map +1 -1
  21. package/dist/builder/cert-review-card.js +172 -199
  22. package/dist/builder/cert-review-card.js.map +1 -1
  23. package/dist/builder/contract-card.d.ts +18 -1
  24. package/dist/builder/contract-card.d.ts.map +1 -1
  25. package/dist/builder/contract-card.js +60 -10
  26. package/dist/builder/contract-card.js.map +1 -1
  27. package/dist/builder/delivery-runtime.d.ts +38 -4
  28. package/dist/builder/delivery-runtime.d.ts.map +1 -1
  29. package/dist/builder/delivery-runtime.js +43 -5
  30. package/dist/builder/delivery-runtime.js.map +1 -1
  31. package/dist/builder/index.d.ts +4 -4
  32. package/dist/builder/index.js +4 -4
  33. package/dist/builder/ssr-seed.d.ts +5 -4
  34. package/dist/builder/ssr-seed.d.ts.map +1 -1
  35. package/dist/builder/ssr-seed.js +4 -3
  36. package/dist/builder/ssr-seed.js.map +1 -1
  37. package/dist/builder/version.d.ts +3 -0
  38. package/dist/builder/version.d.ts.map +1 -0
  39. package/dist/builder/version.js +3 -0
  40. package/dist/builder/version.js.map +1 -0
  41. package/dist/verify/cache.d.ts +24 -0
  42. package/dist/verify/cache.d.ts.map +1 -1
  43. package/dist/verify/cache.js +78 -6
  44. package/dist/verify/cache.js.map +1 -1
  45. package/dist/verify/get-verified-delivery.d.ts +18 -0
  46. package/dist/verify/get-verified-delivery.d.ts.map +1 -1
  47. package/dist/verify/get-verified-delivery.js +30 -3
  48. package/dist/verify/get-verified-delivery.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/builder/card-beacon.ts +201 -0
  51. package/src/builder/card-contract.ts +25 -3
  52. package/src/builder/card-placement.ts +208 -0
  53. package/src/builder/card-report.ts +79 -14
  54. package/src/builder/cert-review-card.tsx +208 -238
  55. package/src/builder/contract-card.tsx +65 -8
  56. package/src/builder/delivery-runtime.ts +45 -5
  57. package/src/builder/index.tsx +4 -4
  58. package/src/builder/ssr-seed.ts +5 -4
  59. package/src/builder/version.ts +2 -0
  60. package/src/verify/cache.ts +78 -6
  61. package/src/verify/get-verified-delivery.ts +40 -3
@@ -22,14 +22,17 @@ import * as React from 'react'
22
22
  import { Suspense } from 'react'
23
23
  import { memoToggleFor, toggleMemo } from '../components/memo-toggle.js'
24
24
  import { type CertBlockMode, renderCertBlock } from '../components/render-cert-block.js'
25
- import { CERT_STRINGS_VERSION, type ResolvedBlockTheme } from '../components/render-def.js'
25
+ import { CERT_STRINGS_VERSION } from '../components/render-def.js'
26
26
  import { type CertPayload, renderVerdict } from '../contract/kernel.js'
27
27
  import {
28
28
  type DeliveredFace,
29
29
  type DesignPin,
30
+ deliveryLoading,
30
31
  getVerifiedDelivery,
31
32
  parseDesignPin,
32
33
  readVerifiedDelivery,
34
+ rememberDeliveryUnreachable,
35
+ unreachableDelivery,
33
36
  type VerifiedDelivery,
34
37
  } from '../verify/get-verified-delivery.js'
35
38
  import {
@@ -45,6 +48,7 @@ import {
45
48
  } from './card-contract.js'
46
49
  import { CARD_READER_CSS } from './card-reader-css.js'
47
50
  import {
51
+ CARD_FALLBACK_REASONS,
48
52
  type CardFallbackDetail,
49
53
  type CardFallbackReason,
50
54
  reportCardFallbackInBrowser,
@@ -64,7 +68,6 @@ import {
64
68
  import { cardDeliveryRuntime, cardResolveKidFor } from './delivery-runtime.js'
65
69
  import { envelopeCardHtml, envelopeCardInput, isReaderFaceDef } from './envelope-face.js'
66
70
  import {
67
- type FallbackFace,
68
71
  type PageRecord,
69
72
  pageHoldsServerHtml,
70
73
  pageRecordFor,
@@ -121,10 +124,10 @@ export interface CertReviewCardProps extends Partial<BuilderCertChromeData> {
121
124
  * A Delivery read FROM THE BROWSER for a card the server did not render (a client-side navigation, Builder's editor).
122
125
  * 2.0.0: ON by default (absent or `true`): it is the only way CertREV's server-rendered card exists after an in-app
123
126
  * navigation, and it needs the Delivery API's origin (`https://portal.certrev.com`) in the page's `connect-src`, one
124
- * of the four CSP sources 2.0 asks for. A read the policy blocks is a read that did not reach CertREV: the block's
125
- * own options face, as 1.3.1 painted it (and a reported `delivery_unreachable`). `false` restores 1.2.0 to 1.3.1's
126
- * behaviour: no request from the browser, and such a card paints the block's options face. Not a Builder input: a
127
- * brand sets it in code, on a wrapper of this component.
127
+ * of the four CSP sources 2.0 asks for. A read the policy blocks is a read that did not reach CertREV: nothing, and a
128
+ * reported `delivery_unreachable` (2.0.1; 2.0.0 painted the block's options face). `false`: no request from the
129
+ * browser, and such a card prints nothing and reports `client_read_off`. Not a Builder input: a brand sets it in code,
130
+ * on a wrapper of this component.
128
131
  */
129
132
  clientRead?: boolean
130
133
  }
@@ -143,28 +146,6 @@ function text(value: unknown): string {
143
146
  return typeof value === 'string' ? value : ''
144
147
  }
145
148
 
146
- /** The same rule where ABSENT and EMPTY are different renders (`authorTitle`, `bio`). */
147
- function optionalText(value: unknown): string | undefined {
148
- return text(value) || undefined
149
- }
150
-
151
- /**
152
- * The fused credential pair — accepted ONLY when both halves are present AND both are strings.
153
- *
154
- * The R1 gate ("a credential never displays without its dated verification") is THIS FUNCTION,
155
- * not the type: the pair arrives as untyped CMS JSON, and the old truthiness check keyed the
156
- * whole pair off the OBJECT, so `{credential:'MD'}` forwarded `verifiedAt: undefined` into the
157
- * renderer and crashed. Anything malformed — a half pair, a bare string, an array, a number
158
- * where a date belongs — degrades to the name-only face, which is the honest render of a
159
- * delivery whose verification cannot be read.
160
- */
161
- function fusedCredential(pair: BuilderCertChromeData['credentialVerification'] | undefined) {
162
- if (!pair || typeof pair !== 'object') return null
163
- const credential = text(pair.credential)
164
- const verifiedAt = text(pair.verifiedAt)
165
- return credential && verifiedAt ? { credential, verifiedAt } : null
166
- }
167
-
168
149
  /**
169
150
  * The delivery marker, accepted only as `{ v: 1, baseUrl }` with `baseUrl` an `https:` ORIGIN on
170
151
  * `certrev.com` or one of its subdomains (no path, query, credentials or port). Anything else is
@@ -193,17 +174,82 @@ export function acceptedDeliveryMarker(value: unknown): { readonly baseUrl: stri
193
174
  * flight; React 18 (no `use`) gets the promise thrown, the Suspense protocol it supports. Either
194
175
  * way the retry finds the settled read in the cache through `readVerifiedDelivery`, so it never
195
176
  * suspends on a second promise.
177
+ *
178
+ * 2.0.2, on a server (`suspendOnServer`): the render waits at most the runtime's `serverWaitMs` (300 ms). Past it, the
179
+ * placement's failure is remembered in the cache (so every render in the failure window answers at once, even where the
180
+ * runtime abandons the read when the response ends), the render prints nothing (`delivery_unreachable`), and the read
181
+ * runs on: when it lands, the next render paints it. A render that finds a read of the placement already running that it
182
+ * did not start (another request's, a background refresh, or a second card of the placement on the same page: React
183
+ * gives a component no way to tell these apart) never waits on it: it is `'skipped'` and prints a marker only
184
+ * (`SkippedRead`), which the same card instance re-renders while hydrating and replaces with the page's verified seed
185
+ * right after.
196
186
  */
197
- const inflightReads = new Map<string, Promise<VerifiedDelivery>>()
198
- function suspendOn(key: string, start: () => Promise<VerifiedDelivery>): VerifiedDelivery {
199
- let pending = inflightReads.get(key)
187
+ /** The reads a render is suspended on, per runtime cache (a fresh runtime, a fresh page in a test, starts with none). */
188
+ const inflightReadsByCache = new WeakMap<object, Map<string, Promise<VerifiedDelivery>>>()
189
+
190
+ function suspendOn(cache: object, key: string, start: () => Promise<VerifiedDelivery>): VerifiedDelivery {
191
+ let reads = inflightReadsByCache.get(cache)
192
+ if (!reads) {
193
+ reads = new Map()
194
+ inflightReadsByCache.set(cache, reads)
195
+ }
196
+ let pending = reads.get(key)
200
197
  if (!pending) {
201
- pending = start().finally(() => inflightReads.delete(key))
202
- inflightReads.set(key, pending)
198
+ const own = reads
199
+ pending = start().finally(() => own.delete(key))
200
+ reads.set(key, pending)
203
201
  }
204
202
  return suspendUntil(pending)
205
203
  }
206
204
 
205
+ function suspendOnServer(
206
+ key: string,
207
+ read: Parameters<typeof getVerifiedDelivery>[0],
208
+ waitMs: number | undefined,
209
+ ): VerifiedDelivery | 'skipped' {
210
+ if (deliveryLoading(read)) return 'skipped'
211
+ return suspendOn(read.cache ?? inflightReadsByCache, key, () => {
212
+ const loaded = getVerifiedDelivery(read)
213
+ if (waitMs === undefined || !Number.isFinite(waitMs) || waitMs <= 0) return loaded
214
+ return new Promise<VerifiedDelivery>((resolve) => {
215
+ const timer = setTimeout(() => {
216
+ rememberDeliveryUnreachable(read)
217
+ resolve(unreachableDelivery())
218
+ }, waitMs)
219
+ loaded.then(
220
+ (settled) => {
221
+ clearTimeout(timer)
222
+ resolve(settled)
223
+ },
224
+ () => {
225
+ clearTimeout(timer)
226
+ resolve(unreachableDelivery())
227
+ },
228
+ )
229
+ })
230
+ })
231
+ }
232
+
233
+ /** 2.0.2: the attribute a skipped server render's marker carries: the card instance's `useId`. */
234
+ export const SKIPPED_ATTRIBUTE = 'data-certrev-skipped'
235
+
236
+ /**
237
+ * 2.0.2: what a server render prints when it did not wait for a read it did not start: a hidden marker naming the card
238
+ * instance, and no record (it knows nothing about the placement, so it must never stand in for another card's record).
239
+ * Not a failure: nothing is reported or remembered.
240
+ */
241
+ function SkippedRead({ instance }: { readonly instance: string }) {
242
+ return <div hidden {...{ [SKIPPED_ATTRIBUTE]: instance }} />
243
+ }
244
+
245
+ /** 2.0.2: whether the live document holds this card instance's skipped marker (it is hydrating one). */
246
+ function skippedInDocument(instance: string): boolean {
247
+ for (const el of Array.from(document.querySelectorAll(`[${SKIPPED_ATTRIBUTE}]`))) {
248
+ if (el.getAttribute(SKIPPED_ATTRIBUTE) === instance) return true
249
+ }
250
+ return false
251
+ }
252
+
207
253
  /** `use` where React has it (19), else the thrown promise (18). */
208
254
  function suspendUntil<T>(pending: Promise<T>): T {
209
255
  const use = (React as unknown as { use?: <U>(p: Promise<U>) => U }).use
@@ -460,43 +506,6 @@ function paintEnvelope(
460
506
  return <EnvelopeFace html={renderCertBlock(input)} defVersion={defVersion} seed={seed} />
461
507
  }
462
508
 
463
- const FALLBACK_LAYOUTS: readonly string[] = ['banner', 'sidebar', 'floating']
464
-
465
- /**
466
- * The options face a block with the delivery marker paints in place of the envelope (1.2.0), or null. Its placement is
467
- * the block's own `mode`, else its def's layout, as the envelope face's is, else the banner bridge. A pinned card (the
468
- * canary replica) paints it too, so the replica shows what a brand's page shows when CertREV is not reached; its
469
- * record names the pin and says `'options'`, so a capture can never take it for the pinned design.
470
- */
471
- function fallbackFace(props: EnvelopeCardProps): string | null {
472
- const { block } = props
473
- const layout = block.renderDef?.layout
474
- const mode =
475
- typeof block.mode === 'string' && FALLBACK_LAYOUTS.includes(block.mode)
476
- ? block.mode
477
- : typeof layout === 'string' && FALLBACK_LAYOUTS.includes(layout)
478
- ? (layout as CertBlockMode)
479
- : undefined
480
- return optionsFaceHtml(block, mode)
481
- }
482
-
483
- /** The block's options face, stamped with a fallback record when the server wrote one (`ssr-seed.ts`). */
484
- function OptionsFace(props: { readonly block: CertReviewCardProps; readonly html: string; readonly record?: string }) {
485
- return (
486
- <div
487
- data-certrev-cert-chrome=""
488
- // The two provenance stamps the crawl monitor reads — emitted ONLY when the delivery
489
- // carried the right type, so a mistyped option leaves the attribute off instead of
490
- // stamping "[object Object]" as a version.
491
- data-certrev-strings-version={optionalText(props.block.stringsVersion)}
492
- data-certrev-def-version={typeof props.block.renderDef?.v === 'number' ? props.block.renderDef.v : undefined}
493
- {...(props.record === undefined ? {} : { [SEED_ATTRIBUTE]: props.record })}
494
- // biome-ignore lint/security/noDangerouslySetInnerHtml: renderCertBlock is the escaping-safe SSR string renderer
495
- dangerouslySetInnerHTML={{ __html: props.html }}
496
- />
497
- )
498
- }
499
-
500
509
  /** Where the server painted no face, an empty hidden element carries the record that says so. */
501
510
  function NoFace(props: { readonly record: string }) {
502
511
  return <div hidden {...{ [SEED_ATTRIBUTE]: props.record }} />
@@ -504,14 +513,19 @@ function NoFace(props: { readonly record: string }) {
504
513
 
505
514
  /**
506
515
  * 2.0.0: reports one fallback (`card-report.ts`) and renders nothing (no element, no marker: it changes no byte of the
507
- * page). On a server it reports as it renders; in a browser, once it has mounted.
516
+ * page). On a server it reports as it renders; in a browser, once it has mounted. 2.0.1: the report is also a beacon to
517
+ * `origin`, the delivery origin the block reads (none: CertREV's production origin).
508
518
  */
509
- function FallbackReport(props: { readonly detail: CardFallbackDetail; readonly server: boolean }) {
510
- const { detail, server } = props
511
- if (server) reportCardFallbackOnServer(detail)
519
+ function FallbackReport(props: {
520
+ readonly detail: CardFallbackDetail
521
+ readonly server: boolean
522
+ readonly origin?: string
523
+ }) {
524
+ const { detail, server, origin } = props
525
+ if (server) reportCardFallbackOnServer(detail, origin)
512
526
  React.useEffect(() => {
513
- reportCardFallbackInBrowser(detail)
514
- }, [detail])
527
+ reportCardFallbackInBrowser(detail, origin)
528
+ }, [detail, origin])
515
529
  return null
516
530
  }
517
531
 
@@ -522,17 +536,19 @@ function withReport(
522
536
  props: EnvelopeCardProps,
523
537
  server: boolean,
524
538
  certId?: string,
539
+ status?: number,
525
540
  ): React.ReactElement {
526
541
  const detail: CardFallbackDetail = {
527
542
  reason,
528
543
  platform: 'builder',
529
544
  externalId: props.externalId,
530
545
  ...(certId === undefined ? {} : { certId }),
546
+ ...(status === undefined ? {} : { status }),
531
547
  }
532
548
  return (
533
549
  <>
534
550
  {face}
535
- <FallbackReport detail={detail} server={server} />
551
+ <FallbackReport detail={detail} server={server} origin={props.baseUrl} />
536
552
  </>
537
553
  )
538
554
  }
@@ -596,40 +612,49 @@ function contractNone(
596
612
  }
597
613
 
598
614
  /**
599
- * What a card with no verified envelope paints: the options face when CertREV was not reached, else nothing. 2.0.0:
600
- * the same faces and records as 1.3.1, and two of them are now reported: the options face painted because CertREV
601
- * was not reached (`delivery_unreachable`), and nothing painted because the envelope's kid is unknown (`unknown_key`).
615
+ * What a card with no verified envelope paints: nothing, and a `'none'` record. 2.0.1: CertREV not reached prints
616
+ * nothing too (it painted the block's export-time options through 2.0.0), and the record names `delivery_unreachable`
617
+ * so the browser reports it as well (with CertREV's HTTP status when it answered with one); an unknown kid is reported as
618
+ * `unknown_key`, and (2.0.1) an envelope that does not verify as `envelope_verify_failed`. A definitive no (revoked,
619
+ * expired, for another entry, not found) prints nothing, by design, unreported.
602
620
  */
603
621
  function settleWithoutEnvelope(
604
622
  props: EnvelopeCardProps,
605
- verdict: { readonly unreachable: boolean; readonly reason?: string },
623
+ verdict: { readonly unreachable: boolean; readonly reason?: string; readonly status?: number },
606
624
  server: boolean,
607
625
  ): React.ReactElement {
608
- const html = verdict.unreachable ? fallbackFace(props) : null
609
- const face: FallbackFace = html === null ? 'none' : 'options'
626
+ const reason: CardFallbackReason | undefined = verdict.unreachable
627
+ ? 'delivery_unreachable'
628
+ : verdict.reason === 'unknown_key'
629
+ ? 'unknown_key'
630
+ : verdict.reason !== undefined && ENVELOPE_VERIFY_FAILURES.has(verdict.reason)
631
+ ? 'envelope_verify_failed'
632
+ : undefined
610
633
  const record = serializeFallback({
611
634
  baseUrl: props.baseUrl,
612
635
  externalId: props.externalId,
613
636
  ...(props.design ? { design: props.design } : {}),
614
- face,
637
+ face: 'none',
638
+ ...(reason ? { reason } : {}),
615
639
  })
616
- if (html !== null) {
617
- return withReport(
618
- <OptionsFace block={props.block} html={html} record={record} />,
619
- 'delivery_unreachable',
620
- props,
621
- server,
622
- )
623
- }
624
640
  const none = <NoFace record={record} />
625
- // A block with no options to paint when CertREV was not reached is reported too (break-it B5).
626
- if (verdict.unreachable) return withReport(none, 'delivery_unreachable', props, server)
627
- return verdict.reason === 'unknown_key' ? withReport(none, 'unknown_key', props, server) : none
641
+ return reason
642
+ ? withReport(none, reason, props, server, undefined, verdict.unreachable ? verdict.status : undefined)
643
+ : none
628
644
  }
629
645
 
646
+ /** 2.0.1: the kernel's suppress reasons that mean the envelope itself did not verify (reported, never silent). */
647
+ const ENVELOPE_VERIFY_FAILURES: ReadonlySet<string> = new Set([
648
+ 'invalid_signature',
649
+ 'unsupported_alg',
650
+ 'platform_mismatch',
651
+ 'content_drift',
652
+ 'unsupported_contract_version',
653
+ ])
654
+
630
655
  /**
631
656
  * Paint what the page's record for this placement says: a seed its envelope (judged at the browser's clock first when
632
- * `judge`), an `'options'` record the options face, a `'none'` record nothing.
657
+ * `judge`), a `'none'` record nothing, and (2.0.1) a 2.0.0 server's `'options'` record nothing too.
633
658
  */
634
659
  function paintRecord(
635
660
  record: PageRecord,
@@ -652,7 +677,7 @@ function paintRecord(
652
677
  if (binding !== null) {
653
678
  const outcome = contractOutcome(binding, seed.card, seed.payload, props)
654
679
  if (outcome.kind === 'card')
655
- return <ContractCardFace card={outcome.card} names={outcome.names} seed={record.raw} />
680
+ return <ContractCardFace card={outcome.card} names={outcome.names} seed={record.raw} origin={props.baseUrl} />
656
681
  // Hydrating over the card the server printed, which the browser could not confirm (no WebCrypto): adopt the
657
682
  // server's element; the render right after hydration prints nothing in its place.
658
683
  if (!judge && outcome.reason === 'card_verify_failed' && seed.card !== undefined)
@@ -671,38 +696,21 @@ function paintRecord(
671
696
  if (seed.face && !judge) return <ServerFaceUntilHydrated />
672
697
  return paintEnvelope(seed.payload, seed.renderDef, props, record.raw)
673
698
  }
674
- const html = record.fallback.face === 'options' ? fallbackFace(props) : null
675
- if (html !== null) {
676
- return withReport(
677
- <OptionsFace block={props.block} html={html} record={record.raw} />,
678
- 'delivery_unreachable',
679
- props,
680
- false,
681
- )
682
- }
683
699
  const none = <NoFace record={record.raw} />
700
+ // 2.0.1: an `'options'` record (a page a 2.0.0 server rendered while CertREV was not reached) prints nothing too.
701
+ if (record.fallback.face === 'options') return withReport(none, 'delivery_unreachable', props, false)
684
702
  // 2.0.0: a contract-1 placement's record names why it printed nothing; the browser reports it too.
685
703
  const reason = record.fallback.reason
686
704
  return isFallbackReason(reason) ? withReport(none, reason, props, false) : none
687
705
  }
688
706
 
689
- const FALLBACK_REASONS: readonly string[] = [
690
- 'card_missing',
691
- 'card_verify_failed',
692
- 'card_unsupported_contract',
693
- 'card_pin_mismatch',
694
- 'unknown_key',
695
- 'delivery_unreachable',
696
- ]
697
-
698
707
  function isFallbackReason(value: unknown): value is CardFallbackReason {
699
- return typeof value === 'string' && FALLBACK_REASONS.includes(value)
708
+ return typeof value === 'string' && (CARD_FALLBACK_REASONS as readonly string[]).includes(value)
700
709
  }
701
710
 
702
- /** What a card the client mounts with no record and no `clientRead` paints: the options face, or nothing. */
703
- function clientMountFace(props: EnvelopeCardProps): React.ReactElement | null {
704
- const html = fallbackFace(props)
705
- return html === null ? null : <OptionsFace block={props.block} html={html} />
711
+ /** 2.0.1: a card the client mounts with no record and `clientRead` off prints nothing (2.0.0: the options face). */
712
+ function clientMountFace(props: EnvelopeCardProps): React.ReactElement {
713
+ return withReport(null, 'client_read_off', props, false)
706
714
  }
707
715
 
708
716
  const pendingRecord = () => 'pending' as const
@@ -729,6 +737,27 @@ function StreamedRecordCard(props: EnvelopeCardProps) {
729
737
  return paintRecord(record, props, context, true)
730
738
  }
731
739
 
740
+ /**
741
+ * 2.0.2 (final review R2-1): a card whose server render skipped its read. While hydrating, its marker as the server
742
+ * printed it. After, the record the page holds for its placement (the seed the card that did the read printed), also
743
+ * when that record streams in after this card hydrated; nothing while it has not arrived, and nothing if the page loads
744
+ * without one. It reads nothing.
745
+ */
746
+ function SkippedThenRecord(props: EnvelopeCardProps & { readonly instance: string; readonly hydrating: boolean }) {
747
+ const rt = cardDeliveryRuntime()
748
+ // In render, not in an effect, as `StreamedRecordCard` does.
749
+ watchStreamedRecords()
750
+ const record = React.useSyncExternalStore(
751
+ subscribeStreamedRecords,
752
+ () => streamedRecordFor(props.baseUrl, props.externalId, props.design),
753
+ pendingRecord,
754
+ )
755
+ if (props.hydrating) return <SkippedRead instance={props.instance} />
756
+ if (record === 'pending' || record === 'done') return null
757
+ const context = { platform: 'builder', externalId: props.externalId, now: new Date(rt.now()) }
758
+ return paintRecord(record, props, context, true)
759
+ }
760
+
732
761
  const subscribeNever = () => () => {}
733
762
 
734
763
  /**
@@ -752,27 +781,28 @@ function useHydrating(): boolean {
752
781
  * storefront whose CSP `connect-src` does not name CertREV (e.l.f.'s live policy) never sees a request or a violation
753
782
  * from it, and a page never loses its card for want of one.
754
783
  *
755
- * - Server: read (up to `CARD_DELIVERY_TIMEOUT_MS`, a fresh or stale-while-revalidating cache first), verify and paint
784
+ * - Server: read (2.0.2: waiting at most `CARD_SERVER_WAIT_MS`, never behind another request's read; a fresh or stale-while-revalidating cache, or a remembered failure, first), verify and paint
756
785
  * as through 1.1.1, and write a record of what it painted (`ssr-seed.ts`):
757
786
  * - a verified envelope: its face and the envelope seed (less the fields the def hides, and with no signature if
758
787
  * it hid any);
759
- * - CertREV not reached (a timeout, a network error, a 5xx, a body that is not JSON): the block's own options face,
760
- * as 0.5.x painted it, and an `'options'` record (a pinned card too: the record names its pin);
788
+ * - CertREV not reached (a timeout, a network error, a 5xx, a body that is not JSON) with no verified answer inside
789
+ * the stale window: nothing, and a `'none'` record naming `delivery_unreachable` (2.0.1; through 2.0.0 the
790
+ * block's own options face and an `'options'` record);
761
791
  * - a definitive no (revoked, expired, unverifiable, for another entry, not found): nothing, and a `'none'` record
762
792
  * on an empty hidden element.
763
793
  * - Browser, the page carries this placement's record: paint what it says. A seed paints its envelope; while
764
794
  * hydrating the server's verdict stands (the tree is the server's, byte for byte), and once hydrated the lifecycle
765
- * and subject are judged at the browser's clock, so a card that expired since the server render is taken down. An
766
- * `'options'` record paints the options face, a `'none'` record nothing. This holds when React renders the card
795
+ * and subject are judged at the browser's clock, so a card that expired since the server render is taken down. A
796
+ * `'none'` record, and (2.0.1) a 2.0.0 server's `'options'` record, print nothing. This holds when React renders the card
767
797
  * again on the client after a hydration error elsewhere on the page: the record is still in the document (or in
768
798
  * the page's memory of it), so the browser repaints the server's answer and reads nothing.
769
799
  * - Browser, hydrating with no record: server HTML from before 1.2.0 painted nothing here, so nothing.
770
800
  * - Browser, a card mounted on the client with no record (a client-side navigation to an article this page never
771
- * server-rendered, Builder's editor): the block's options face, pinned or not. With `clientRead`, instead, the
772
- * one read, settled exactly as on the server.
801
+ * server-rendered, Builder's editor): with `clientRead` (the default), the one read, settled exactly as on the
802
+ * server; with `clientRead={false}`, nothing (`client_read_off`; 2.0.0: the block's options face).
773
803
  * - Browser, the same while the document is still loading (1.2.1; React rendered the card on the client before the
774
- * server's streamed record arrived, `ssr-seed.ts`): nothing until the record arrives, then what it says; the
775
- * options face, or nothing, if the page loads without one (`StreamedRecordCard`). `clientRead` reads as above.
804
+ * server's streamed record arrived, `ssr-seed.ts`): nothing until the record arrives, then what it says; if the page
805
+ * loads without one, what a client mount does (`StreamedRecordCard`). `clientRead` reads as above.
776
806
  *
777
807
  * WHICH OF THE TWO FIRST CASES THIS RENDER IS (server or browser) is decided from what the render can observe, with
778
808
  * no switch a test could set. A server render and a hydration are both "hydrating" to React and both see a `document`
@@ -792,6 +822,8 @@ function useHydrating(): boolean {
792
822
  function EnvelopeCertReviewCard(props: EnvelopeCardProps) {
793
823
  const rt = cardDeliveryRuntime()
794
824
  const hydrating = useHydrating()
825
+ // 2.0.2: this card instance's id, the same on the server and while hydrating (`SkippedRead`).
826
+ const instance = React.useId()
795
827
  // Latched at mount: this instance hydrated server HTML (as opposed to a client mount).
796
828
  const [hydratedMount] = React.useState(hydrating)
797
829
  // The clock is read per render, so the lifecycle is judged NOW, not when the envelope was fetched.
@@ -810,13 +842,16 @@ function EnvelopeCertReviewCard(props: EnvelopeCardProps) {
810
842
  const [awaitsStream] = React.useState(
811
843
  () => inBrowser && !hydrating && fromPage === undefined && document.readyState === 'loading',
812
844
  )
845
+ // 2.0.2, latched at mount: this instance is hydrating a server render that skipped its read (`SkippedRead`).
846
+ const [skippedMount] = React.useState(() => inBrowser && hydrating && skippedInDocument(instance))
847
+ if (skippedMount) return <SkippedThenRecord {...props} instance={instance} hydrating={hydrating} />
813
848
  if (awaitsStream) return <StreamedRecordCard {...props} />
814
849
  if (inBrowser) {
815
850
  if (fromPage) return paintRecord(fromPage, props, context, !hydrating)
816
851
  if (hydratedMount) return null
817
852
  if (!props.clientRead) return clientMountFace(props)
818
853
  }
819
- return readAndPaint(props, context, inBrowser)
854
+ return readAndPaint(props, context, inBrowser, instance)
820
855
  }
821
856
 
822
857
  /**
@@ -827,6 +862,7 @@ function readAndPaint(
827
862
  props: EnvelopeCardProps,
828
863
  context: Parameters<typeof renderVerdict>[1],
829
864
  inBrowser: boolean,
865
+ instance = '',
830
866
  ): React.ReactElement | null {
831
867
  const rt = cardDeliveryRuntime()
832
868
  const { design } = props
@@ -844,11 +880,20 @@ function readAndPaint(
844
880
  const key = design
845
881
  ? `${props.baseUrl}|${props.externalId}|design=${design.version}|${design.brand ?? ''}`
846
882
  : `${props.baseUrl}|${props.externalId}`
847
- const settled = readVerifiedDelivery(read) ?? suspendOn(key, () => getVerifiedDelivery(read))
883
+ const settled =
884
+ readVerifiedDelivery(read) ??
885
+ (inBrowser
886
+ ? suspendOn(rt.cache, key, () => getVerifiedDelivery(read))
887
+ : suspendOnServer(key, read, rt.serverWaitMs))
888
+ if (settled === 'skipped') return <SkippedRead instance={instance} />
848
889
  if (settled.verdict.decision !== 'render') {
849
890
  return settleWithoutEnvelope(
850
891
  props,
851
- { unreachable: settled.unreachable === true, reason: settled.verdict.reason },
892
+ {
893
+ unreachable: settled.unreachable === true,
894
+ reason: settled.verdict.reason,
895
+ ...(settled.status === undefined ? {} : { status: settled.status }),
896
+ },
852
897
  !inBrowser,
853
898
  )
854
899
  }
@@ -872,6 +917,7 @@ function readAndPaint(
872
917
  card={outcome.card}
873
918
  names={outcome.names}
874
919
  seed={serializeSeed({ ...seed, card: outcome.card as unknown as Readonly<Record<string, unknown>> })}
920
+ origin={props.baseUrl}
875
921
  />
876
922
  )
877
923
  }
@@ -897,116 +943,40 @@ function readAndPaint(
897
943
  }
898
944
 
899
945
  export function CertReviewCard(props: CertReviewCardProps) {
900
- // 1.1.0: a block that carries the delivery marker paints from the signed envelope and the
901
- // brand's def, never from the options beside it. No marker (every entry exported before 1.1.0)
902
- // takes the options path below, unchanged from 1.0.3.
903
- // 1.2.0-canary.5: a design pin (CertREV's canary replica only) fails closed. One that cannot be read renders
904
- // nothing, and so does one on a block without the delivery marker: the options face is never the pinned design.
905
- // 1.2.0: where the envelope cannot be had (CertREV not reached, or no server render and no `clientRead`), a block
906
- // with the marker paints this options face too, pinned or not: see `EnvelopeCertReviewCard`.
946
+ // 1.1.0: a block that carries the delivery marker paints from the signed envelope and the brand's def, never from
947
+ // the options beside it. 2.0.1: a block WITHOUT the marker (an entry exported before 1.1.0, a block dropped in by
948
+ // hand) prints nothing at all and reports `no_marker`; through 2.0.0 it painted its export-time options (a reviewer
949
+ // name, a credential, a memo, a verify link nobody had signed). A marker this client does not accept, or a block
950
+ // with no Builder entry to read for, prints nothing and reports `marker_invalid`.
951
+ // 1.2.0-canary.5: a design pin (CertREV's canary replica only) that cannot be read renders nothing.
907
952
  const design = parseDesignPin(props.builderContext?.context?.certrevDesignPin)
908
953
  if (design === 'invalid') return null
909
- if (props.delivery != null) {
910
- const marker = acceptedDeliveryMarker(props.delivery)
911
- const entryId = text(props.builderContext?.content?.id)
912
- if (!marker || !entryId) return null
913
- return (
914
- <Suspense fallback={null}>
915
- <EnvelopeCertReviewCard
916
- baseUrl={marker.baseUrl}
917
- externalId={entryId}
918
- mode={props.mode}
919
- part={props.part}
920
- design={design}
921
- block={props}
922
- clientRead={props.clientRead !== false}
923
- />
924
- </Suspense>
925
- )
926
- }
927
- if (design) return null
928
-
929
- const html = optionsFaceHtml(props, 'banner')
930
- if (html === null) return null // no chrome pre-cert (or an unreadable delivery)
931
- return <OptionsFace block={props} html={html} />
954
+ const entryId = text(props.builderContext?.content?.id)
955
+ if (props.delivery == null) return <BlockReport reason="no_marker" externalId={entryId} />
956
+ const marker = acceptedDeliveryMarker(props.delivery)
957
+ if (!marker || !entryId) return <BlockReport reason="marker_invalid" externalId={entryId} />
958
+ return (
959
+ <Suspense fallback={null}>
960
+ <EnvelopeCertReviewCard
961
+ baseUrl={marker.baseUrl}
962
+ externalId={entryId}
963
+ mode={props.mode}
964
+ part={props.part}
965
+ design={design}
966
+ block={props}
967
+ clientRead={props.clientRead !== false}
968
+ />
969
+ </Suspense>
970
+ )
932
971
  }
933
972
 
934
- /**
935
- * The options face's HTML (1.0.3's, unchanged), or null when the block lacks a reviewer or a certificate link. `bridge`
936
- * is the placement a block with no `mode` gets: `'banner'` for a block without the delivery marker (the bridge below);
937
- * the def's layout for a marked block's fallback (`fallbackFace`).
938
- */
939
- function optionsFaceHtml(props: CertReviewCardProps, bridge: CertBlockMode | undefined): string | null {
940
- const reviewerName = text(props.reviewerName)
941
- const verifyUrl = text(props.verifyUrl)
942
- if (!reviewerName || !verifyUrl) return null
943
-
944
- // Name-only face when the pair is unreadable, or when the brand set showCredentials:false.
945
- const showCred = props.display?.showCredentials !== false
946
- const withCred = showCred ? fusedCredential(props.credentialVerification) : null
947
-
948
- const tokens = props.renderDef?.tokens
949
- const theme: Partial<ResolvedBlockTheme> | undefined = tokens
950
- ? {
951
- accentColor: optionalText(tokens.accentColor),
952
- surface: optionalText(tokens.surface),
953
- cornerRadius: optionalText(tokens.cornerRadius),
954
- fontSlot: optionalText(tokens.fontSlot) as ResolvedBlockTheme['fontSlot'] | undefined,
955
- // v2 brand-ink (post-demo): the bar + free-content ink follow the def.
956
- barInk: optionalText(tokens.barInk),
957
- inkColor: optionalText(tokens.inkColor),
958
- }
959
- : undefined
960
-
961
- return renderCertBlock({
962
- // COMPATIBILITY BRIDGE, NOT THE DESIGN. Delete this fallback once the exporter emits an
963
- // explicit `mode` and existing entries have been backfilled; until then it must stay.
964
- //
965
- // `renderCertBlock` owns the ONE placement default (absent or unrecognised ⇒ sidebar) and
966
- // that decision STANDS: it is what a direct caller gets, and `sidebar` is the registered
967
- // input's `defaultValue`, so every block created in the editor from now on carries an
968
- // explicit `mode` and never reaches this line.
969
- //
970
- // The problem is the entries that already exist. `mode` is a PLACEMENT input, not one of
971
- // the 20 wire keys (`BUILDER_CERT_CHROME_KEYS`), and the exporter emits only the wire
972
- // projection (`options: { ...certData }`), so every entry exported to date has NO `mode`
973
- // key at all. Through 0.5.5 this line read `?? 'banner'` and those entries rendered the
974
- // banner face. Removing it in 1.0.0 did not give them the renderer's default in the
975
- // abstract; it silently repainted live pages from banner to sidebar, with no entry edit
976
- // and no version pin to protect them, because their install line is unpinned.
977
- //
978
- // So this is a bridge for pre-existing entries, NOT a reversal of the sidebar default.
979
- // The three-copies-of-a-default problem that motivated removing it is still real and is
980
- // still fixed: the helper text and the wire type's doc comment no longer name a default,
981
- // and this line names one only for input the renderer can no longer distinguish from a
982
- // deliberate choice. The exporter writing `mode` is what retires it.
983
- mode: props.mode ?? bridge ?? 'banner',
984
- theme,
985
- // First-class banner memo split — renderCertBlock composes the requested
986
- // part directly; no string-splitting. Ignored outside banner mode, and an unrecognised
987
- // value is normalised to 'full' there, same as `mode` — not here.
988
- ...(props.part ? { part: props.part } : {}),
989
- facts: {
990
- authorName: optionalText(props.authorName) ?? 'Editorial',
991
- authorTitle: optionalText(props.authorTitle),
992
- reviewerName,
993
- credential: withCred?.credential ?? '',
994
- credentialVerifiedAt: withCred?.verifiedAt ?? '',
995
- certifiedAt: text(props.certifiedAt),
996
- memo: text(props.memo),
997
- bio: optionalText(props.bio),
998
- profileUrl: text(props.reviewerProfileUrl),
999
- certificateUrl: verifyUrl,
1000
- // The compliance disclosures are CONSTANTS the renderer owns — a delivered cue is
1001
- // IGNORED (an editor cannot retype the FTC copy into something friendlier), and the
1002
- // scope line is not forwarded at all. The ONE thing honored is an explicit `null`:
1003
- // the pro-bono reviewer has no compensation to disclose, so the cue is omitted
1004
- // rather than replaced by the compensated constant (the modal already gets this
1005
- // right — a card claiming "Compensated expert" over a volunteer is an FTC-facing
1006
- // falsehood, not a cosmetic default).
1007
- compensationCue: props.compensationCue === null ? null : undefined,
1008
- },
1009
- })
973
+ /** 2.0.1: a block with nothing to read reports why, to CertREV's production origin (it has no delivery origin). */
974
+ function BlockReport(props: { readonly reason: CardFallbackReason; readonly externalId: string }) {
975
+ const detail = React.useMemo<CardFallbackDetail>(
976
+ () => ({ reason: props.reason, platform: 'builder', externalId: props.externalId }),
977
+ [props.reason, props.externalId],
978
+ )
979
+ return <FallbackReport detail={detail} server={typeof document === 'undefined'} />
1010
980
  }
1011
981
 
1012
982
  // ─────────────────────────────────────────────────────────────────────────────