@certrev/cert-block
v0.1.3
Published
Headless / crypto_verify render edge for the CertREV CertDeliveryEnvelope. SSR-safe React components (<CertBadge>/<ExpertBio>/<CertRevBacklink>), a deterministic schema.org JSON-LD projector, a fail-closed verify layer over the shared VerdictKernel, and a
Maintainers
Readme
@certrev/cert-block
The headless / crypto_verify render edge for the CertREV CertDeliveryEnvelope. Sign once in the portal, render everywhere: SSR-safe React components, a deterministic schema.org JSON-LD projector, a fail-closed verify layer over the shared VerdictKernel, and a framework-agnostic <certrev-badge> Web Component.
This is the SDK the brand SSRs into a Hydrogen loader, a Next server component, a Builder client component, or a universal server-side include. It is the headless_react + universal_embed surface classes from the cross-platform delivery design.
What this is
CertREV's durable asset is a portable, forgery-proof "this content was reviewed by a credentialed expert" credential — a single Ed25519-signed CertDeliveryEnvelope. Each delivery surface renders the visible UI + JSON-LD from that envelope and enforces its validity. The WordPress plugin does it in PHP; the Shopify Liquid extension does it in Liquid; this package does it in TS/JS for any React or vanilla-JS surface that can run Node/WebCrypto.
The package gives a brand four things:
- React components —
<CertBadge>,<ExpertBio>,<CertRevBacklink>,<CertJsonLd>, and the<CertReview>composite. SSR-safe (render-pure; nouseState/useEffect/browser globals), theme-light, accessible, and they escape every field. Presentation is driven by the signedcontent.displayconfig (badgeStyle,accentColor,showExpertPhoto,showMemo,showAuthor). - A deterministic JSON-LD projector —
projectCertJsonLd(facts)→ a schema.orgArticle+reviewedBy(Person)+Review+Organization@graph, designed to merge by@idinto the host page's existing Article graph (won't collide with Yoast / the theme's structured data). The JSON-LD is projected from the facts at render time, never stored in the envelope. - A thin verify layer —
getVerifiedEnvelope(source)fetches the signed envelope from either a Shopify metafield OR the Delivery API (GET /api/cert/v1/delivery/{platform}/{externalId}), runs the fail-closedVerdictKernel, and caches the verdict (TTL + single-flight) so concurrent SSR renders don't stampede the origin. - A Web Component —
<certrev-badge>, theuniversal_embedsurface for sites with no native integration, wrapping the same render as<CertBadge>via a shared string renderer.
The contract shape (settled)
A CertDeliveryEnvelope = { payload, signature }. The detached signature is Ed25519 over RFC-8785 JCS(payload).
payload = {
contractVersion: 1,
certId,
subject: { platform, externalId, logicalArticleId, canonicalUrls[], installationId, contentDigest },
content: { expert{displayName,credentials[],profileUrl,photoUrl}, author{name,title},
memo, certifiedAt, contentModifiedAt, verifyUrl,
display{accentColor,showExpertPhoto,showAuthor,showMemo,badgeStyle} },
lifecycle: { issuedAt, expiresAt, revokedAt|null, revision },
}content is structured FACTS, not rendered JSON-LD — the JSON-LD is projected from these facts at render, never stored. The VerdictKernel (shared) verifies the signature, then checks subject (platform/externalId), lifecycle (revokedAt/expiresAt), and content drift (contentDigest vs the live hash), failing closed at every step.
Public API
// React components (SSR-safe)
import { CertBadge, ExpertBio, CertRevBacklink, CertJsonLd, CertReview } from '@certrev/cert-block'
// JSON-LD projector
import { projectCertJsonLd, projectCertJsonLdString, serializeJsonLdForScript } from '@certrev/cert-block'
// Verify layer
import { getVerifiedEnvelope, invalidateVerdict, sharedVerdictCache, TtlCache } from '@certrev/cert-block'
import { staticKidResolver, fetchingKidResolver } from '@certrev/cert-block'
// Contract surface (re-exported from the binding point; → @certrev/cert-contract on publish)
import type { CertDeliveryEnvelope, CertPayload, CertVerdict, RenderContext } from '@certrev/cert-block'
import { verifyEnvelope, renderVerdict, verifySignatureOnly } from '@certrev/cert-block'
// Web Component (separate subpath — does NOT touch customElements on import)
import { defineCertRevBadge, setCertRevKidResolver } from '@certrev/cert-block/webcomponent'Usage — headless React (Hydrogen / Next server component)
Fetch + verify in the server runtime (it can run Ed25519 over JCS — full crypto_verify parity), then render the badge + JSON-LD only on a render verdict.
import { getVerifiedEnvelope, staticKidResolver, CertReview } from '@certrev/cert-block'
const resolveKid = staticKidResolver({ 'certrev-2026-1': process.env.CERTREV_PUBKEY_PEM! })
// In a Hydrogen loader / Next server component:
const verdict = await getVerifiedEnvelope({
// PULL from the Delivery API (or { kind: 'metafield', value } for a Shopify metafield)
source: { kind: 'delivery_api', baseUrl: 'https://portal.certrev.com', platform: 'shopify', externalId: articleGid },
resolveKid,
context: { platform: 'shopify', externalId: articleGid, liveContentHash },
})
// <CertReview> is fail-closed: renders the badge + projected JSON-LD on `render`, NOTHING on `suppress`.
return <CertReview verdict={verdict} pageUrl={canonicalUrl} />For finer control, render the pieces independently from verdict.payload:
{verdict.decision === 'render' && (
<>
<CertBadge payload={verdict.payload} badgeStyle="compact" />
<ExpertBio payload={verdict.payload} headingLevel="h2" />
<CertRevBacklink payload={verdict.payload} />
<CertJsonLd payload={verdict.payload} pageUrl={canonicalUrl} />
</>
)}Usage — JSON-LD merge (don't collide with Yoast)
The Article node carries the SAME @id the host page's primary Article uses ({pageUrl}#article, query + fragment stripped), so a consumer merges our reviewedBy / dateModified into the existing node instead of emitting a competing primary entity. Pass wrapGraph: false to get a bare node array to splice into a @graph you already own.
const graph = projectCertJsonLd(payload, { pageUrl: canonicalUrl }) // { '@context', '@graph' }
const nodes = projectCertJsonLd(payload, { pageUrl: canonicalUrl, wrapGraph: false }) // bare node[]Usage — universal embed (Web Component)
Preferred mode is SSR + hydrate (crawlable): a server-side include emits the badge HTML (via renderBadgeHtml) inside the element; the component leaves the server-rendered child alone. Client-fetch mode (not crawlable) is a documented fallback and is fail-closed — it renders nothing without a configured key resolver.
<!-- SSR: server emits the badge markup inside the tag; crawler reads it -->
<certrev-badge>{{ renderBadgeHtml(payload) }}</certrev-badge>
<script type="module">
import { defineCertRevBadge, setCertRevKidResolver } from '@certrev/cert-block/webcomponent'
setCertRevKidResolver(myResolver) // only needed for the client-fetch fallback
defineCertRevBadge()
</script>Fail-closed, everywhere
The package never renders an unverified credential. Every error path — bad signature, unknown kid, wrong post, revoked, expired, content drift, network failure, malformed JSON, a throwing resolver — collapses to a suppress verdict (or a null render), never an exception that a caller could swallow into a render. <CertReview> and <CertRevBacklink> also fail closed at the component boundary (suppress / unsafe URL → render nothing). URLs pass through safeHttpUrl (drops javascript:/data:); accent colors through safeCssColor; the JSON-LD body neutralizes </> so a hostile memo can't break out of the <script> tag.
Server-safe guarantee
- The main entry never touches
customElements/window— the Web Component ships from the./webcomponentsubpath so importing@certrev/cert-blockin an RSC/Node loader is side-effect-free. Registration only happens when you calldefineCertRevBadge()in a browser. - The React components are render-pure (verified by
react-dom/serverSSR tests) so they work as Server Components and as client components that SSR identically — same crawlable output either way. - Date formatting is deterministic UTC (fixed English month abbreviations, not
toLocaleDateString) so SSR output is byte-stable and never causes a hydration mismatch.
Integration TODOs (wire the real @certrev/cert-contract)
The shared contract types + the VerdictKernel live in the published @certrev/cert-contract package, which is not yet on a registry this workspace installs from. Until it publishes, the SDK binds the contract surface through a local stub so everything builds + tests now. To cut over on publish:
- Delete
src/contract/kernel-contract.ts(the type mirror) andsrc/contract/kernel-stub.ts(the faithful kernel impl). - In
src/contract/kernel.ts(the single binding point), replace both re-export blocks with one line:export * from '@certrev/cert-contract'. The exported names are deliberately identical, so nothing else in the SDK changes. - In
package.json, flip the@certrev/cert-contractpeer dependency back tooptional: falseand remove the root.npmrcauto-install-peers=falseline (it exists only so the workspace installs against the stub). - Point
src/contract/fixtures.tsat cert-contract's realcanonicalPayloadBytes(RFC 8785) so the test fixtures sign bytes the published kernel verifies. (Today the stub + fixtures share a minimal JCS sufficient for the all-string/number/bool envelope shape.) - Confirm
verifyEnvelope/renderVerdict/verifySignatureOnly/toEd25519PublicKeyresolve from the published package, then re-runpnpm typecheck && pnpm test && pnpm build.
A WebCrypto verify path for the Web Component's client-fetch fallback lives in cert-contract; once published, wire it into certrev-badge.ts so the universal embed can verify in the browser without a pre-supplied resolver.
Current consumers
- None yet. Target consumers: brand-owned Hydrogen / Next / Remix storefronts (
headless_react), Builder.io spaces (headless_visual_cms), and any site dropping in<certrev-badge>(universal_embed). Add each here as it adopts the SDK.
Tests
pnpm test (Vitest). Coverage:
__tests__/verify.test.ts— the kernel via the SDK binding with real Ed25519 over JCS (freshly-signed fixture envelopes, no mocked crypto): render, tamper →invalid_signature, unknown kid, platform/subject mismatch, revoked, expired, content drift;getVerifiedEnvelopeover metafield (object + JSON-string) and Delivery API sources, 404/410 fail-closed, and the single-flight thundering-herd guard (N concurrent renders → one fetch).__tests__/project.test.ts— the JSON-LD projector:@graphshape, Article@idhost-merge alignment (query + fragment stripped), expert-as-reviewedBy, credentials →hasCredential+honorificSuffix, CertREV-namespaced node@ids, determinism, and</script>/ HTML-comment neutralization.__tests__/components.test.tsx— every React component rendered throughreact-dom/serverwith mock facts: structure, accessibility (aria-label, heading levels), accent theming, display-flag honoring, hostile-input escaping,javascript:-URL dropping, and fail-closed (suppressverdict / unsafe URL → nothing).__tests__/webcomponent.test.tsx— the sharedrenderBadgeHtmlstring 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).
Facts are mocked via makeMockPayload / makeSignedEnvelope (src/contract/fixtures.ts).
Version
0.1.0 — initial scaffold against the settled contract shape, bound to the local contract stub. Publishes publicly to npm as @certrev/cert-block (publishConfig.access: public).
