@certrev/cert-block 0.3.0 → 0.5.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/README.md +48 -1
- package/dist/builder/cert-chrome-data.d.ts +129 -0
- package/dist/builder/cert-chrome-data.d.ts.map +1 -0
- package/dist/builder/cert-chrome-data.js +59 -0
- package/dist/builder/cert-chrome-data.js.map +1 -0
- package/dist/builder/cert-review-card.d.ts +68 -0
- package/dist/builder/cert-review-card.d.ts.map +1 -0
- package/dist/builder/cert-review-card.js +129 -0
- package/dist/builder/cert-review-card.js.map +1 -0
- package/dist/builder/index.d.ts +2 -0
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +3 -0
- package/dist/builder/index.js.map +1 -1
- package/dist/components/preview-block.d.ts +43 -0
- package/dist/components/preview-block.d.ts.map +1 -0
- package/dist/components/preview-block.js +53 -0
- package/dist/components/preview-block.js.map +1 -0
- package/dist/components/render-cert-block.d.ts +50 -7
- package/dist/components/render-cert-block.d.ts.map +1 -1
- package/dist/components/render-cert-block.js +304 -80
- package/dist/components/render-cert-block.js.map +1 -1
- package/dist/components/render-def.d.ts +27 -0
- package/dist/components/render-def.d.ts.map +1 -1
- package/dist/components/render-def.js +38 -0
- package/dist/components/render-def.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/modal/avatar-resize.d.ts +60 -0
- package/dist/modal/avatar-resize.d.ts.map +1 -0
- package/dist/modal/avatar-resize.js +91 -0
- package/dist/modal/avatar-resize.js.map +1 -0
- package/dist/modal/cert-modal-view.d.ts +74 -0
- package/dist/modal/cert-modal-view.d.ts.map +1 -0
- package/dist/modal/cert-modal-view.js +160 -0
- package/dist/modal/cert-modal-view.js.map +1 -0
- package/dist/modal/certrev-cert-modal.d.ts +72 -0
- package/dist/modal/certrev-cert-modal.d.ts.map +1 -0
- package/dist/modal/certrev-cert-modal.js +270 -0
- package/dist/modal/certrev-cert-modal.js.map +1 -0
- package/dist/modal/certrev-cert-modal.styles.d.ts +35 -0
- package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -0
- package/dist/modal/certrev-cert-modal.styles.js +488 -0
- package/dist/modal/certrev-cert-modal.styles.js.map +1 -0
- package/dist/modal/certrev-cert.entry.d.ts +19 -0
- package/dist/modal/certrev-cert.entry.d.ts.map +1 -0
- package/dist/modal/certrev-cert.entry.js +39 -0
- package/dist/modal/certrev-cert.entry.js.map +1 -0
- package/dist/modal/certrev-cert.js +557 -0
- package/dist/modal/display-strings.d.ts +22 -0
- package/dist/modal/display-strings.d.ts.map +1 -0
- package/dist/modal/display-strings.js +35 -0
- package/dist/modal/display-strings.js.map +1 -0
- package/dist/modal/escape.d.ts +13 -0
- package/dist/modal/escape.d.ts.map +1 -0
- package/dist/modal/escape.js +20 -0
- package/dist/modal/escape.js.map +1 -0
- package/dist/modal/font-face.d.ts +4 -0
- package/dist/modal/font-face.d.ts.map +1 -0
- package/dist/modal/font-face.js +137 -0
- package/dist/modal/font-face.js.map +1 -0
- package/dist/modal/ftc-disclosure.d.ts +12 -0
- package/dist/modal/ftc-disclosure.d.ts.map +1 -0
- package/dist/modal/ftc-disclosure.js +12 -0
- package/dist/modal/ftc-disclosure.js.map +1 -0
- package/dist/modal/ftc-guard.d.ts +50 -0
- package/dist/modal/ftc-guard.d.ts.map +1 -0
- package/dist/modal/ftc-guard.js +117 -0
- package/dist/modal/ftc-guard.js.map +1 -0
- package/dist/modal/index.d.ts +20 -0
- package/dist/modal/index.d.ts.map +1 -0
- package/dist/modal/index.js +25 -0
- package/dist/modal/index.js.map +1 -0
- package/dist/modal/interactions.d.ts +53 -0
- package/dist/modal/interactions.d.ts.map +1 -0
- package/dist/modal/interactions.js +147 -0
- package/dist/modal/interactions.js.map +1 -0
- package/dist/modal/logo-paths.d.ts +15 -0
- package/dist/modal/logo-paths.d.ts.map +1 -0
- package/dist/modal/logo-paths.js +24 -0
- package/dist/modal/logo-paths.js.map +1 -0
- package/dist/modal/placement.d.ts +95 -0
- package/dist/modal/placement.d.ts.map +1 -0
- package/dist/modal/placement.js +302 -0
- package/dist/modal/placement.js.map +1 -0
- package/dist/modal/revalidate.d.ts +48 -0
- package/dist/modal/revalidate.d.ts.map +1 -0
- package/dist/modal/revalidate.js +65 -0
- package/dist/modal/revalidate.js.map +1 -0
- package/dist/modal/verify-client.d.ts +24 -0
- package/dist/modal/verify-client.d.ts.map +1 -0
- package/dist/modal/verify-client.js +38 -0
- package/dist/modal/verify-client.js.map +1 -0
- package/package.json +14 -4
- package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +10 -6
- package/src/__tests__/preview-block.test.ts +84 -0
- package/src/__tests__/render-cert-block.test.ts +380 -5
- package/src/builder/__tests__/builder.test.tsx +71 -0
- package/src/builder/cert-chrome-data.ts +159 -0
- package/src/builder/cert-review-card.tsx +177 -0
- package/src/builder/index.tsx +18 -0
- package/src/components/preview-block.ts +74 -0
- package/src/components/render-cert-block.ts +387 -102
- package/src/components/render-def.ts +55 -0
- package/src/index.ts +9 -0
- package/src/modal/avatar-resize.ts +99 -0
- package/src/modal/cert-modal-view.test.ts +175 -0
- package/src/modal/cert-modal-view.ts +210 -0
- package/src/modal/certrev-cert-modal.styles.ts +494 -0
- package/src/modal/certrev-cert-modal.test.ts +125 -0
- package/src/modal/certrev-cert-modal.ts +273 -0
- package/src/modal/certrev-cert.entry.ts +44 -0
- package/src/modal/display-strings.ts +38 -0
- package/src/modal/escape.ts +20 -0
- package/src/modal/font-face.ts +138 -0
- package/src/modal/ftc-disclosure.ts +13 -0
- package/src/modal/ftc-guard.test.ts +153 -0
- package/src/modal/ftc-guard.ts +119 -0
- package/src/modal/index.ts +34 -0
- package/src/modal/interactions.test.ts +250 -0
- package/src/modal/interactions.ts +166 -0
- package/src/modal/logo-paths.ts +23 -0
- package/src/modal/placement.test.ts +398 -0
- package/src/modal/placement.ts +331 -0
- package/src/modal/revalidate.test.ts +111 -0
- package/src/modal/revalidate.ts +81 -0
- package/src/modal/verify-client.test.ts +164 -0
- package/src/modal/verify-client.ts +42 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shopify cert-badge Web-Component revalidation — the `crypto_verify` progressive-enhancement
|
|
3
|
+
* layer over the SSR Liquid badge.
|
|
4
|
+
*
|
|
5
|
+
* The Liquid edge renders the badge server-side (crawlable) and gates lifecycle in Liquid, but
|
|
6
|
+
* it CANNOT run Ed25519 (`platform_ownership`). This module upgrades the HUMAN-visible badge to
|
|
7
|
+
* a real crypto_verify check: on page load it re-fetches the Delivery artifact from the public
|
|
8
|
+
* Delivery API and runs CertREV's CANONICAL verdict kernel (`verifyArtifact` from
|
|
9
|
+
* `@certrev/cert-contract` — NOT a re-implementation), then:
|
|
10
|
+
*
|
|
11
|
+
* • verdict `render` → leave the SSR badge (verified live);
|
|
12
|
+
* • verdict `suppress` (invalid-sig / subject-mismatch / revoked / expired) → HIDE it
|
|
13
|
+
* (fail-closed), stamping `data-certrev-suppressed` with the verdict's reason. A revoked
|
|
14
|
+
* placement now serves the SLIM `CertTombstone` (POR-10481), which `verifyArtifact`
|
|
15
|
+
* resolves to `suppress:'revoked'` — so the badge hides AND the suppressed-reason telemetry
|
|
16
|
+
* is right (`verifyEnvelope` used to mis-report it as `unsupported_contract_version`;
|
|
17
|
+
* POR-10684);
|
|
18
|
+
* • the Delivery API 404s (no servable artifact) or the network errors → LEAVE the SSR badge.
|
|
19
|
+
* The SSR copy already passed the Liquid lifecycle gate against the app-owned metafield
|
|
20
|
+
* (the authoritative revoke/expire surface — the binding retracts it on revoke), so the WC
|
|
21
|
+
* is enhancement, never a NEW failure mode or a false-hide on an untracked placement.
|
|
22
|
+
*
|
|
23
|
+
* Crawlers + AI extractors read the SSR copy regardless (this never blocks first paint). The
|
|
24
|
+
* verdict source is injected so the unit test drives it without a network; the production entry
|
|
25
|
+
* (`certrev-cert.entry.ts`) wires the real fetch-+-`verifyArtifact` verifier + published key.
|
|
26
|
+
*/
|
|
27
|
+
import type { CertVerdict } from '@certrev/cert-contract';
|
|
28
|
+
/** Where + what to verify, derived from the `<certrev-badge>` host's routing attributes. */
|
|
29
|
+
export interface BadgeSource {
|
|
30
|
+
readonly baseUrl: string;
|
|
31
|
+
readonly platform: string;
|
|
32
|
+
readonly externalId: string;
|
|
33
|
+
}
|
|
34
|
+
/** Fetch-the-Delivery-API-+-verify, injected so tests drive it without a network. THROWS on a
|
|
35
|
+
* 404 / network error (→ keep SSR); returns a verdict on a served envelope. */
|
|
36
|
+
export type VerifyBadgeFn = (source: BadgeSource) => Promise<CertVerdict>;
|
|
37
|
+
export interface RevalidateDeps {
|
|
38
|
+
readonly verify: VerifyBadgeFn;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Re-verify one `<certrev-badge revalidate>` host and hide its inner `.certrev-badge` on a
|
|
42
|
+
* definitive `suppress` verdict. No-op (keep SSR) when the host lacks routing attributes or the
|
|
43
|
+
* verify call throws (404 / offline).
|
|
44
|
+
*/
|
|
45
|
+
export declare function revalidateBadge(host: Element, deps: RevalidateDeps): Promise<void>;
|
|
46
|
+
/** Revalidate every `<certrev-badge revalidate>` once the DOM is ready. */
|
|
47
|
+
export declare function initBadgeRevalidation(doc: Document, deps: RevalidateDeps): void;
|
|
48
|
+
//# sourceMappingURL=revalidate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revalidate.d.ts","sourceRoot":"","sources":["../../src/modal/revalidate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEzD,4FAA4F;AAC5F,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC3B;AAED;gFACgF;AAChF,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;AAEzE,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBxF;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAQ/E"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shopify cert-badge Web-Component revalidation — the `crypto_verify` progressive-enhancement
|
|
3
|
+
* layer over the SSR Liquid badge.
|
|
4
|
+
*
|
|
5
|
+
* The Liquid edge renders the badge server-side (crawlable) and gates lifecycle in Liquid, but
|
|
6
|
+
* it CANNOT run Ed25519 (`platform_ownership`). This module upgrades the HUMAN-visible badge to
|
|
7
|
+
* a real crypto_verify check: on page load it re-fetches the Delivery artifact from the public
|
|
8
|
+
* Delivery API and runs CertREV's CANONICAL verdict kernel (`verifyArtifact` from
|
|
9
|
+
* `@certrev/cert-contract` — NOT a re-implementation), then:
|
|
10
|
+
*
|
|
11
|
+
* • verdict `render` → leave the SSR badge (verified live);
|
|
12
|
+
* • verdict `suppress` (invalid-sig / subject-mismatch / revoked / expired) → HIDE it
|
|
13
|
+
* (fail-closed), stamping `data-certrev-suppressed` with the verdict's reason. A revoked
|
|
14
|
+
* placement now serves the SLIM `CertTombstone` (POR-10481), which `verifyArtifact`
|
|
15
|
+
* resolves to `suppress:'revoked'` — so the badge hides AND the suppressed-reason telemetry
|
|
16
|
+
* is right (`verifyEnvelope` used to mis-report it as `unsupported_contract_version`;
|
|
17
|
+
* POR-10684);
|
|
18
|
+
* • the Delivery API 404s (no servable artifact) or the network errors → LEAVE the SSR badge.
|
|
19
|
+
* The SSR copy already passed the Liquid lifecycle gate against the app-owned metafield
|
|
20
|
+
* (the authoritative revoke/expire surface — the binding retracts it on revoke), so the WC
|
|
21
|
+
* is enhancement, never a NEW failure mode or a false-hide on an untracked placement.
|
|
22
|
+
*
|
|
23
|
+
* Crawlers + AI extractors read the SSR copy regardless (this never blocks first paint). The
|
|
24
|
+
* verdict source is injected so the unit test drives it without a network; the production entry
|
|
25
|
+
* (`certrev-cert.entry.ts`) wires the real fetch-+-`verifyArtifact` verifier + published key.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Re-verify one `<certrev-badge revalidate>` host and hide its inner `.certrev-badge` on a
|
|
29
|
+
* definitive `suppress` verdict. No-op (keep SSR) when the host lacks routing attributes or the
|
|
30
|
+
* verify call throws (404 / offline).
|
|
31
|
+
*/
|
|
32
|
+
export async function revalidateBadge(host, deps) {
|
|
33
|
+
const badge = host.querySelector('.certrev-badge');
|
|
34
|
+
if (!badge)
|
|
35
|
+
return;
|
|
36
|
+
const baseUrl = host.getAttribute('data-delivery-api');
|
|
37
|
+
const externalId = host.getAttribute('data-external-id');
|
|
38
|
+
const platform = host.getAttribute('data-platform') ?? 'shopify';
|
|
39
|
+
if (!baseUrl || !externalId)
|
|
40
|
+
return; // no source → keep the lifecycle-gated SSR copy
|
|
41
|
+
let verdict;
|
|
42
|
+
try {
|
|
43
|
+
verdict = await deps.verify({ baseUrl, platform, externalId });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return; // 404 / offline → keep SSR (never a new failure mode or a false-hide)
|
|
47
|
+
}
|
|
48
|
+
if (verdict.decision !== 'render') {
|
|
49
|
+
badge.setAttribute('hidden', '');
|
|
50
|
+
badge.setAttribute('data-certrev-suppressed', verdict.reason);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Revalidate every `<certrev-badge revalidate>` once the DOM is ready. */
|
|
54
|
+
export function initBadgeRevalidation(doc, deps) {
|
|
55
|
+
const run = () => {
|
|
56
|
+
for (const host of Array.from(doc.querySelectorAll('certrev-badge[revalidate]'))) {
|
|
57
|
+
void revalidateBadge(host, deps);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
if (doc.readyState === 'loading')
|
|
61
|
+
doc.addEventListener('DOMContentLoaded', run, { once: true });
|
|
62
|
+
else
|
|
63
|
+
run();
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=revalidate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revalidate.js","sourceRoot":"","sources":["../../src/modal/revalidate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAmBH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAa,EAAE,IAAoB;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAA;IAClD,IAAI,CAAC,KAAK;QAAE,OAAM;IAElB,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAA;IACtD,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,CAAA;IACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,IAAI,SAAS,CAAA;IAChE,IAAI,CAAC,OAAO,IAAI,CAAC,UAAU;QAAE,OAAM,CAAC,gDAAgD;IAEpF,IAAI,OAAoB,CAAA;IACxB,IAAI,CAAC;QACJ,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAA;IAC/D,CAAC;IAAC,MAAM,CAAC;QACR,OAAM,CAAC,sEAAsE;IAC9E,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACnC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;QAChC,KAAK,CAAC,YAAY,CAAC,yBAAyB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9D,CAAC;AACF,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,qBAAqB,CAAC,GAAa,EAAE,IAAoB;IACxE,MAAM,GAAG,GAAG,GAAS,EAAE;QACtB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC,EAAE,CAAC;YAClF,KAAK,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACjC,CAAC;IACF,CAAC,CAAA;IACD,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS;QAAE,GAAG,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;;QAC1F,GAAG,EAAE,CAAA;AACX,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-side Delivery-API fetch + verify the Shopify Web Component uses. Thin glue around
|
|
3
|
+
* the CANONICAL kernel: it GETs the public, CDN-cacheable Delivery API
|
|
4
|
+
* (`{baseUrl}/api/cert/v1/delivery/{platform}/{externalId}`) and runs `verifyArtifact` from
|
|
5
|
+
* `@certrev/cert-contract` (WebCrypto Ed25519 over RFC-8785 JCS — the same kernel the server +
|
|
6
|
+
* every other edge run). No crypto is reimplemented here.
|
|
7
|
+
*
|
|
8
|
+
* `verifyArtifact` (POR-10684, cert-contract 0.3.0) dispatches on the `kind: 'tombstone'`
|
|
9
|
+
* discriminator: a full `CertDeliveryEnvelope` runs the whole crypto + lifecycle pipeline, while
|
|
10
|
+
* the SLIM `CertTombstone` the control plane now serves for a revoked placement (POR-10481) —
|
|
11
|
+
* which carries NO payload — verifies its own signature + subject and resolves to
|
|
12
|
+
* `suppress:'revoked'`. The old `verifyEnvelope` fails a tombstone's shape-check and returns
|
|
13
|
+
* `suppress:'unsupported_contract_version'` (still a blank badge, but the wrong suppressed-reason
|
|
14
|
+
* telemetry), so the suppression path routes through `verifyArtifact`.
|
|
15
|
+
*
|
|
16
|
+
* Drift is core-compensated (the browser can't recompute the canonical body hash), so
|
|
17
|
+
* `liveContentHash` is null → the kernel verifies signature + subject + lifecycle and skips
|
|
18
|
+
* drift. A non-2xx response (the API 404s on an untracked placement) THROWS, so the caller keeps
|
|
19
|
+
* the lifecycle-gated SSR badge rather than false-hiding it.
|
|
20
|
+
*/
|
|
21
|
+
import { type ResolvePublicKeyByKid } from '@certrev/cert-contract';
|
|
22
|
+
import type { VerifyBadgeFn } from './revalidate.js';
|
|
23
|
+
export declare function createDeliveryVerifier(resolveKid: ResolvePublicKeyByKid, fetchImpl?: typeof fetch): VerifyBadgeFn;
|
|
24
|
+
//# sourceMappingURL=verify-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-client.d.ts","sourceRoot":"","sources":["../../src/modal/verify-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAA6B,KAAK,qBAAqB,EAAkB,MAAM,wBAAwB,CAAA;AAC9G,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEjE,wBAAgB,sBAAsB,CACrC,UAAU,EAAE,qBAAqB,EACjC,SAAS,GAAE,OAAO,KAAa,GAC7B,aAAa,CAcf"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-side Delivery-API fetch + verify the Shopify Web Component uses. Thin glue around
|
|
3
|
+
* the CANONICAL kernel: it GETs the public, CDN-cacheable Delivery API
|
|
4
|
+
* (`{baseUrl}/api/cert/v1/delivery/{platform}/{externalId}`) and runs `verifyArtifact` from
|
|
5
|
+
* `@certrev/cert-contract` (WebCrypto Ed25519 over RFC-8785 JCS — the same kernel the server +
|
|
6
|
+
* every other edge run). No crypto is reimplemented here.
|
|
7
|
+
*
|
|
8
|
+
* `verifyArtifact` (POR-10684, cert-contract 0.3.0) dispatches on the `kind: 'tombstone'`
|
|
9
|
+
* discriminator: a full `CertDeliveryEnvelope` runs the whole crypto + lifecycle pipeline, while
|
|
10
|
+
* the SLIM `CertTombstone` the control plane now serves for a revoked placement (POR-10481) —
|
|
11
|
+
* which carries NO payload — verifies its own signature + subject and resolves to
|
|
12
|
+
* `suppress:'revoked'`. The old `verifyEnvelope` fails a tombstone's shape-check and returns
|
|
13
|
+
* `suppress:'unsupported_contract_version'` (still a blank badge, but the wrong suppressed-reason
|
|
14
|
+
* telemetry), so the suppression path routes through `verifyArtifact`.
|
|
15
|
+
*
|
|
16
|
+
* Drift is core-compensated (the browser can't recompute the canonical body hash), so
|
|
17
|
+
* `liveContentHash` is null → the kernel verifies signature + subject + lifecycle and skips
|
|
18
|
+
* drift. A non-2xx response (the API 404s on an untracked placement) THROWS, so the caller keeps
|
|
19
|
+
* the lifecycle-gated SSR badge rather than false-hiding it.
|
|
20
|
+
*/
|
|
21
|
+
import { verifyArtifact } from '@certrev/cert-contract';
|
|
22
|
+
export function createDeliveryVerifier(resolveKid, fetchImpl = fetch) {
|
|
23
|
+
return async (source) => {
|
|
24
|
+
const base = source.baseUrl.replace(/\/+$/, '');
|
|
25
|
+
const url = `${base}/api/cert/v1/delivery/${encodeURIComponent(source.platform)}/${encodeURIComponent(source.externalId)}`;
|
|
26
|
+
const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
27
|
+
if (!res.ok)
|
|
28
|
+
throw new Error(`cert delivery ${res.status}`);
|
|
29
|
+
const artifact = (await res.json());
|
|
30
|
+
return verifyArtifact(artifact, resolveKid, {
|
|
31
|
+
platform: source.platform,
|
|
32
|
+
externalId: source.externalId,
|
|
33
|
+
liveContentHash: null,
|
|
34
|
+
now: new Date(),
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=verify-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-client.js","sourceRoot":"","sources":["../../src/modal/verify-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAyD,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAG9G,MAAM,UAAU,sBAAsB,CACrC,UAAiC,EACjC,YAA0B,KAAK;IAE/B,OAAO,KAAK,EAAE,MAAmB,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QAC/C,MAAM,GAAG,GAAG,GAAG,IAAI,yBAAyB,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,kBAAkB,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAA;QAC1H,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAA;QAC7E,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;QAC3D,MAAM,QAAQ,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAyB,CAAA;QAC3D,OAAO,cAAc,CAAC,QAAQ,EAAE,UAAU,EAAE;YAC3C,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,eAAe,EAAE,IAAI;YACrB,GAAG,EAAE,IAAI,IAAI,EAAE;SACf,CAAC,CAAA;IACH,CAAC,CAAA;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@certrev/cert-block",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "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 framework-agnostic <certrev-badge> Web Component. Sign once (portal), render everywhere (Hydrogen / Next / Builder / universal embed).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,16 @@
|
|
|
21
21
|
"./fixtures": {
|
|
22
22
|
"types": "./dist/contract/fixtures.d.ts",
|
|
23
23
|
"default": "./dist/contract/fixtures.js"
|
|
24
|
-
}
|
|
24
|
+
},
|
|
25
|
+
"./modal": {
|
|
26
|
+
"types": "./dist/modal/index.d.ts",
|
|
27
|
+
"default": "./dist/modal/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./modal/placement": {
|
|
30
|
+
"types": "./dist/modal/placement.d.ts",
|
|
31
|
+
"default": "./dist/modal/placement.js"
|
|
32
|
+
},
|
|
33
|
+
"./modal/certrev-cert.js": "./dist/modal/certrev-cert.js"
|
|
25
34
|
},
|
|
26
35
|
"files": [
|
|
27
36
|
"dist",
|
|
@@ -37,12 +46,13 @@
|
|
|
37
46
|
"github-packages"
|
|
38
47
|
],
|
|
39
48
|
"scripts": {
|
|
40
|
-
"build": "tsc",
|
|
49
|
+
"build": "tsc && pnpm build:modal-bundle",
|
|
50
|
+
"build:modal-bundle": "pnpm dlx esbuild@0.27.7 src/modal/certrev-cert.entry.ts --bundle --minify --target=es2020 --format=iife --legal-comments=none --tsconfig-raw='{\"compilerOptions\":{\"target\":\"ES2017\",\"module\":\"esnext\",\"strict\":true}}' --banner:js='/* biome-ignore-all format: generated cert-badge bundle, see @certrev/cert-block src/modal/ */ /* biome-ignore-all lint: generated cert-badge bundle, see @certrev/cert-block src/modal/ */' --outfile=dist/modal/certrev-cert.js",
|
|
41
51
|
"typecheck": "tsc --noEmit",
|
|
42
52
|
"test": "vitest run"
|
|
43
53
|
},
|
|
44
54
|
"peerDependencies": {
|
|
45
|
-
"@certrev/cert-contract": ">=0.
|
|
55
|
+
"@certrev/cert-contract": ">=0.5.0",
|
|
46
56
|
"react": ">=18.0.0"
|
|
47
57
|
},
|
|
48
58
|
"peerDependenciesMeta": {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`renderCertBlock — locked-design snapshots > banner (navy default) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#0a1b3f;--ba-fg:#ffffff;--br:14px;--bf:'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;--surface:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background
|
|
3
|
+
exports[`renderCertBlock — locked-design snapshots > banner (navy default) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#0a1b3f;--ba-fg:#ffffff;--br:14px;--cr-bf:var(--bf,'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);--surface:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--cr-bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);cursor:pointer;"><span style="display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="20" height="20" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" aria-label="View certificate" style="display:inline-flex;align-items:center;flex-shrink:0;color:inherit;opacity:.72;text-decoration:none;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div><div style="padding:22px 24px 18px;"><div style="display:grid;grid-template-columns:1fr 1fr;gap:22px;"><div><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Written by</div><div style="display:flex;align-items:center;gap:12px;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">TE</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">The Editorial Team</div><div style="font-size:13px;color:var(--cr-ink-sub);">Editorial</div></div></div></div><div><div style="font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);margin-bottom:11px;">Reviewed by</div><div class="certrev-cert__rev" role="button" tabindex="0" aria-expanded="false" data-certrev-bio-toggle aria-label="Read more about Jane Doe" style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;border-radius:8px;transition:opacity .15s;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">JD</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD<span class="certrev-cert__caret" style="display:inline-flex;vertical-align:middle;margin-left:4px;color:var(--navy-35);transition:transform .18s ease-out;"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></div><div style="font-size:13px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:13px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div><div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--cr-ink-soft);">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</div></div></div></div></div><div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><span>Certified <span style="margin-left:8px;">Apr 14, 2026</span></span><span>Compensated expert</span></div></div></div><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--cr-bf);"><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--navy-55);margin-bottom:14px;">Expert memo</div><div style="display:flex;gap:15px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div style="flex:1;min-width:0;"><div style="font-size:16px;line-height:1.2;color:var(--cr-ink);"><span style="font-weight:700;">Jane Doe</span><span style="color:var(--cr-ink-sub);">, MD</span></div><p style="margin:11px 0 14px;font-size:15.5px;line-height:1.6;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</p></div></div><div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:16px 22px;flex-wrap:wrap;"><div style="display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><svg viewBox="0 0 375 375" width="15" height="15" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg><span>Verified by <span style="font-weight:700;color:var(--navy);">CertREV</span></span></div><div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Jane's Profile</a><span aria-hidden="true" style="font-family:var(--font-mono);font-size:10.5px;color:var(--navy-35);">|</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Certificate</a></div></div><div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div></div>"`;
|
|
4
4
|
|
|
5
|
-
exports[`renderCertBlock — locked-design snapshots > banner (themed, non-navy accent) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--bf:'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif;--surface:#faf8f4;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background
|
|
5
|
+
exports[`renderCertBlock — locked-design snapshots > banner (themed, non-navy accent) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--cr-bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);cursor:pointer;"><span style="display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="20" height="20" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" aria-label="View certificate" style="display:inline-flex;align-items:center;flex-shrink:0;color:inherit;opacity:.72;text-decoration:none;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div><div style="padding:22px 24px 18px;"><div style="display:grid;grid-template-columns:1fr 1fr;gap:22px;"><div><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Written by</div><div style="display:flex;align-items:center;gap:12px;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">TE</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">The Editorial Team</div><div style="font-size:13px;color:var(--cr-ink-sub);">Editorial</div></div></div></div><div><div style="font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);margin-bottom:11px;">Reviewed by</div><div class="certrev-cert__rev" role="button" tabindex="0" aria-expanded="false" data-certrev-bio-toggle aria-label="Read more about Jane Doe" style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;border-radius:8px;transition:opacity .15s;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">JD</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD<span class="certrev-cert__caret" style="display:inline-flex;vertical-align:middle;margin-left:4px;color:var(--navy-35);transition:transform .18s ease-out;"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></div><div style="font-size:13px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:13px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div><div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--cr-ink-soft);">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</div></div></div></div></div><div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><span>Certified <span style="margin-left:8px;">Apr 14, 2026</span></span><span>Compensated expert</span></div></div></div><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--cr-bf);"><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--navy-55);margin-bottom:14px;">Expert memo</div><div style="display:flex;gap:15px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div style="flex:1;min-width:0;"><div style="font-size:16px;line-height:1.2;color:var(--cr-ink);"><span style="font-weight:700;">Jane Doe</span><span style="color:var(--cr-ink-sub);">, MD</span></div><p style="margin:11px 0 14px;font-size:15.5px;line-height:1.6;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</p></div></div><div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:16px 22px;flex-wrap:wrap;"><div style="display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><svg viewBox="0 0 375 375" width="15" height="15" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg><span>Verified by <span style="font-weight:700;color:var(--navy);">CertREV</span></span></div><div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Jane's Profile</a><span aria-hidden="true" style="font-family:var(--font-mono);font-size:10.5px;color:var(--navy-35);">|</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Certificate</a></div></div><div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div></div>"`;
|
|
6
6
|
|
|
7
|
-
exports[`renderCertBlock — locked-design snapshots >
|
|
7
|
+
exports[`renderCertBlock — locked-design snapshots > banner (v2 brand-ink: barInk + inkColor) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--certrev-bar-bg:#000000;--certrev-bar-fg:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--certrev-ink:#1a1a1a;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:color-mix(in srgb,var(--cr-ink) 55%,transparent);--cr-ink-soft:color-mix(in srgb,var(--cr-ink) 75%,transparent);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--cr-bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);cursor:pointer;"><span style="display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="20" height="20" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" aria-label="View certificate" style="display:inline-flex;align-items:center;flex-shrink:0;color:inherit;opacity:.72;text-decoration:none;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div><div style="padding:22px 24px 18px;"><div style="display:grid;grid-template-columns:1fr 1fr;gap:22px;"><div><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Written by</div><div style="display:flex;align-items:center;gap:12px;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">TE</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">The Editorial Team</div><div style="font-size:13px;color:var(--cr-ink-sub);">Editorial</div></div></div></div><div><div style="font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);margin-bottom:11px;">Reviewed by</div><div class="certrev-cert__rev" role="button" tabindex="0" aria-expanded="false" data-certrev-bio-toggle aria-label="Read more about Jane Doe" style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;border-radius:8px;transition:opacity .15s;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">JD</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD<span class="certrev-cert__caret" style="display:inline-flex;vertical-align:middle;margin-left:4px;color:var(--navy-35);transition:transform .18s ease-out;"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></div><div style="font-size:13px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:13px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div><div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--cr-ink-soft);">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</div></div></div></div></div><div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><span>Certified <span style="margin-left:8px;">Apr 14, 2026</span></span><span>Compensated expert</span></div></div></div><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--cr-bf);"><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--navy-55);margin-bottom:14px;">Expert memo</div><div style="display:flex;gap:15px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div style="flex:1;min-width:0;"><div style="font-size:16px;line-height:1.2;color:var(--cr-ink);"><span style="font-weight:700;">Jane Doe</span><span style="color:var(--cr-ink-sub);">, MD</span></div><p style="margin:11px 0 14px;font-size:15.5px;line-height:1.6;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</p></div></div><div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:16px 22px;flex-wrap:wrap;"><div style="display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><svg viewBox="0 0 375 375" width="15" height="15" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg><span>Verified by <span style="font-weight:700;color:var(--navy);">CertREV</span></span></div><div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Jane's Profile</a><span aria-hidden="true" style="font-family:var(--font-mono);font-size:10.5px;color:var(--navy-35);">|</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Certificate</a></div></div><div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div></div>"`;
|
|
8
8
|
|
|
9
|
-
exports[`renderCertBlock — locked-design snapshots > floating (
|
|
9
|
+
exports[`renderCertBlock — locked-design snapshots > floating (navy default) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--floating" data-certrev-mode="floating" data-certrev-strings-version="2026-07" style="--ba:#0a1b3f;--ba-fg:#ffffff;--br:14px;--cr-bf:var(--bf,'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);--surface:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div class="certrev-cert__pill" style="display:inline-flex;align-items:center;gap:14px;padding:12px 22px;border-radius:999px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);box-shadow:0 10px 26px var(--navy-10);"><span style="font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;opacity:.75;">Expert reviewed:</span><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Profile</a><span style="opacity:.5;">·</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Certificate</a></div></div>"`;
|
|
10
10
|
|
|
11
|
-
exports[`renderCertBlock — locked-design snapshots >
|
|
11
|
+
exports[`renderCertBlock — locked-design snapshots > floating (themed, non-navy accent) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--floating" data-certrev-mode="floating" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div class="certrev-cert__pill" style="display:inline-flex;align-items:center;gap:14px;padding:12px 22px;border-radius:999px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);box-shadow:0 10px 26px var(--navy-10);"><span style="font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;opacity:.75;">Expert reviewed:</span><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Profile</a><span style="opacity:.5;">·</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Certificate</a></div></div>"`;
|
|
12
12
|
|
|
13
|
-
exports[`renderCertBlock — locked-design snapshots > sidebar (
|
|
13
|
+
exports[`renderCertBlock — locked-design snapshots > sidebar (navy default) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--sidebar" data-certrev-mode="sidebar" data-certrev-strings-version="2026-07" style="--ba:#0a1b3f;--ba-fg:#ffffff;--br:14px;--cr-bf:var(--bf,'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);--surface:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="background:var(--surface);border:1px solid var(--navy-10);border-radius:var(--br,14px);overflow:hidden;box-shadow:0 4px 18px var(--navy-06);font-family:var(--cr-bf);"><div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="18" height="18" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</div><div style="padding:20px;"><div style="display:flex;align-items:flex-start;gap:13px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div><div style="font-size:16px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div></div></div><p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</p><blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</blockquote><div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;"><span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">Compensated expert</span> · Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div><div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;border-right:1px solid var(--navy-10);cursor:pointer;">View Profile</a><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;">View Certificate</a></div></div></div>"`;
|
|
14
|
+
|
|
15
|
+
exports[`renderCertBlock — locked-design snapshots > sidebar (themed, non-navy accent) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--sidebar" data-certrev-mode="sidebar" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="background:var(--surface);border:1px solid var(--navy-10);border-radius:var(--br,14px);overflow:hidden;box-shadow:0 4px 18px var(--navy-06);font-family:var(--cr-bf);"><div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="18" height="18" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</div><div style="padding:20px;"><div style="display:flex;align-items:flex-start;gap:13px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div><div style="font-size:16px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div></div></div><p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</p><blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</blockquote><div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;"><span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">Compensated expert</span> · Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div><div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;border-right:1px solid var(--navy-10);cursor:pointer;">View Profile</a><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;">View Certificate</a></div></div></div>"`;
|
|
16
|
+
|
|
17
|
+
exports[`renderCertBlock — locked-design snapshots > sidebar (v2 brand-ink: barInk + inkColor) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--sidebar" data-certrev-mode="sidebar" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--certrev-bar-bg:#000000;--certrev-bar-fg:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--certrev-ink:#1a1a1a;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:color-mix(in srgb,var(--cr-ink) 55%,transparent);--cr-ink-soft:color-mix(in srgb,var(--cr-ink) 75%,transparent);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="background:var(--surface);border:1px solid var(--navy-10);border-radius:var(--br,14px);overflow:hidden;box-shadow:0 4px 18px var(--navy-06);font-family:var(--cr-bf);"><div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="18" height="18" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</div><div style="padding:20px;"><div style="display:flex;align-items:flex-start;gap:13px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div><div style="font-size:16px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div></div></div><p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</p><blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</blockquote><div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;"><span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">Compensated expert</span> · Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div><div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;border-right:1px solid var(--navy-10);cursor:pointer;">View Profile</a><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;">View Certificate</a></div></div></div>"`;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderPreviewBlock (POR-10743) — a static, non-delivery, hard-bounded pre-sales preview.
|
|
3
|
+
* Asserts: pixel-parity with renderCertBlock, the required expiry bound, the machine markers,
|
|
4
|
+
* and the safe-by-default visible label.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from 'vitest'
|
|
8
|
+
import { previewExpiry, renderPreviewBlock } from '../components/preview-block.js'
|
|
9
|
+
import type { CertBlockFacts } from '../components/render-cert-block.js'
|
|
10
|
+
import { renderCertBlock } from '../components/render-cert-block.js'
|
|
11
|
+
|
|
12
|
+
const FACTS: CertBlockFacts = {
|
|
13
|
+
authorName: 'Editorial',
|
|
14
|
+
reviewerName: 'Dr. Jane Doe',
|
|
15
|
+
credential: 'MD',
|
|
16
|
+
credentialVerifiedAt: '2026-06-20T00:00:00.000Z',
|
|
17
|
+
certifiedAt: '2026-06-20T00:00:00.000Z',
|
|
18
|
+
memo: 'Reviewed the claims against current guidance; accurate and safely framed.',
|
|
19
|
+
profileUrl: 'https://certrev.com/experts/jane',
|
|
20
|
+
certificateUrl: 'https://certrev.com/verify/cert_x',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const EXPIRES = '2026-08-01T00:00:00.000Z'
|
|
24
|
+
const input = { mode: 'banner', facts: FACTS } as const
|
|
25
|
+
|
|
26
|
+
describe('renderPreviewBlock', () => {
|
|
27
|
+
it('REQUIRES expiresAt — a preview must be time-bounded', () => {
|
|
28
|
+
expect(() => renderPreviewBlock(input, { expiresAt: '' })).toThrow(/time-bounded/)
|
|
29
|
+
expect(() => renderPreviewBlock(input, { expiresAt: ' ' })).toThrow(/time-bounded/)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('REJECTS an unparseable expiry — the bound must be a real date, not a toothless string', () => {
|
|
33
|
+
expect(() => renderPreviewBlock(input, { expiresAt: 'banana' })).toThrow(/parseable date/)
|
|
34
|
+
expect(() => renderPreviewBlock(input, { expiresAt: 'someday' })).toThrow(/parseable date/)
|
|
35
|
+
// a valid ISO date is accepted
|
|
36
|
+
expect(() => renderPreviewBlock(input, { expiresAt: EXPIRES })).not.toThrow()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('is PIXEL-PARITY — the inner visual is byte-identical to renderCertBlock', () => {
|
|
40
|
+
const preview = renderPreviewBlock(input, { expiresAt: EXPIRES, showLabel: false })
|
|
41
|
+
expect(preview).toContain(renderCertBlock(input))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('carries the non-delivery machine markers + the required expiry', () => {
|
|
45
|
+
const html = renderPreviewBlock(input, { expiresAt: EXPIRES })
|
|
46
|
+
expect(html).toContain('data-certrev-preview="1"')
|
|
47
|
+
expect(html).toContain(`data-certrev-preview-expires="${EXPIRES}"`)
|
|
48
|
+
// a leading comment banner spells out the non-delivery contract
|
|
49
|
+
expect(html).toContain('<!-- CertREV PREVIEW')
|
|
50
|
+
expect(html).toContain('Never use as delivery')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('shows the safe-default visible label; suppressible for a pixel-parity look-demo', () => {
|
|
54
|
+
const labeled = renderPreviewBlock(input, { expiresAt: EXPIRES })
|
|
55
|
+
expect(labeled).toContain('data-certrev-preview-label')
|
|
56
|
+
expect(labeled).toContain('Preview — not a verified certification')
|
|
57
|
+
|
|
58
|
+
const bare = renderPreviewBlock(input, { expiresAt: EXPIRES, showLabel: false })
|
|
59
|
+
expect(bare).not.toContain('data-certrev-preview-label')
|
|
60
|
+
// even bare, the machine marker + expiry remain (never suppressible)
|
|
61
|
+
expect(bare).toContain('data-certrev-preview="1"')
|
|
62
|
+
expect(bare).toContain(`data-certrev-preview-expires="${EXPIRES}"`)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('honors a custom label', () => {
|
|
66
|
+
const html = renderPreviewBlock(input, { expiresAt: EXPIRES, labelText: 'Sample only' })
|
|
67
|
+
expect(html).toContain('Sample only')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('ESCAPES the caller-supplied label — a malicious labelText cannot inject markup', () => {
|
|
71
|
+
const html = renderPreviewBlock(input, {
|
|
72
|
+
expiresAt: EXPIRES,
|
|
73
|
+
labelText: '<img src=x onerror=alert(1)>',
|
|
74
|
+
})
|
|
75
|
+
expect(html).not.toContain('<img src=x onerror=alert(1)>')
|
|
76
|
+
expect(html).toContain('<img src=x onerror=alert(1)>')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('previewExpiry extracts the baked expiry, and is null for a non-preview', () => {
|
|
80
|
+
const html = renderPreviewBlock(input, { expiresAt: EXPIRES })
|
|
81
|
+
expect(previewExpiry(html)).toBe(EXPIRES)
|
|
82
|
+
expect(previewExpiry(renderCertBlock(input))).toBeNull()
|
|
83
|
+
})
|
|
84
|
+
})
|