@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.
@@ -1,73 +1,75 @@
1
1
  /**
2
- * @certrev/cert-block/builder — the Builder.io adapter for the cert-block render edge.
2
+ * @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
3
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.
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 LAYOUTan 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
- * 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
+ * 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
- * 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.
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
- /** 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 {
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
- /** 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;
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
- * 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).
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 CertReviewBlock({ placementId }: CertReviewBlockProps): import("react").JSX.Element | null;
58
+ export declare function CertRevAnchor(): import("react").JSX.Element | null;
50
59
  /**
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.
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 CertReviewBuilderRegistration {
57
- component: typeof CertReviewBlock;
64
+ export interface CertRevBuilderRegistration {
65
+ component: typeof CertRevAnchor;
58
66
  name: string;
59
- inputs: Array<{
60
- name: string;
61
- type: 'string';
62
- required?: boolean;
63
- helperText?: string;
64
- }>;
67
+ inputs: never[];
65
68
  }
66
69
  /**
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).
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 certReviewBuilderComponent: CertReviewBuilderRegistration;
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;;;;;;;;;;;;;;;;;;;;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"}
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"}
@@ -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 — the Builder.io adapter for the cert-block render edge.
3
+ * @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
4
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.
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 LAYOUTan 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
- * 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
+ * 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
- * 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.
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 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
+ 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 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).
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 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 });
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 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).
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 certReviewBuilderComponent = {
51
- component: CertReviewBlock,
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;;;;;;;;;;;;;;;;;;;;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"}
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
- "name": "@certrev/cert-block",
3
- "version": "0.1.2",
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"
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`), 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:
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
- * a render-verdict placement renders the in-DOM cert badge (crawlable);
7
- * • EVERY non-render pathsuppress 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.
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 { CertReviewBlock, CertVerifyProvider, certReviewBuilderComponent, type VerifiedPlacements } from '../index.js'
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 renderBlock(placements: VerifiedPlacements, placementId?: string): string {
23
+ function render(current: CurrentCredential | null, isEditing = false): string {
25
24
  return renderToStaticMarkup(
26
- <CertVerifyProvider value={placements}>
27
- <CertReviewBlock placementId={placementId} />
28
- </CertVerifyProvider>,
25
+ <CertRevProvider current={current} isEditing={isEditing}>
26
+ <CertRevAnchor />
27
+ </CertRevProvider>,
29
28
  )
30
29
  }
31
30
 
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')
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 — 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')
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('data-certrev-cert-id="cert_fixture_001"')
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-verdict placement renders nothing', () => {
52
- expect(renderBlock({ 'p-revoked': { verdict: suppressVerdict } }, 'p-revoked')).toBe('')
47
+ it('FAIL-CLOSED: a suppress verdict renders nothing', () => {
48
+ expect(render({ verdict: suppressVerdict })).toBe('')
53
49
  })
54
50
 
55
- it('FAIL-CLOSED: an unknown placementId renders nothing', () => {
56
- expect(renderBlock({ 'p-valid': { verdict: renderVerdict } }, 'p-not-here')).toBe('')
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('FAIL-CLOSED: a missing placementId renders nothing', () => {
60
- expect(renderBlock({ 'p-valid': { verdict: renderVerdict } }, undefined)).toBe('')
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('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
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
  })
@@ -1,100 +1,122 @@
1
1
  /**
2
- * @certrev/cert-block/builder — the Builder.io adapter for the cert-block render edge.
2
+ * @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
3
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.
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 LAYOUTan 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
- * 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
+ * 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
- * 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.
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
- /** 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 {
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
- /** 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>
38
+ interface CertRevContextValue {
39
+ readonly current: CurrentCredential | null
40
+ readonly isEditing: boolean
41
+ }
36
42
 
37
- const CertVerifyContext = createContext<VerifiedPlacements>({})
43
+ const CertRevContext = createContext<CertRevContextValue>({ current: null, isEditing: false })
38
44
 
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,
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 value: VerifiedPlacements
56
+ readonly current: CurrentCredential | null
57
+ readonly isEditing?: boolean
46
58
  readonly children: ReactNode
47
59
  }) {
48
- return <CertVerifyContext.Provider value={value}>{children}</CertVerifyContext.Provider>
60
+ return <CertRevContext.Provider value={{ current, isEditing }}>{children}</CertRevContext.Provider>
49
61
  }
50
62
 
51
- export interface CertReviewBlockProps {
52
- /** The CertREV placement id the editor set on this Builder block. */
53
- readonly placementId?: string
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 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).
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 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} />
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` — 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.
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 CertReviewBuilderRegistration {
76
- component: typeof CertReviewBlock
107
+ export interface CertRevBuilderRegistration {
108
+ component: typeof CertRevAnchor
77
109
  name: string
78
- inputs: Array<{ name: string; type: 'string'; required?: boolean; helperText?: string }>
110
+ inputs: never[]
79
111
  }
80
112
 
81
113
  /**
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).
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 certReviewBuilderComponent: CertReviewBuilderRegistration = {
88
- component: CertReviewBlock,
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
  }