@certrev/cert-block 0.1.1 → 0.1.3

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
3
+ *
4
+ * Validated by an independent architecture panel (Codex gpt-5.5 + Gemini, blind): a visual
5
+ * page builder must NOT choose WHICH credential renders — that invites cross-article misuse and
6
+ * re-opens the wrong-subject surface. Instead:
7
+ * • the CMS controls LAYOUT — an editor drags a ZERO-INPUT <CertRevAnchor/> to position the
8
+ * badge on the page;
9
+ * • the SYSTEM controls TRUTH — the headless loader resolves THIS page's credential from its
10
+ * own URL (Track-1 delivery), crypto-verifies it (fail-closed VerdictKernel), and supplies
11
+ * the verdict via context.
12
+ * The anchor renders the current page's verified badge or NOTHING. It cannot be pointed at
13
+ * another article, and there is no `placementId` input to forge or mis-select.
14
+ *
15
+ * JSON-LD is DECOUPLED (panel finding): the anchor renders the BADGE only (`omitJsonLd`). The
16
+ * article template emits the schema.org JSON-LD server-side from the same verified verdict, so a
17
+ * page builder can't duplicate, move, or omit structured data. (See the route example.)
18
+ *
19
+ * EDGE-CACHE DISCIPLINE (panel finding): the consumer's loader MUST verify per request as a
20
+ * dynamic edge subrequest and must NOT long-cache the rendered badge HTML — a revoked/expired
21
+ * credential sitting in a stale edge cache would defeat fail-closed. Bound any positive-verdict
22
+ * cache by min(expiry, revocation-TTL, content-version).
23
+ *
24
+ * cert-block carries no Builder dependency: the registration shape is declared structurally, and
25
+ * `isEditing` is a passed-in flag (the consumer supplies Builder's `isPreviewing()`).
26
+ */
27
+ import { type ReactNode } from 'react';
28
+ import type { CertVerdict } from '../contract/kernel.js';
29
+ /** The current page's credential, resolved + verified by the loader (Track-1 delivery). */
30
+ export interface CurrentCredential {
31
+ readonly verdict: CertVerdict;
32
+ /** Canonical page URL for JSON-LD @id alignment (used by the server-side JSON-LD, not the anchor). */
33
+ readonly pageUrl?: string;
34
+ }
35
+ /**
36
+ * Supplies THIS page's loader-verified credential to the anchor. `current` is the verdict for
37
+ * the current page (or null if the page has no delivered credential). `isEditing` is the
38
+ * consumer's Builder `isPreviewing()` result — it ONLY toggles a design-time placeholder and
39
+ * NEVER affects the published render.
40
+ */
41
+ export declare function CertRevProvider({ current, isEditing, children, }: {
42
+ readonly current: CurrentCredential | null;
43
+ readonly isEditing?: boolean;
44
+ readonly children: ReactNode;
45
+ }): import("react").JSX.Element;
46
+ /**
47
+ * Design-time placeholder shown ONLY in the Builder editor when the current page has no live
48
+ * verified credential. Truthful (never a fake badge), emits NO JSON-LD, and never renders in
49
+ * production — so it cannot leak to or be crawled on the live site.
50
+ */
51
+ export declare function CertRevEditorPlaceholder(): import("react").JSX.Element;
52
+ /**
53
+ * The Builder block. ZERO inputs — a layout anchor only. Renders the current page's VERIFIED
54
+ * badge (badge only; JSON-LD is emitted server-side), the editor placeholder in design mode, or
55
+ * NOTHING in production (fail-closed). The credential is whatever Track-1 delivered + verified for
56
+ * THIS page; the editor can neither choose nor forge it.
57
+ */
58
+ export declare function CertRevAnchor(): import("react").JSX.Element | null;
59
+ /**
60
+ * Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` — the subset cert-block
61
+ * populates. Declared locally so cert-block needs no Builder build/runtime dependency; assignable
62
+ * to Builder's `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>`.
63
+ */
64
+ export interface CertRevBuilderRegistration {
65
+ component: typeof CertRevAnchor;
66
+ name: string;
67
+ inputs: never[];
68
+ }
69
+ /**
70
+ * The registration to pass to Builder's `<Content customComponents={[...]}>`. ZERO inputs by
71
+ * design — the editor places it (layout); the system resolves the credential from the page URL
72
+ * (truth). Matched by NAME at render, so Write-API content works without visual-editor setup.
73
+ */
74
+ export declare const certRevAnchorComponent: CertRevBuilderRegistration;
75
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAA;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,2FAA2F;AAC3F,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,sGAAsG;IACtG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CACzB;AASD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC/B,OAAO,EACP,SAAiB,EACjB,QAAQ,GACR,EAAE;IACF,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC5B,+BAEA;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,gCAiBvC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,uCAO5B;AAED;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IAC1C,SAAS,EAAE,OAAO,aAAa,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,KAAK,EAAE,CAAA;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,0BAIpC,CAAA"}
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
4
+ *
5
+ * Validated by an independent architecture panel (Codex gpt-5.5 + Gemini, blind): a visual
6
+ * page builder must NOT choose WHICH credential renders — that invites cross-article misuse and
7
+ * re-opens the wrong-subject surface. Instead:
8
+ * • the CMS controls LAYOUT — an editor drags a ZERO-INPUT <CertRevAnchor/> to position the
9
+ * badge on the page;
10
+ * • the SYSTEM controls TRUTH — the headless loader resolves THIS page's credential from its
11
+ * own URL (Track-1 delivery), crypto-verifies it (fail-closed VerdictKernel), and supplies
12
+ * the verdict via context.
13
+ * The anchor renders the current page's verified badge or NOTHING. It cannot be pointed at
14
+ * another article, and there is no `placementId` input to forge or mis-select.
15
+ *
16
+ * JSON-LD is DECOUPLED (panel finding): the anchor renders the BADGE only (`omitJsonLd`). The
17
+ * article template emits the schema.org JSON-LD server-side from the same verified verdict, so a
18
+ * page builder can't duplicate, move, or omit structured data. (See the route example.)
19
+ *
20
+ * EDGE-CACHE DISCIPLINE (panel finding): the consumer's loader MUST verify per request as a
21
+ * dynamic edge subrequest and must NOT long-cache the rendered badge HTML — a revoked/expired
22
+ * credential sitting in a stale edge cache would defeat fail-closed. Bound any positive-verdict
23
+ * cache by min(expiry, revocation-TTL, content-version).
24
+ *
25
+ * cert-block carries no Builder dependency: the registration shape is declared structurally, and
26
+ * `isEditing` is a passed-in flag (the consumer supplies Builder's `isPreviewing()`).
27
+ */
28
+ import { createContext, useContext } from 'react';
29
+ import { CertReview } from '../components/CertReview.js';
30
+ const CertRevContext = createContext({ current: null, isEditing: false });
31
+ /**
32
+ * Supplies THIS page's loader-verified credential to the anchor. `current` is the verdict for
33
+ * the current page (or null if the page has no delivered credential). `isEditing` is the
34
+ * consumer's Builder `isPreviewing()` result — it ONLY toggles a design-time placeholder and
35
+ * NEVER affects the published render.
36
+ */
37
+ export function CertRevProvider({ current, isEditing = false, children, }) {
38
+ return _jsx(CertRevContext.Provider, { value: { current, isEditing }, children: children });
39
+ }
40
+ /**
41
+ * Design-time placeholder shown ONLY in the Builder editor when the current page has no live
42
+ * verified credential. Truthful (never a fake badge), emits NO JSON-LD, and never renders in
43
+ * production — so it cannot leak to or be crawled on the live site.
44
+ */
45
+ export function CertRevEditorPlaceholder() {
46
+ return (_jsxs("div", { "data-certrev-editor-placeholder": "", style: {
47
+ border: '1px dashed #e6007e',
48
+ borderRadius: 8,
49
+ padding: '10px 14px',
50
+ font: '13px system-ui',
51
+ color: '#6b7280',
52
+ background: '#fff5fa',
53
+ }, children: ["CertREV Review \u2014 ", _jsx("b", { children: "resolves at publish" }), " from issuer verification. The expert badge appears here once this page\u2019s article is certified, and only while the credential is valid."] }));
54
+ }
55
+ /**
56
+ * The Builder block. ZERO inputs — a layout anchor only. Renders the current page's VERIFIED
57
+ * badge (badge only; JSON-LD is emitted server-side), the editor placeholder in design mode, or
58
+ * NOTHING in production (fail-closed). The credential is whatever Track-1 delivered + verified for
59
+ * THIS page; the editor can neither choose nor forge it.
60
+ */
61
+ export function CertRevAnchor() {
62
+ const { current, isEditing } = useContext(CertRevContext);
63
+ if (current && current.verdict.decision === 'render') {
64
+ return _jsx(CertReview, { verdict: current.verdict, pageUrl: current.pageUrl, omitJsonLd: true });
65
+ }
66
+ if (isEditing)
67
+ return _jsx(CertRevEditorPlaceholder, {});
68
+ return null;
69
+ }
70
+ /**
71
+ * The registration to pass to Builder's `<Content customComponents={[...]}>`. ZERO inputs by
72
+ * design — the editor places it (layout); the system resolves the credential from the page URL
73
+ * (truth). Matched by NAME at render, so Write-API content works without visual-editor setup.
74
+ */
75
+ export const certRevAnchorComponent = {
76
+ component: CertRevAnchor,
77
+ name: 'CertREV Review',
78
+ inputs: [],
79
+ };
80
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAexD,MAAM,cAAc,GAAG,aAAa,CAAsB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;AAE9F;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,EAC/B,OAAO,EACP,SAAS,GAAG,KAAK,EACjB,QAAQ,GAKR;IACA,OAAO,KAAC,cAAc,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,YAAG,QAAQ,GAA2B,CAAA;AACpG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB;IACvC,OAAO,CACN,kDACiC,EAAE,EAClC,KAAK,EAAE;YACN,MAAM,EAAE,oBAAoB;YAC5B,YAAY,EAAE,CAAC;YACf,OAAO,EAAE,WAAW;YACpB,IAAI,EAAE,gBAAgB;YACtB,KAAK,EAAE,SAAS;YAChB,UAAU,EAAE,SAAS;SACrB,uCAEgB,8CAA0B,oJAEtC,CACN,CAAA;AACF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC5B,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,UAAU,CAAC,cAAc,CAAC,CAAA;IACzD,IAAI,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACtD,OAAO,KAAC,UAAU,IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,UAAU,SAAG,CAAA;IACrF,CAAC;IACD,IAAI,SAAS;QAAE,OAAO,KAAC,wBAAwB,KAAG,CAAA;IAClD,OAAO,IAAI,CAAA;AACZ,CAAC;AAaD;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAA+B;IACjE,SAAS,EAAE,aAAa;IACxB,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE,EAAE;CACV,CAAA"}
package/package.json CHANGED
@@ -1,74 +1,78 @@
1
1
  {
2
- "name": "@certrev/cert-block",
3
- "version": "0.1.1",
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
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "default": "./dist/index.js"
12
- },
13
- "./webcomponent": {
14
- "types": "./dist/webcomponent/certrev-badge.d.ts",
15
- "default": "./dist/webcomponent/certrev-badge.js"
16
- },
17
- "./fixtures": {
18
- "types": "./dist/contract/fixtures.d.ts",
19
- "default": "./dist/contract/fixtures.js"
20
- }
21
- },
22
- "files": [
23
- "dist",
24
- "src",
25
- "README.md"
26
- ],
27
- "publishConfig": {
28
- "registry": "https://registry.npmjs.org",
29
- "access": "public"
30
- },
31
- "publishTargets": [
32
- "npmjs"
33
- ],
34
- "scripts": {
35
- "build": "tsc",
36
- "typecheck": "tsc --noEmit",
37
- "test": "vitest run"
38
- },
39
- "peerDependencies": {
40
- "@certrev/cert-contract": ">=0.1.2",
41
- "react": ">=18.0.0"
42
- },
43
- "peerDependenciesMeta": {
44
- "react": {
45
- "optional": false
46
- }
47
- },
48
- "devDependencies": {
49
- "@certrev/cert-contract": "workspace:*",
50
- "@testing-library/react": "^16.1.0",
51
- "@types/node": "^20.0.0",
52
- "@types/react": "^18.3.0",
53
- "@types/react-dom": "^18.3.0",
54
- "jsdom": "^25.0.0",
55
- "react": "^18.3.1",
56
- "react-dom": "^18.3.1",
57
- "typescript": "^6.0.3",
58
- "vitest": "^4.1.5"
59
- },
60
- "keywords": [
61
- "certrev",
62
- "certification",
63
- "react",
64
- "server-components",
65
- "hydrogen",
66
- "shopify",
67
- "json-ld",
68
- "schema-org",
69
- "web-component",
70
- "ed25519",
71
- "ssr"
72
- ],
73
- "license": "MIT"
2
+ "name": "@certrev/cert-block",
3
+ "version": "0.1.3",
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
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./builder": {
14
+ "types": "./dist/builder/index.d.ts",
15
+ "default": "./dist/builder/index.js"
16
+ },
17
+ "./webcomponent": {
18
+ "types": "./dist/webcomponent/certrev-badge.d.ts",
19
+ "default": "./dist/webcomponent/certrev-badge.js"
20
+ },
21
+ "./fixtures": {
22
+ "types": "./dist/contract/fixtures.d.ts",
23
+ "default": "./dist/contract/fixtures.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src",
29
+ "README.md"
30
+ ],
31
+ "publishConfig": {
32
+ "registry": "https://registry.npmjs.org",
33
+ "access": "public"
34
+ },
35
+ "publishTargets": [
36
+ "npmjs"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run"
42
+ },
43
+ "peerDependencies": {
44
+ "@certrev/cert-contract": ">=0.1.2",
45
+ "react": ">=18.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "react": {
49
+ "optional": false
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@certrev/cert-contract": "workspace:*",
54
+ "@testing-library/react": "^16.1.0",
55
+ "@types/node": "^20.0.0",
56
+ "@types/react": "^18.3.0",
57
+ "@types/react-dom": "^18.3.0",
58
+ "jsdom": "^25.0.0",
59
+ "react": "^18.3.1",
60
+ "react-dom": "^18.3.1",
61
+ "typescript": "^6.0.3",
62
+ "vitest": "^4.1.5"
63
+ },
64
+ "keywords": [
65
+ "certrev",
66
+ "certification",
67
+ "react",
68
+ "server-components",
69
+ "hydrogen",
70
+ "shopify",
71
+ "json-ld",
72
+ "schema-org",
73
+ "web-component",
74
+ "ed25519",
75
+ "ssr"
76
+ ],
77
+ "license": "MIT"
74
78
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * SSR-render tests for the Builder.io adapter (`@certrev/cert-block/builder`), the panel-validated
3
+ * "ambient-URL anchor" model — exercised through `renderToStaticMarkup` (the edge SSR path).
4
+ *
5
+ * Contract proved here:
6
+ * • the anchor renders the CURRENT page's verified badge — and badge ONLY (JSON-LD is emitted
7
+ * server-side, decoupled from the visual slot);
8
+ * • EVERY non-render path (suppress verdict, no credential for the page) renders nothing in
9
+ * production (fail-closed);
10
+ * • in the Builder editor (`isEditing`) with no live credential, a truthful placeholder shows —
11
+ * never a fake badge, and never any JSON-LD;
12
+ * • the registration is ZERO-input: the editor cannot choose or forge which credential renders.
13
+ */
14
+ import { renderToStaticMarkup } from 'react-dom/server'
15
+ import { describe, expect, it } from 'vitest'
16
+ import { makeMockPayload } from '../../contract/fixtures.js'
17
+ import type { CertVerdict } from '../../contract/kernel.js'
18
+ import { CertRevAnchor, CertRevProvider, certRevAnchorComponent, type CurrentCredential } from '../index.js'
19
+
20
+ const renderVerdict: CertVerdict = { decision: 'render', payload: makeMockPayload() }
21
+ const suppressVerdict: CertVerdict = { decision: 'suppress', reason: 'revoked' }
22
+
23
+ function render(current: CurrentCredential | null, isEditing = false): string {
24
+ return renderToStaticMarkup(
25
+ <CertRevProvider current={current} isEditing={isEditing}>
26
+ <CertRevAnchor />
27
+ </CertRevProvider>,
28
+ )
29
+ }
30
+
31
+ describe('Builder adapter — registration (zero-input anchor)', () => {
32
+ it('registers "CertREV Review" with NO inputs (editor places it; system resolves the credential)', () => {
33
+ expect(certRevAnchorComponent.name).toBe('CertREV Review')
34
+ expect(certRevAnchorComponent.component).toBe(CertRevAnchor)
35
+ expect(certRevAnchorComponent.inputs).toEqual([])
36
+ })
37
+ })
38
+
39
+ describe('Builder adapter — CertRevAnchor render', () => {
40
+ it('renders the current page’s verified badge — and ONLY the badge (no JSON-LD; that is server-side)', () => {
41
+ const html = render({ verdict: renderVerdict, pageUrl: 'https://brand.example.com/a' })
42
+ expect(html).toContain('certrev-badge')
43
+ expect(html).toContain('Dr. Jane Doe')
44
+ expect(html).not.toContain('application/ld+json') // JSON-LD decoupled to the article template
45
+ })
46
+
47
+ it('FAIL-CLOSED: a suppress verdict renders nothing', () => {
48
+ expect(render({ verdict: suppressVerdict })).toBe('')
49
+ })
50
+
51
+ it('FAIL-CLOSED: no credential for this page renders nothing in production', () => {
52
+ expect(render(null, false)).toBe('')
53
+ })
54
+
55
+ it('EDITOR: no live credential + isEditing renders a truthful placeholder (no badge, no JSON-LD)', () => {
56
+ const html = render(null, true)
57
+ expect(html).toContain('data-certrev-editor-placeholder')
58
+ expect(html).toContain('resolves at publish')
59
+ expect(html).not.toContain('certrev-badge')
60
+ expect(html).not.toContain('application/ld+json')
61
+ })
62
+
63
+ it('EDITOR: a live verified credential renders the REAL badge even in editing mode (true draft-verify)', () => {
64
+ const html = render({ verdict: renderVerdict }, true)
65
+ expect(html).toContain('certrev-badge')
66
+ expect(html).not.toContain('data-certrev-editor-placeholder')
67
+ })
68
+ })
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
3
+ *
4
+ * Validated by an independent architecture panel (Codex gpt-5.5 + Gemini, blind): a visual
5
+ * page builder must NOT choose WHICH credential renders — that invites cross-article misuse and
6
+ * re-opens the wrong-subject surface. Instead:
7
+ * • the CMS controls LAYOUT — an editor drags a ZERO-INPUT <CertRevAnchor/> to position the
8
+ * badge on the page;
9
+ * • the SYSTEM controls TRUTH — the headless loader resolves THIS page's credential from its
10
+ * own URL (Track-1 delivery), crypto-verifies it (fail-closed VerdictKernel), and supplies
11
+ * the verdict via context.
12
+ * The anchor renders the current page's verified badge or NOTHING. It cannot be pointed at
13
+ * another article, and there is no `placementId` input to forge or mis-select.
14
+ *
15
+ * JSON-LD is DECOUPLED (panel finding): the anchor renders the BADGE only (`omitJsonLd`). The
16
+ * article template emits the schema.org JSON-LD server-side from the same verified verdict, so a
17
+ * page builder can't duplicate, move, or omit structured data. (See the route example.)
18
+ *
19
+ * EDGE-CACHE DISCIPLINE (panel finding): the consumer's loader MUST verify per request as a
20
+ * dynamic edge subrequest and must NOT long-cache the rendered badge HTML — a revoked/expired
21
+ * credential sitting in a stale edge cache would defeat fail-closed. Bound any positive-verdict
22
+ * cache by min(expiry, revocation-TTL, content-version).
23
+ *
24
+ * cert-block carries no Builder dependency: the registration shape is declared structurally, and
25
+ * `isEditing` is a passed-in flag (the consumer supplies Builder's `isPreviewing()`).
26
+ */
27
+ import { createContext, type ReactNode, useContext } from 'react'
28
+ import { CertReview } from '../components/CertReview.js'
29
+ import type { CertVerdict } from '../contract/kernel.js'
30
+
31
+ /** The current page's credential, resolved + verified by the loader (Track-1 delivery). */
32
+ export interface CurrentCredential {
33
+ readonly verdict: CertVerdict
34
+ /** Canonical page URL for JSON-LD @id alignment (used by the server-side JSON-LD, not the anchor). */
35
+ readonly pageUrl?: string
36
+ }
37
+
38
+ interface CertRevContextValue {
39
+ readonly current: CurrentCredential | null
40
+ readonly isEditing: boolean
41
+ }
42
+
43
+ const CertRevContext = createContext<CertRevContextValue>({ current: null, isEditing: false })
44
+
45
+ /**
46
+ * Supplies THIS page's loader-verified credential to the anchor. `current` is the verdict for
47
+ * the current page (or null if the page has no delivered credential). `isEditing` is the
48
+ * consumer's Builder `isPreviewing()` result — it ONLY toggles a design-time placeholder and
49
+ * NEVER affects the published render.
50
+ */
51
+ export function CertRevProvider({
52
+ current,
53
+ isEditing = false,
54
+ children,
55
+ }: {
56
+ readonly current: CurrentCredential | null
57
+ readonly isEditing?: boolean
58
+ readonly children: ReactNode
59
+ }) {
60
+ return <CertRevContext.Provider value={{ current, isEditing }}>{children}</CertRevContext.Provider>
61
+ }
62
+
63
+ /**
64
+ * Design-time placeholder shown ONLY in the Builder editor when the current page has no live
65
+ * verified credential. Truthful (never a fake badge), emits NO JSON-LD, and never renders in
66
+ * production — so it cannot leak to or be crawled on the live site.
67
+ */
68
+ export function CertRevEditorPlaceholder() {
69
+ return (
70
+ <div
71
+ data-certrev-editor-placeholder=""
72
+ style={{
73
+ border: '1px dashed #e6007e',
74
+ borderRadius: 8,
75
+ padding: '10px 14px',
76
+ font: '13px system-ui',
77
+ color: '#6b7280',
78
+ background: '#fff5fa',
79
+ }}
80
+ >
81
+ CertREV Review — <b>resolves at publish</b> from issuer verification. The expert badge appears here once this
82
+ page’s article is certified, and only while the credential is valid.
83
+ </div>
84
+ )
85
+ }
86
+
87
+ /**
88
+ * The Builder block. ZERO inputs — a layout anchor only. Renders the current page's VERIFIED
89
+ * badge (badge only; JSON-LD is emitted server-side), the editor placeholder in design mode, or
90
+ * NOTHING in production (fail-closed). The credential is whatever Track-1 delivered + verified for
91
+ * THIS page; the editor can neither choose nor forge it.
92
+ */
93
+ export function CertRevAnchor() {
94
+ const { current, isEditing } = useContext(CertRevContext)
95
+ if (current && current.verdict.decision === 'render') {
96
+ return <CertReview verdict={current.verdict} pageUrl={current.pageUrl} omitJsonLd />
97
+ }
98
+ if (isEditing) return <CertRevEditorPlaceholder />
99
+ return null
100
+ }
101
+
102
+ /**
103
+ * Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` — the subset cert-block
104
+ * populates. Declared locally so cert-block needs no Builder build/runtime dependency; assignable
105
+ * to Builder's `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>`.
106
+ */
107
+ export interface CertRevBuilderRegistration {
108
+ component: typeof CertRevAnchor
109
+ name: string
110
+ inputs: never[]
111
+ }
112
+
113
+ /**
114
+ * The registration to pass to Builder's `<Content customComponents={[...]}>`. ZERO inputs by
115
+ * design — the editor places it (layout); the system resolves the credential from the page URL
116
+ * (truth). Matched by NAME at render, so Write-API content works without visual-editor setup.
117
+ */
118
+ export const certRevAnchorComponent: CertRevBuilderRegistration = {
119
+ component: CertRevAnchor,
120
+ name: 'CertREV Review',
121
+ inputs: [],
122
+ }