@certrev/cert-block 0.1.2 → 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.
- package/dist/builder/index.d.ts +54 -52
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +64 -47
- package/dist/builder/index.js.map +1 -1
- package/package.json +76 -76
- package/src/builder/__tests__/builder.test.tsx +38 -51
- package/src/builder/index.tsx +86 -64
package/dist/builder/index.d.ts
CHANGED
|
@@ -1,73 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @certrev/cert-block/builder —
|
|
2
|
+
* @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
+
* 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.)
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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()`).
|
|
21
26
|
*/
|
|
22
27
|
import { type ReactNode } from 'react';
|
|
23
28
|
import type { CertVerdict } from '../contract/kernel.js';
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
export interface VerifiedPlacement {
|
|
29
|
+
/** The current page's credential, resolved + verified by the loader (Track-1 delivery). */
|
|
30
|
+
export interface CurrentCredential {
|
|
27
31
|
readonly verdict: CertVerdict;
|
|
32
|
+
/** Canonical page URL for JSON-LD @id alignment (used by the server-side JSON-LD, not the anchor). */
|
|
28
33
|
readonly pageUrl?: string;
|
|
29
34
|
}
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
|
37
44
|
readonly children: ReactNode;
|
|
38
45
|
}): 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
46
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
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.
|
|
48
57
|
*/
|
|
49
|
-
export declare function
|
|
58
|
+
export declare function CertRevAnchor(): import("react").JSX.Element | null;
|
|
50
59
|
/**
|
|
51
|
-
* Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` —
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>` call site.
|
|
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={[...]}>`.
|
|
55
63
|
*/
|
|
56
|
-
export interface
|
|
57
|
-
component: typeof
|
|
64
|
+
export interface CertRevBuilderRegistration {
|
|
65
|
+
component: typeof CertRevAnchor;
|
|
58
66
|
name: string;
|
|
59
|
-
inputs:
|
|
60
|
-
name: string;
|
|
61
|
-
type: 'string';
|
|
62
|
-
required?: boolean;
|
|
63
|
-
helperText?: string;
|
|
64
|
-
}>;
|
|
67
|
+
inputs: never[];
|
|
65
68
|
}
|
|
66
69
|
/**
|
|
67
|
-
* The registration
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* Builder Write API (no visual-editor registration required for rendering).
|
|
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.
|
|
71
73
|
*/
|
|
72
|
-
export declare const
|
|
74
|
+
export declare const certRevAnchorComponent: CertRevBuilderRegistration;
|
|
73
75
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/builder/index.js
CHANGED
|
@@ -1,63 +1,80 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* @certrev/cert-block/builder —
|
|
3
|
+
* @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
+
* 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.)
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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()`).
|
|
22
27
|
*/
|
|
23
28
|
import { createContext, useContext } from 'react';
|
|
24
29
|
import { CertReview } from '../components/CertReview.js';
|
|
25
|
-
const
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
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."] }));
|
|
30
54
|
}
|
|
31
55
|
/**
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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.
|
|
36
60
|
*/
|
|
37
|
-
export function
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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;
|
|
43
69
|
}
|
|
44
70
|
/**
|
|
45
|
-
* The registration
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Builder Write API (no visual-editor registration required for rendering).
|
|
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.
|
|
49
74
|
*/
|
|
50
|
-
export const
|
|
51
|
-
component:
|
|
75
|
+
export const certRevAnchorComponent = {
|
|
76
|
+
component: CertRevAnchor,
|
|
52
77
|
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
|
-
],
|
|
78
|
+
inputs: [],
|
|
62
79
|
};
|
|
63
80
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":";AAAA
|
|
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,78 +1,78 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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"
|
|
78
78
|
}
|
|
@@ -1,81 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSR-render tests for the Builder.io adapter (`@certrev/cert-block/builder`),
|
|
3
|
-
* through `
|
|
4
|
-
* produce crawlable HTML on the edge. These prove the adapter's load-bearing contract:
|
|
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).
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
* •
|
|
8
|
-
*
|
|
9
|
-
* •
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
14
13
|
*/
|
|
15
14
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
16
15
|
import { describe, expect, it } from 'vitest'
|
|
17
16
|
import { makeMockPayload } from '../../contract/fixtures.js'
|
|
18
17
|
import type { CertVerdict } from '../../contract/kernel.js'
|
|
19
|
-
import {
|
|
18
|
+
import { CertRevAnchor, CertRevProvider, certRevAnchorComponent, type CurrentCredential } from '../index.js'
|
|
20
19
|
|
|
21
20
|
const renderVerdict: CertVerdict = { decision: 'render', payload: makeMockPayload() }
|
|
22
21
|
const suppressVerdict: CertVerdict = { decision: 'suppress', reason: 'revoked' }
|
|
23
22
|
|
|
24
|
-
function
|
|
23
|
+
function render(current: CurrentCredential | null, isEditing = false): string {
|
|
25
24
|
return renderToStaticMarkup(
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
</
|
|
25
|
+
<CertRevProvider current={current} isEditing={isEditing}>
|
|
26
|
+
<CertRevAnchor />
|
|
27
|
+
</CertRevProvider>,
|
|
29
28
|
)
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
describe('Builder adapter — registration', () => {
|
|
33
|
-
it('registers
|
|
34
|
-
expect(
|
|
35
|
-
expect(
|
|
36
|
-
|
|
37
|
-
expect(input).toBeDefined()
|
|
38
|
-
expect(input?.required).toBe(true)
|
|
39
|
-
expect(input?.type).toBe('string')
|
|
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([])
|
|
40
36
|
})
|
|
41
37
|
})
|
|
42
38
|
|
|
43
|
-
describe('Builder adapter —
|
|
44
|
-
it('renders the verified badge
|
|
45
|
-
const html =
|
|
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' })
|
|
46
42
|
expect(html).toContain('certrev-badge')
|
|
47
43
|
expect(html).toContain('Dr. Jane Doe')
|
|
48
|
-
expect(html).toContain('
|
|
44
|
+
expect(html).not.toContain('application/ld+json') // JSON-LD decoupled to the article template
|
|
49
45
|
})
|
|
50
46
|
|
|
51
|
-
it('FAIL-CLOSED: a suppress
|
|
52
|
-
expect(
|
|
47
|
+
it('FAIL-CLOSED: a suppress verdict renders nothing', () => {
|
|
48
|
+
expect(render({ verdict: suppressVerdict })).toBe('')
|
|
53
49
|
})
|
|
54
50
|
|
|
55
|
-
it('FAIL-CLOSED:
|
|
56
|
-
expect(
|
|
51
|
+
it('FAIL-CLOSED: no credential for this page renders nothing in production', () => {
|
|
52
|
+
expect(render(null, false)).toBe('')
|
|
57
53
|
})
|
|
58
54
|
|
|
59
|
-
it('
|
|
60
|
-
|
|
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
61
|
})
|
|
62
62
|
|
|
63
|
-
it('
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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')
|
|
80
67
|
})
|
|
81
68
|
})
|
package/src/builder/index.tsx
CHANGED
|
@@ -1,100 +1,122 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @certrev/cert-block/builder —
|
|
2
|
+
* @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
+
* 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.)
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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()`).
|
|
21
26
|
*/
|
|
22
27
|
import { createContext, type ReactNode, useContext } from 'react'
|
|
23
28
|
import { CertReview } from '../components/CertReview.js'
|
|
24
29
|
import type { CertVerdict } from '../contract/kernel.js'
|
|
25
30
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
export interface VerifiedPlacement {
|
|
31
|
+
/** The current page's credential, resolved + verified by the loader (Track-1 delivery). */
|
|
32
|
+
export interface CurrentCredential {
|
|
29
33
|
readonly verdict: CertVerdict
|
|
34
|
+
/** Canonical page URL for JSON-LD @id alignment (used by the server-side JSON-LD, not the anchor). */
|
|
30
35
|
readonly pageUrl?: string
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
interface CertRevContextValue {
|
|
39
|
+
readonly current: CurrentCredential | null
|
|
40
|
+
readonly isEditing: boolean
|
|
41
|
+
}
|
|
36
42
|
|
|
37
|
-
const
|
|
43
|
+
const CertRevContext = createContext<CertRevContextValue>({ current: null, isEditing: false })
|
|
38
44
|
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
43
54
|
children,
|
|
44
55
|
}: {
|
|
45
|
-
readonly
|
|
56
|
+
readonly current: CurrentCredential | null
|
|
57
|
+
readonly isEditing?: boolean
|
|
46
58
|
readonly children: ReactNode
|
|
47
59
|
}) {
|
|
48
|
-
return <
|
|
60
|
+
return <CertRevContext.Provider value={{ current, isEditing }}>{children}</CertRevContext.Provider>
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
)
|
|
54
85
|
}
|
|
55
86
|
|
|
56
87
|
/**
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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.
|
|
61
92
|
*/
|
|
62
|
-
export function
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
67
100
|
}
|
|
68
101
|
|
|
69
102
|
/**
|
|
70
|
-
* Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` —
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>` call site.
|
|
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={[...]}>`.
|
|
74
106
|
*/
|
|
75
|
-
export interface
|
|
76
|
-
component: typeof
|
|
107
|
+
export interface CertRevBuilderRegistration {
|
|
108
|
+
component: typeof CertRevAnchor
|
|
77
109
|
name: string
|
|
78
|
-
inputs:
|
|
110
|
+
inputs: never[]
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
/**
|
|
82
|
-
* The registration
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* Builder Write API (no visual-editor registration required for rendering).
|
|
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.
|
|
86
117
|
*/
|
|
87
|
-
export const
|
|
88
|
-
component:
|
|
118
|
+
export const certRevAnchorComponent: CertRevBuilderRegistration = {
|
|
119
|
+
component: CertRevAnchor,
|
|
89
120
|
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
|
-
],
|
|
121
|
+
inputs: [],
|
|
100
122
|
}
|