@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
|
-
|
|
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
|
-
|
|
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
|
+
}
|