@certrev/cert-block 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @certrev/cert-block/builder — the Builder.io adapter for the cert-block render edge.
|
|
3
|
+
*
|
|
4
|
+
* Lets a Builder.io content editor place a "CertREV Review" block into a page; the
|
|
5
|
+
* headless app's loader (Hydrogen/Next/Remix) crypto-verifies that placement's signed
|
|
6
|
+
* envelope and supplies the verdict via React context; this block renders cert-block's
|
|
7
|
+
* <CertReview> from the VERIFIED verdict — and renders NOTHING (fail-closed) on any
|
|
8
|
+
* suppress or unknown placement.
|
|
9
|
+
*
|
|
10
|
+
* THE TRUST BOUNDARY: the credential is never trusted from CMS content. A Builder block
|
|
11
|
+
* only carries an opaque `placementId` pointer; the authority comes solely from the
|
|
12
|
+
* loader-side WebCrypto verdict supplied through <CertVerifyProvider>. So a revoked /
|
|
13
|
+
* tampered / expired / wrong-subject placement that an editor drops into Builder renders
|
|
14
|
+
* nothing, even though the block is present in the published content.
|
|
15
|
+
*
|
|
16
|
+
* cert-block carries NO dependency on `@builder.io/sdk-react` — the registration's shape is
|
|
17
|
+
* declared structurally below, so the emitted JS pulls no Builder code and the type resolves
|
|
18
|
+
* without Builder installed. The consumer (who already has Builder) passes
|
|
19
|
+
* `certReviewBuilderComponent` to Builder's `<Content customComponents={[...]}>`; it is
|
|
20
|
+
* assignable to Builder's `RegisteredComponent` there.
|
|
21
|
+
*/
|
|
22
|
+
import { type ReactNode } from 'react';
|
|
23
|
+
import type { CertVerdict } from '../contract/kernel.js';
|
|
24
|
+
/** A loader-verified placement: the verdict cert-block renders from, plus the canonical
|
|
25
|
+
* page URL for JSON-LD @id alignment. */
|
|
26
|
+
export interface VerifiedPlacement {
|
|
27
|
+
readonly verdict: CertVerdict;
|
|
28
|
+
readonly pageUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Map of `placementId` → the loader's verified result. Built server-side in the loader
|
|
31
|
+
* (it crypto-verifies each placement) and handed to <CertVerifyProvider>. */
|
|
32
|
+
export type VerifiedPlacements = Record<string, VerifiedPlacement>;
|
|
33
|
+
/** Wraps Builder's <Content>; carries the loader's verified verdicts down to every
|
|
34
|
+
* CertReview block, keyed by `placementId`. */
|
|
35
|
+
export declare function CertVerifyProvider({ value, children, }: {
|
|
36
|
+
readonly value: VerifiedPlacements;
|
|
37
|
+
readonly children: ReactNode;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
export interface CertReviewBlockProps {
|
|
40
|
+
/** The CertREV placement id the editor set on this Builder block. */
|
|
41
|
+
readonly placementId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The component Builder renders for a "CertREV Review" block. It pulls the loader-verified
|
|
45
|
+
* verdict for its `placementId` from context and renders <CertReview>. Unknown placement
|
|
46
|
+
* OR a non-`render` verdict → renders nothing. cert-block's <CertReview> ALSO suppresses on
|
|
47
|
+
* a non-render verdict, so the boundary fails closed twice over (defense in depth).
|
|
48
|
+
*/
|
|
49
|
+
export declare function CertReviewBlock({ placementId }: CertReviewBlockProps): import("react").JSX.Element | null;
|
|
50
|
+
/**
|
|
51
|
+
* Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` — exactly the subset
|
|
52
|
+
* cert-block populates. Declared locally (not imported) so cert-block needs no Builder
|
|
53
|
+
* build- or runtime-dependency; the object below is assignable to Builder's
|
|
54
|
+
* `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>` call site.
|
|
55
|
+
*/
|
|
56
|
+
export interface CertReviewBuilderRegistration {
|
|
57
|
+
component: typeof CertReviewBlock;
|
|
58
|
+
name: string;
|
|
59
|
+
inputs: Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
type: 'string';
|
|
62
|
+
required?: boolean;
|
|
63
|
+
helperText?: string;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The registration object to pass to Builder's `<Content customComponents={[...]}>`.
|
|
68
|
+
* Editors see a "CertREV Review" component with one input — the placement id. Because the
|
|
69
|
+
* block is matched by NAME at render time, this also works for content created via the
|
|
70
|
+
* Builder Write API (no visual-editor registration required for rendering).
|
|
71
|
+
*/
|
|
72
|
+
export declare const certReviewBuilderComponent: CertReviewBuilderRegistration;
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAA;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD;0CAC0C;AAC1C,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;8EAC8E;AAC9E,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAIlE;gDACgD;AAChD,wBAAgB,kBAAkB,CAAC,EAClC,KAAK,EACL,QAAQ,GACR,EAAE;IACF,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAA;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC5B,+BAEA;AAED,MAAM,WAAW,oBAAoB;IACpC,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,EAAE,oBAAoB,sCAKpE;AAED;;;;;GAKG;AACH,MAAM,WAAW,6BAA6B;IAC7C,SAAS,EAAE,OAAO,eAAe,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACxF;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,6BAaxC,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @certrev/cert-block/builder — the Builder.io adapter for the cert-block render edge.
|
|
4
|
+
*
|
|
5
|
+
* Lets a Builder.io content editor place a "CertREV Review" block into a page; the
|
|
6
|
+
* headless app's loader (Hydrogen/Next/Remix) crypto-verifies that placement's signed
|
|
7
|
+
* envelope and supplies the verdict via React context; this block renders cert-block's
|
|
8
|
+
* <CertReview> from the VERIFIED verdict — and renders NOTHING (fail-closed) on any
|
|
9
|
+
* suppress or unknown placement.
|
|
10
|
+
*
|
|
11
|
+
* THE TRUST BOUNDARY: the credential is never trusted from CMS content. A Builder block
|
|
12
|
+
* only carries an opaque `placementId` pointer; the authority comes solely from the
|
|
13
|
+
* loader-side WebCrypto verdict supplied through <CertVerifyProvider>. So a revoked /
|
|
14
|
+
* tampered / expired / wrong-subject placement that an editor drops into Builder renders
|
|
15
|
+
* nothing, even though the block is present in the published content.
|
|
16
|
+
*
|
|
17
|
+
* cert-block carries NO dependency on `@builder.io/sdk-react` — the registration's shape is
|
|
18
|
+
* declared structurally below, so the emitted JS pulls no Builder code and the type resolves
|
|
19
|
+
* without Builder installed. The consumer (who already has Builder) passes
|
|
20
|
+
* `certReviewBuilderComponent` to Builder's `<Content customComponents={[...]}>`; it is
|
|
21
|
+
* assignable to Builder's `RegisteredComponent` there.
|
|
22
|
+
*/
|
|
23
|
+
import { createContext, useContext } from 'react';
|
|
24
|
+
import { CertReview } from '../components/CertReview.js';
|
|
25
|
+
const CertVerifyContext = createContext({});
|
|
26
|
+
/** Wraps Builder's <Content>; carries the loader's verified verdicts down to every
|
|
27
|
+
* CertReview block, keyed by `placementId`. */
|
|
28
|
+
export function CertVerifyProvider({ value, children, }) {
|
|
29
|
+
return _jsx(CertVerifyContext.Provider, { value: value, children: children });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The component Builder renders for a "CertREV Review" block. It pulls the loader-verified
|
|
33
|
+
* verdict for its `placementId` from context and renders <CertReview>. Unknown placement
|
|
34
|
+
* OR a non-`render` verdict → renders nothing. cert-block's <CertReview> ALSO suppresses on
|
|
35
|
+
* a non-render verdict, so the boundary fails closed twice over (defense in depth).
|
|
36
|
+
*/
|
|
37
|
+
export function CertReviewBlock({ placementId }) {
|
|
38
|
+
const placements = useContext(CertVerifyContext);
|
|
39
|
+
const placement = placementId ? placements[placementId] : undefined;
|
|
40
|
+
if (!placement)
|
|
41
|
+
return null;
|
|
42
|
+
return _jsx(CertReview, { verdict: placement.verdict, pageUrl: placement.pageUrl });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The registration object to pass to Builder's `<Content customComponents={[...]}>`.
|
|
46
|
+
* Editors see a "CertREV Review" component with one input — the placement id. Because the
|
|
47
|
+
* block is matched by NAME at render time, this also works for content created via the
|
|
48
|
+
* Builder Write API (no visual-editor registration required for rendering).
|
|
49
|
+
*/
|
|
50
|
+
export const certReviewBuilderComponent = {
|
|
51
|
+
component: CertReviewBlock,
|
|
52
|
+
name: 'CertREV Review',
|
|
53
|
+
inputs: [
|
|
54
|
+
{
|
|
55
|
+
name: 'placementId',
|
|
56
|
+
type: 'string',
|
|
57
|
+
required: true,
|
|
58
|
+
helperText: 'CertREV placement id (which certified article this badge attests). The headless ' +
|
|
59
|
+
'loader verifies that placement’s signed envelope; only a render-verdict shows a badge.',
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAcxD,MAAM,iBAAiB,GAAG,aAAa,CAAqB,EAAE,CAAC,CAAA;AAE/D;gDACgD;AAChD,MAAM,UAAU,kBAAkB,CAAC,EAClC,KAAK,EACL,QAAQ,GAIR;IACA,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAA;AACzF,CAAC;AAOD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,WAAW,EAAwB;IACpE,MAAM,UAAU,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAA;IAChD,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACnE,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAA;IAC3B,OAAO,KAAC,UAAU,IAAC,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,GAAI,CAAA;AAC9E,CAAC;AAcD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAkC;IACxE,SAAS,EAAE,eAAe;IAC1B,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE;QACP;YACC,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,IAAI;YACd,UAAU,EACT,kFAAkF;gBAClF,wFAAwF;SACzF;KACD;CACD,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@certrev/cert-block",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./builder": {
|
|
14
|
+
"types": "./dist/builder/index.d.ts",
|
|
15
|
+
"default": "./dist/builder/index.js"
|
|
16
|
+
},
|
|
13
17
|
"./webcomponent": {
|
|
14
18
|
"types": "./dist/webcomponent/certrev-badge.d.ts",
|
|
15
19
|
"default": "./dist/webcomponent/certrev-badge.js"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR-render tests for the Builder.io adapter (`@certrev/cert-block/builder`), exercised
|
|
3
|
+
* through `react-dom/server`'s `renderToStaticMarkup` — the same path Hydrogen/Next use to
|
|
4
|
+
* produce crawlable HTML on the edge. These prove the adapter's load-bearing contract:
|
|
5
|
+
*
|
|
6
|
+
* • a render-verdict placement renders the in-DOM cert badge (crawlable);
|
|
7
|
+
* • EVERY non-render path — suppress verdict, unknown placement, missing placementId —
|
|
8
|
+
* renders NOTHING (fail-closed survives the CMS boundary);
|
|
9
|
+
* • when one Content tree mixes a valid + a revoked placement (the /builder-cert proof
|
|
10
|
+
* shape), only the valid one renders.
|
|
11
|
+
*
|
|
12
|
+
* Authority comes solely from the loader-supplied verdict in <CertVerifyProvider>; the
|
|
13
|
+
* `placementId` is an opaque pointer the CMS cannot use to manufacture a badge.
|
|
14
|
+
*/
|
|
15
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
import { makeMockPayload } from '../../contract/fixtures.js'
|
|
18
|
+
import type { CertVerdict } from '../../contract/kernel.js'
|
|
19
|
+
import { CertReviewBlock, CertVerifyProvider, certReviewBuilderComponent, type VerifiedPlacements } from '../index.js'
|
|
20
|
+
|
|
21
|
+
const renderVerdict: CertVerdict = { decision: 'render', payload: makeMockPayload() }
|
|
22
|
+
const suppressVerdict: CertVerdict = { decision: 'suppress', reason: 'revoked' }
|
|
23
|
+
|
|
24
|
+
function renderBlock(placements: VerifiedPlacements, placementId?: string): string {
|
|
25
|
+
return renderToStaticMarkup(
|
|
26
|
+
<CertVerifyProvider value={placements}>
|
|
27
|
+
<CertReviewBlock placementId={placementId} />
|
|
28
|
+
</CertVerifyProvider>,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Builder adapter — registration', () => {
|
|
33
|
+
it('registers as "CertREV Review" with a single required string `placementId` input', () => {
|
|
34
|
+
expect(certReviewBuilderComponent.name).toBe('CertREV Review')
|
|
35
|
+
expect(certReviewBuilderComponent.component).toBe(CertReviewBlock)
|
|
36
|
+
const input = certReviewBuilderComponent.inputs?.find((i) => i.name === 'placementId')
|
|
37
|
+
expect(input).toBeDefined()
|
|
38
|
+
expect(input?.required).toBe(true)
|
|
39
|
+
expect(input?.type).toBe('string')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('Builder adapter — CertReviewBlock render', () => {
|
|
44
|
+
it('renders the verified badge for a render-verdict placement (in-DOM, crawlable)', () => {
|
|
45
|
+
const html = renderBlock({ 'p-valid': { verdict: renderVerdict, pageUrl: 'https://brand.example.com/a' } }, 'p-valid')
|
|
46
|
+
expect(html).toContain('certrev-badge')
|
|
47
|
+
expect(html).toContain('Dr. Jane Doe')
|
|
48
|
+
expect(html).toContain('data-certrev-cert-id="cert_fixture_001"')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('FAIL-CLOSED: a suppress-verdict placement renders nothing', () => {
|
|
52
|
+
expect(renderBlock({ 'p-revoked': { verdict: suppressVerdict } }, 'p-revoked')).toBe('')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('FAIL-CLOSED: an unknown placementId renders nothing', () => {
|
|
56
|
+
expect(renderBlock({ 'p-valid': { verdict: renderVerdict } }, 'p-not-here')).toBe('')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('FAIL-CLOSED: a missing placementId renders nothing', () => {
|
|
60
|
+
expect(renderBlock({ 'p-valid': { verdict: renderVerdict } }, undefined)).toBe('')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('mixed content (valid + revoked in one Content tree): only the valid placement renders', () => {
|
|
64
|
+
const placements: VerifiedPlacements = {
|
|
65
|
+
'p-valid': { verdict: renderVerdict, pageUrl: 'https://brand.example.com/a' },
|
|
66
|
+
'p-revoked': { verdict: suppressVerdict },
|
|
67
|
+
}
|
|
68
|
+
const html = renderToStaticMarkup(
|
|
69
|
+
<CertVerifyProvider value={placements}>
|
|
70
|
+
<div id="valid-block">
|
|
71
|
+
<CertReviewBlock placementId="p-valid" />
|
|
72
|
+
</div>
|
|
73
|
+
<div id="revoked-block">
|
|
74
|
+
<CertReviewBlock placementId="p-revoked" />
|
|
75
|
+
</div>
|
|
76
|
+
</CertVerifyProvider>,
|
|
77
|
+
)
|
|
78
|
+
expect(html).toContain('certrev-badge') // valid rendered
|
|
79
|
+
expect(html).toContain('<div id="revoked-block"></div>') // revoked empty — fail-closed through the CMS
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @certrev/cert-block/builder — the Builder.io adapter for the cert-block render edge.
|
|
3
|
+
*
|
|
4
|
+
* Lets a Builder.io content editor place a "CertREV Review" block into a page; the
|
|
5
|
+
* headless app's loader (Hydrogen/Next/Remix) crypto-verifies that placement's signed
|
|
6
|
+
* envelope and supplies the verdict via React context; this block renders cert-block's
|
|
7
|
+
* <CertReview> from the VERIFIED verdict — and renders NOTHING (fail-closed) on any
|
|
8
|
+
* suppress or unknown placement.
|
|
9
|
+
*
|
|
10
|
+
* THE TRUST BOUNDARY: the credential is never trusted from CMS content. A Builder block
|
|
11
|
+
* only carries an opaque `placementId` pointer; the authority comes solely from the
|
|
12
|
+
* loader-side WebCrypto verdict supplied through <CertVerifyProvider>. So a revoked /
|
|
13
|
+
* tampered / expired / wrong-subject placement that an editor drops into Builder renders
|
|
14
|
+
* nothing, even though the block is present in the published content.
|
|
15
|
+
*
|
|
16
|
+
* cert-block carries NO dependency on `@builder.io/sdk-react` — the registration's shape is
|
|
17
|
+
* declared structurally below, so the emitted JS pulls no Builder code and the type resolves
|
|
18
|
+
* without Builder installed. The consumer (who already has Builder) passes
|
|
19
|
+
* `certReviewBuilderComponent` to Builder's `<Content customComponents={[...]}>`; it is
|
|
20
|
+
* assignable to Builder's `RegisteredComponent` there.
|
|
21
|
+
*/
|
|
22
|
+
import { createContext, type ReactNode, useContext } from 'react'
|
|
23
|
+
import { CertReview } from '../components/CertReview.js'
|
|
24
|
+
import type { CertVerdict } from '../contract/kernel.js'
|
|
25
|
+
|
|
26
|
+
/** A loader-verified placement: the verdict cert-block renders from, plus the canonical
|
|
27
|
+
* page URL for JSON-LD @id alignment. */
|
|
28
|
+
export interface VerifiedPlacement {
|
|
29
|
+
readonly verdict: CertVerdict
|
|
30
|
+
readonly pageUrl?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Map of `placementId` → the loader's verified result. Built server-side in the loader
|
|
34
|
+
* (it crypto-verifies each placement) and handed to <CertVerifyProvider>. */
|
|
35
|
+
export type VerifiedPlacements = Record<string, VerifiedPlacement>
|
|
36
|
+
|
|
37
|
+
const CertVerifyContext = createContext<VerifiedPlacements>({})
|
|
38
|
+
|
|
39
|
+
/** Wraps Builder's <Content>; carries the loader's verified verdicts down to every
|
|
40
|
+
* CertReview block, keyed by `placementId`. */
|
|
41
|
+
export function CertVerifyProvider({
|
|
42
|
+
value,
|
|
43
|
+
children,
|
|
44
|
+
}: {
|
|
45
|
+
readonly value: VerifiedPlacements
|
|
46
|
+
readonly children: ReactNode
|
|
47
|
+
}) {
|
|
48
|
+
return <CertVerifyContext.Provider value={value}>{children}</CertVerifyContext.Provider>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CertReviewBlockProps {
|
|
52
|
+
/** The CertREV placement id the editor set on this Builder block. */
|
|
53
|
+
readonly placementId?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The component Builder renders for a "CertREV Review" block. It pulls the loader-verified
|
|
58
|
+
* verdict for its `placementId` from context and renders <CertReview>. Unknown placement
|
|
59
|
+
* OR a non-`render` verdict → renders nothing. cert-block's <CertReview> ALSO suppresses on
|
|
60
|
+
* a non-render verdict, so the boundary fails closed twice over (defense in depth).
|
|
61
|
+
*/
|
|
62
|
+
export function CertReviewBlock({ placementId }: CertReviewBlockProps) {
|
|
63
|
+
const placements = useContext(CertVerifyContext)
|
|
64
|
+
const placement = placementId ? placements[placementId] : undefined
|
|
65
|
+
if (!placement) return null
|
|
66
|
+
return <CertReview verdict={placement.verdict} pageUrl={placement.pageUrl} />
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` — exactly the subset
|
|
71
|
+
* cert-block populates. Declared locally (not imported) so cert-block needs no Builder
|
|
72
|
+
* build- or runtime-dependency; the object below is assignable to Builder's
|
|
73
|
+
* `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>` call site.
|
|
74
|
+
*/
|
|
75
|
+
export interface CertReviewBuilderRegistration {
|
|
76
|
+
component: typeof CertReviewBlock
|
|
77
|
+
name: string
|
|
78
|
+
inputs: Array<{ name: string; type: 'string'; required?: boolean; helperText?: string }>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The registration object to pass to Builder's `<Content customComponents={[...]}>`.
|
|
83
|
+
* Editors see a "CertREV Review" component with one input — the placement id. Because the
|
|
84
|
+
* block is matched by NAME at render time, this also works for content created via the
|
|
85
|
+
* Builder Write API (no visual-editor registration required for rendering).
|
|
86
|
+
*/
|
|
87
|
+
export const certReviewBuilderComponent: CertReviewBuilderRegistration = {
|
|
88
|
+
component: CertReviewBlock,
|
|
89
|
+
name: 'CertREV Review',
|
|
90
|
+
inputs: [
|
|
91
|
+
{
|
|
92
|
+
name: 'placementId',
|
|
93
|
+
type: 'string',
|
|
94
|
+
required: true,
|
|
95
|
+
helperText:
|
|
96
|
+
'CertREV placement id (which certified article this badge attests). The headless ' +
|
|
97
|
+
'loader verifies that placement’s signed envelope; only a render-verdict shows a badge.',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}
|