@certrev/cert-block 0.5.4 → 1.0.0

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.
Files changed (189) hide show
  1. package/CHANGELOG.md +195 -12
  2. package/LICENSE +21 -0
  3. package/README.md +422 -50
  4. package/dist/builder/cert-chrome-data.d.ts +26 -8
  5. package/dist/builder/cert-chrome-data.d.ts.map +1 -1
  6. package/dist/builder/cert-chrome-data.js +5 -5
  7. package/dist/builder/cert-chrome-data.js.map +1 -1
  8. package/dist/builder/cert-review-card.d.ts +35 -5
  9. package/dist/builder/cert-review-card.d.ts.map +1 -1
  10. package/dist/builder/cert-review-card.js +115 -34
  11. package/dist/builder/cert-review-card.js.map +1 -1
  12. package/dist/builder/index.d.ts +33 -16
  13. package/dist/builder/index.d.ts.map +1 -1
  14. package/dist/builder/index.js +33 -16
  15. package/dist/builder/index.js.map +1 -1
  16. package/dist/components/CertBadge.d.ts +2 -2
  17. package/dist/components/CertBadge.d.ts.map +1 -1
  18. package/dist/components/CertBadge.js +11 -5
  19. package/dist/components/CertBadge.js.map +1 -1
  20. package/dist/components/CertJsonLd.d.ts.map +1 -1
  21. package/dist/components/CertJsonLd.js.map +1 -1
  22. package/dist/components/CertRevBacklink.d.ts.map +1 -1
  23. package/dist/components/CertRevBacklink.js +3 -2
  24. package/dist/components/CertRevBacklink.js.map +1 -1
  25. package/dist/components/CertReview.d.ts +1 -1
  26. package/dist/components/ExpertBio.d.ts.map +1 -1
  27. package/dist/components/ExpertBio.js +12 -4
  28. package/dist/components/ExpertBio.js.map +1 -1
  29. package/dist/components/escape.d.ts +28 -1
  30. package/dist/components/escape.d.ts.map +1 -1
  31. package/dist/components/escape.js +37 -8
  32. package/dist/components/escape.js.map +1 -1
  33. package/dist/components/format.d.ts +51 -2
  34. package/dist/components/format.d.ts.map +1 -1
  35. package/dist/components/format.js +103 -6
  36. package/dist/components/format.js.map +1 -1
  37. package/dist/components/preview-block.d.ts +1 -1
  38. package/dist/components/preview-block.js +3 -3
  39. package/dist/components/preview-block.js.map +1 -1
  40. package/dist/components/rel.d.ts +24 -0
  41. package/dist/components/rel.d.ts.map +1 -0
  42. package/dist/components/rel.js +24 -0
  43. package/dist/components/rel.js.map +1 -0
  44. package/dist/components/render-cert-block.d.ts +50 -22
  45. package/dist/components/render-cert-block.d.ts.map +1 -1
  46. package/dist/components/render-cert-block.js +146 -80
  47. package/dist/components/render-cert-block.js.map +1 -1
  48. package/dist/components/render-def.d.ts +13 -14
  49. package/dist/components/render-def.d.ts.map +1 -1
  50. package/dist/components/render-def.js +14 -15
  51. package/dist/components/render-def.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/jsonld/project.d.ts +46 -13
  57. package/dist/jsonld/project.d.ts.map +1 -1
  58. package/dist/jsonld/project.js +139 -56
  59. package/dist/jsonld/project.js.map +1 -1
  60. package/dist/modal/avatar-resize.d.ts +10 -10
  61. package/dist/modal/avatar-resize.js +14 -14
  62. package/dist/modal/avatar-resize.js.map +1 -1
  63. package/dist/modal/cert-modal-view.d.ts +27 -7
  64. package/dist/modal/cert-modal-view.d.ts.map +1 -1
  65. package/dist/modal/cert-modal-view.js +49 -25
  66. package/dist/modal/cert-modal-view.js.map +1 -1
  67. package/dist/modal/certrev-cert-modal.d.ts +5 -5
  68. package/dist/modal/certrev-cert-modal.js +9 -9
  69. package/dist/modal/certrev-cert-modal.js.map +1 -1
  70. package/dist/modal/certrev-cert-modal.styles.d.ts +6 -6
  71. package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -1
  72. package/dist/modal/certrev-cert-modal.styles.js +8 -8
  73. package/dist/modal/certrev-cert-modal.styles.js.map +1 -1
  74. package/dist/modal/certrev-cert.entry.d.ts +8 -6
  75. package/dist/modal/certrev-cert.entry.d.ts.map +1 -1
  76. package/dist/modal/certrev-cert.entry.js +9 -7
  77. package/dist/modal/certrev-cert.entry.js.map +1 -1
  78. package/dist/modal/certrev-cert.js +6 -6
  79. package/dist/modal/display-strings.d.ts +25 -5
  80. package/dist/modal/display-strings.d.ts.map +1 -1
  81. package/dist/modal/display-strings.js +27 -5
  82. package/dist/modal/display-strings.js.map +1 -1
  83. package/dist/modal/escape.d.ts +26 -3
  84. package/dist/modal/escape.d.ts.map +1 -1
  85. package/dist/modal/escape.js +32 -3
  86. package/dist/modal/escape.js.map +1 -1
  87. package/dist/modal/font-face.d.ts.map +1 -1
  88. package/dist/modal/font-face.js +8 -11
  89. package/dist/modal/font-face.js.map +1 -1
  90. package/dist/modal/ftc-disclosure.d.ts +2 -2
  91. package/dist/modal/ftc-disclosure.js +2 -2
  92. package/dist/modal/ftc-guard.d.ts +44 -9
  93. package/dist/modal/ftc-guard.d.ts.map +1 -1
  94. package/dist/modal/ftc-guard.js +50 -17
  95. package/dist/modal/ftc-guard.js.map +1 -1
  96. package/dist/modal/index.d.ts +1 -1
  97. package/dist/modal/index.js +1 -1
  98. package/dist/modal/interactions.d.ts +2 -2
  99. package/dist/modal/interactions.js +2 -2
  100. package/dist/modal/logo-paths.js +2 -2
  101. package/dist/modal/logo-paths.js.map +1 -1
  102. package/dist/modal/placement.d.ts +4 -3
  103. package/dist/modal/placement.d.ts.map +1 -1
  104. package/dist/modal/placement.js +7 -6
  105. package/dist/modal/placement.js.map +1 -1
  106. package/dist/modal/revalidate.d.ts +2 -3
  107. package/dist/modal/revalidate.d.ts.map +1 -1
  108. package/dist/modal/revalidate.js +2 -3
  109. package/dist/modal/revalidate.js.map +1 -1
  110. package/dist/modal/verify-client.d.ts +2 -2
  111. package/dist/modal/verify-client.js +2 -2
  112. package/dist/verify/cache.d.ts +21 -6
  113. package/dist/verify/cache.d.ts.map +1 -1
  114. package/dist/verify/cache.js +29 -8
  115. package/dist/verify/cache.js.map +1 -1
  116. package/dist/verify/fetch-with-deadline.d.ts +35 -0
  117. package/dist/verify/fetch-with-deadline.d.ts.map +1 -0
  118. package/dist/verify/fetch-with-deadline.js +55 -0
  119. package/dist/verify/fetch-with-deadline.js.map +1 -0
  120. package/dist/verify/get-verified-envelope.d.ts +23 -9
  121. package/dist/verify/get-verified-envelope.d.ts.map +1 -1
  122. package/dist/verify/get-verified-envelope.js +89 -40
  123. package/dist/verify/get-verified-envelope.js.map +1 -1
  124. package/dist/verify/resolve-kid.d.ts +3 -0
  125. package/dist/verify/resolve-kid.d.ts.map +1 -1
  126. package/dist/verify/resolve-kid.js +17 -8
  127. package/dist/verify/resolve-kid.js.map +1 -1
  128. package/dist/webcomponent/certrev-badge.d.ts.map +1 -1
  129. package/dist/webcomponent/certrev-badge.js +24 -2
  130. package/dist/webcomponent/certrev-badge.js.map +1 -1
  131. package/dist/webcomponent/render-badge-html.d.ts +1 -1
  132. package/dist/webcomponent/render-badge-html.d.ts.map +1 -1
  133. package/dist/webcomponent/render-badge-html.js +36 -17
  134. package/dist/webcomponent/render-badge-html.js.map +1 -1
  135. package/package.json +14 -2
  136. package/src/builder/cert-chrome-data.ts +26 -8
  137. package/src/builder/cert-review-card.tsx +147 -38
  138. package/src/builder/index.tsx +34 -17
  139. package/src/components/CertBadge.tsx +14 -9
  140. package/src/components/CertJsonLd.tsx +1 -1
  141. package/src/components/CertRevBacklink.tsx +10 -3
  142. package/src/components/CertReview.tsx +1 -1
  143. package/src/components/ExpertBio.tsx +12 -7
  144. package/src/components/escape.ts +37 -7
  145. package/src/components/format.ts +102 -6
  146. package/src/components/preview-block.ts +3 -3
  147. package/src/components/rel.ts +23 -0
  148. package/src/components/render-cert-block.ts +192 -109
  149. package/src/components/render-def.ts +14 -15
  150. package/src/index.ts +4 -2
  151. package/src/jsonld/project.ts +155 -59
  152. package/src/modal/avatar-resize.ts +14 -14
  153. package/src/modal/cert-modal-view.ts +52 -27
  154. package/src/modal/certrev-cert-modal.styles.ts +8 -8
  155. package/src/modal/certrev-cert-modal.ts +9 -9
  156. package/src/modal/certrev-cert.entry.ts +9 -7
  157. package/src/modal/display-strings.ts +26 -5
  158. package/src/modal/escape.ts +32 -4
  159. package/src/modal/font-face.ts +8 -11
  160. package/src/modal/ftc-disclosure.ts +2 -2
  161. package/src/modal/ftc-guard.ts +73 -18
  162. package/src/modal/index.ts +1 -1
  163. package/src/modal/interactions.ts +2 -2
  164. package/src/modal/logo-paths.ts +2 -2
  165. package/src/modal/placement.ts +7 -6
  166. package/src/modal/revalidate.ts +2 -3
  167. package/src/modal/verify-client.ts +2 -2
  168. package/src/verify/cache.ts +34 -8
  169. package/src/verify/fetch-with-deadline.ts +60 -0
  170. package/src/verify/get-verified-envelope.ts +131 -48
  171. package/src/verify/resolve-kid.ts +22 -6
  172. package/src/webcomponent/certrev-badge.ts +24 -2
  173. package/src/webcomponent/render-badge-html.ts +66 -19
  174. package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +0 -17
  175. package/src/__tests__/components.test.tsx +0 -206
  176. package/src/__tests__/preview-block.test.ts +0 -84
  177. package/src/__tests__/project.test.ts +0 -128
  178. package/src/__tests__/render-cert-block.test.ts +0 -657
  179. package/src/__tests__/render-def.test.tsx +0 -295
  180. package/src/__tests__/verify.test.ts +0 -239
  181. package/src/__tests__/webcomponent.test.tsx +0 -116
  182. package/src/builder/__tests__/builder.test.tsx +0 -158
  183. package/src/modal/cert-modal-view.test.ts +0 -175
  184. package/src/modal/certrev-cert-modal.test.ts +0 -125
  185. package/src/modal/ftc-guard.test.ts +0 -153
  186. package/src/modal/interactions.test.ts +0 -250
  187. package/src/modal/placement.test.ts +0 -398
  188. package/src/modal/revalidate.test.ts +0 -111
  189. package/src/modal/verify-client.test.ts +0 -164
@@ -1 +1 @@
1
- {"version":3,"file":"render-badge-html.d.ts","sourceRoot":"","sources":["../../src/webcomponent/render-badge-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAGN,KAAK,aAAa,EAGlB,MAAM,6BAA6B,CAAA;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;CAClC;AASD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,GAAE,kBAAuB,GAAG,MAAM,CAkG3F"}
1
+ {"version":3,"file":"render-badge-html.d.ts","sourceRoot":"","sources":["../../src/webcomponent/render-badge-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH,OAAO,EAGN,KAAK,aAAa,EAGlB,MAAM,6BAA6B,CAAA;AACpC,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;CAClC;AAwCD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,GAAE,kBAAuB,GAAG,MAAM,CA2G3F"}
@@ -12,9 +12,18 @@
12
12
  * the badge must be in the server HTML, never client-only.
13
13
  */
14
14
  import { escapeAttribute, escapeHtml, safeCssColor, safeHttpUrl } from '../components/escape.js';
15
- import { credentialSuffix, formatDate, resolveDisplay } from '../components/format.js';
15
+ import { credentialSuffix, dedupeCredential, expertNameWithCredentials, formatDate, resolveDisplay, } from '../components/format.js';
16
+ import { CERTREV_LINK_REL } from '../components/rel.js';
16
17
  import { CERT_SCOPE_LINE, CERT_STRINGS_VERSION, resolveRenderTheme, themeCssVarDeclarations, } from '../components/render-def.js';
17
18
  const C = 'certrev-badge';
19
+ /**
20
+ * The badge has exactly two faces, and this is the ONE place that decides which. Anything
21
+ * that is not the compact face renders the full face — so an unvalidated `badgeStyle` (a
22
+ * malformed envelope's number, an untyped caller's typo) can never reach the class name.
23
+ */
24
+ function badgeFace(style) {
25
+ return style === 'compact' ? 'compact' : 'full';
26
+ }
18
27
  function attr(name, value) {
19
28
  if (value == null || value === '')
20
29
  return '';
@@ -26,26 +35,31 @@ function attr(name, value) {
26
35
  * already decided to render by the time this is called).
27
36
  */
28
37
  export function renderBadgeHtml(payload, opts = {}) {
29
- const content = payload.content;
30
- const display = resolveDisplay(content.display, opts.accentColor, opts.renderDef);
38
+ const content = payload?.content ?? {};
39
+ const expert = content.expert ?? null;
40
+ const display = resolveDisplay(content.display ?? undefined, opts.accentColor, opts.renderDef);
31
41
  const theme = resolveRenderTheme(opts.renderDef);
32
- const style = opts.badgeStyle ?? display.badgeStyle;
42
+ const style = badgeFace(opts.badgeStyle ?? display.badgeStyle);
33
43
  const accent = safeCssColor(display.accentColor) ?? '#0f766e';
34
44
  const verifyUrl = safeHttpUrl(content.verifyUrl);
35
- const profileUrl = safeHttpUrl(content.expert.profileUrl);
36
- const photoUrl = style === 'full' && display.showExpertPhoto ? safeHttpUrl(content.expert.photoUrl) : null;
37
- const suffix = credentialSuffix(content);
45
+ const profileUrl = safeHttpUrl(expert?.profileUrl);
46
+ const photoUrl = style === 'full' && display.showExpertPhoto ? safeHttpUrl(expert?.photoUrl) : null;
47
+ // Same composition rule as <CertBadge>: the suffix drops a credential the display name
48
+ // already carries, so the two renderers never disagree about one payload.
49
+ const suffix = dedupeCredential(expert?.displayName, credentialSuffix(payload?.content));
38
50
  const certified = formatDate(content.certifiedAt);
39
51
  const updated = formatDate(content.contentModifiedAt);
40
- const name = `${escapeHtml(content.expert.displayName)}${suffix ? `<span class="${C}__credentials">, ${escapeHtml(suffix)}</span>` : ''}`;
52
+ const name = `${escapeHtml(expert?.displayName)}${suffix ? `<span class="${C}__credentials">, ${escapeHtml(suffix)}</span>` : ''}`;
41
53
  const nameNode = profileUrl
42
- ? `<a class="${C}__expert-link" href="${escapeAttribute(profileUrl)}" rel="noopener">${name}</a>`
54
+ ? `<a class="${C}__expert-link" href="${escapeAttribute(profileUrl)}" rel="${CERTREV_LINK_REL}">${name}</a>`
43
55
  : name;
44
56
  const mark = `<svg class="${C}__mark" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">` +
45
57
  `<circle cx="12" cy="12" r="11" fill="${escapeAttribute(accent)}"></circle>` +
46
58
  `<path d="M7 12.5l3.2 3.2L17 9" stroke="#ffffff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;
59
+ // alt="" is deliberate: the byline below renders the same name as text, so alt text would make a
60
+ // screen reader announce it twice. Kept in lockstep with <CertBadge> and the modal avatar.
47
61
  const photo = photoUrl
48
- ? `<img class="${C}__photo" src="${escapeAttribute(photoUrl)}" alt="Photo of ${escapeAttribute(content.expert.displayName)}" width="40" height="40" loading="lazy" decoding="async" />`
62
+ ? `<img class="${C}__photo" src="${escapeAttribute(photoUrl)}" alt="" width="40" height="40" loading="lazy" decoding="async" />`
49
63
  : '';
50
64
  const header = `<div class="${C}__header">${mark}${photo}` +
51
65
  `<div class="${C}__heading">` +
@@ -57,7 +71,7 @@ export function renderBadgeHtml(payload, opts = {}) {
57
71
  if (display.showMemo && content.memo) {
58
72
  body += `<p class="${C}__memo">${escapeHtml(content.memo)}</p>`;
59
73
  }
60
- if (display.showAuthor && content.author.name && content.author.name !== content.expert.displayName) {
74
+ if (display.showAuthor && content.author?.name && content.author.name !== expert?.displayName) {
61
75
  const title = content.author.title ? `, ${escapeHtml(content.author.title)}` : '';
62
76
  body += `<p class="${C}__author">Written by ${escapeHtml(content.author.name)}${title}</p>`;
63
77
  }
@@ -79,17 +93,19 @@ export function renderBadgeHtml(payload, opts = {}) {
79
93
  // byte-verbatim COMPENSATED_EXPERT_CUE. The scope line is byte-verbatim
80
94
  // CERT_SCOPE_LINE (its curly apostrophe U+2019 survives escapeHtml untouched).
81
95
  const cueInner = profileUrl
82
- ? `Compensated <a class="${C}__cue-link" href="${escapeAttribute(profileUrl)}" rel="noopener">expert</a>`
96
+ ? `Compensated <a class="${C}__cue-link" href="${escapeAttribute(profileUrl)}" rel="${CERTREV_LINK_REL}">expert</a>`
83
97
  : 'Compensated expert';
84
98
  const disclosure = `<div class="${C}__disclosure">` +
85
99
  `<span class="${C}__compensated-cue">${cueInner}</span>` +
86
100
  `<p class="${C}__ftc" data-ftc-disclosure><span data-ftc-line>${escapeHtml(CERT_SCOPE_LINE)}</span></p>` +
87
101
  `</div>`;
88
102
  const verify = verifyUrl
89
- ? `<a class="${C}__verify" href="${escapeAttribute(verifyUrl)}" rel="noopener" aria-label="Verify this certification on CertREV">Verify on CertREV</a>`
103
+ ? `<a class="${C}__verify" href="${escapeAttribute(verifyUrl)}" rel="${CERTREV_LINK_REL}" aria-label="Verify this certification on CertREV">Verify on CertREV</a>`
90
104
  : '';
91
- const ariaLabel = `Content reviewed by ${content.expert.displayName}${suffix ? `, ${suffix}` : ''}`;
92
- // Accent LEADS so the un-themed output is byte-identical to the pre-WS6 single-var
105
+ // Same derivation <CertBadge> labels with, so the two renderers cannot drift on the one
106
+ // string a screen reader actually announces.
107
+ const ariaLabel = `Content reviewed by ${expertNameWithCredentials(payload?.content)}`;
108
+ // Accent LEADS so the un-themed output is byte-identical to the older single-var
93
109
  // form. A THEMED brand appends surface/radius/font: the data-bearing tokens ride
94
110
  // escaped (a no-op for validated hex/length — defense in depth), the font is a
95
111
  // controlled literal stack emitted raw. The whole string is NOT re-escaped
@@ -99,8 +115,11 @@ export function renderBadgeHtml(payload, opts = {}) {
99
115
  for (const [name, value] of themeCssVarDeclarations(theme)) {
100
116
  rootStyle += name === '--certrev-font' ? `;${name}:${value}` : `;${name}:${escapeAttribute(value)}`;
101
117
  }
102
- return (`<section class="${C} ${C}--${escapeAttribute(style)}" style="${rootStyle}"` +
103
- attr('data-certrev-cert-id', payload.certId) +
118
+ return (
119
+ // `style` is one of two literals by construction (`badgeFace`), so the class name needs
120
+ // no escaping — there is no path by which caller data reaches it.
121
+ `<section class="${C} ${C}--${style}" style="${rootStyle}"` +
122
+ attr('data-certrev-cert-id', payload?.certId) +
104
123
  ` data-certrev-strings-version="${CERT_STRINGS_VERSION}"` +
105
124
  attr('data-certrev-def-version', theme.version != null ? String(theme.version) : null) +
106
125
  ` aria-label="${escapeAttribute(ariaLabel)}">${header}${body}${disclosure}${verify}</section>`);
@@ -1 +1 @@
1
- {"version":3,"file":"render-badge-html.js","sourceRoot":"","sources":["../../src/webcomponent/render-badge-html.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"render-badge-html.js","sourceRoot":"","sources":["../../src/webcomponent/render-badge-html.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "@certrev/cert-block",
3
- "version": "0.5.4",
4
- "description": "Headless / crypto_verify render edge for the CertREV CertDeliveryEnvelope. SSR-safe React components (<CertBadge>/<ExpertBio>/<CertRevBacklink>), a deterministic schema.org JSON-LD projector, a fail-closed verify layer over the shared VerdictKernel, and a framework-agnostic <certrev-badge> Web Component. Sign once (portal), render everywhere (Hydrogen / Next / Builder / universal embed).",
3
+ "version": "1.0.0",
4
+ "description": "Headless render edge for the CertREV CertDeliveryEnvelope: SSR-safe React cert components, a framework-agnostic <certrev-badge> Web Component, a schema.org JSON-LD projector, and a fail-closed verify layer over the shared VerdictKernel.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/CertREV/cert-kit.git",
8
+ "directory": "packages/cert-block"
9
+ },
10
+ "homepage": "https://github.com/CertREV/cert-kit/tree/HEAD/packages/cert-block#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/CertREV/cert-kit/issues"
13
+ },
5
14
  "type": "module",
6
15
  "main": "./dist/index.js",
7
16
  "types": "./dist/index.d.ts",
@@ -35,6 +44,9 @@
35
44
  "files": [
36
45
  "dist",
37
46
  "src",
47
+ "!src/**/__tests__",
48
+ "!src/**/*.test.ts",
49
+ "!src/**/*.test.tsx",
38
50
  "README.md",
39
51
  "CHANGELOG.md"
40
52
  ],
@@ -3,14 +3,14 @@
3
3
  * `BuilderCertChromeData` — the CANONICAL headless cert-chrome wire type
4
4
  * ─────────────────────────────────────────────────────────────────────────────
5
5
  *
6
- * The brand-render projection of the cert envelope (portal POR-10091) — the OPTIONS
6
+ * The brand-render projection of the cert envelope — the OPTIONS
7
7
  * of a brand's native cert component (the registered `CertReviewCard`) on
8
8
  * a headless blocks-mode entry (Builder.io). CertREV serves the data + the ready-made
9
9
  * strings; the brand renders it within the design-guide ladder (enforced live by the
10
10
  * crawl monitor + the MSA anti-stripping teeth).
11
11
  *
12
- * SINGLE SOURCE (POR-10721 W1): this type is HOMED HERE so the portal exporter
13
- * (`src/lib/builder/cert-chrome.ts`) and the kit component (`CertReviewCard`) +
12
+ * SINGLE SOURCE: this type is HOMED HERE so the portal's cert-chrome exporter,
13
+ * the kit component (`CertReviewCard`) and
14
14
  * `BUILDER_REGISTRATION.inputs` all reference ONE definition — killing the drift the
15
15
  * hand-rolled Hydrogen-replica component suffered (it carried a stale separate
16
16
  * `credential`/`credentialVerifiedAt` shape from before the fused-pair projection).
@@ -49,7 +49,12 @@ export interface CertChromeRenderDef {
49
49
  readonly barInkFg?: '#141414' | '#ffffff'
50
50
  /** The subtractive field-visibility hide-set. */
51
51
  readonly hidden: readonly string[]
52
- /** v2 layout axis. Omitted ⇒ 'banner'. */
52
+ /**
53
+ * v2 layout axis. This is the DEF's own axis (what the brand baked), NOT the placement the
54
+ * component renders — the entry/editor picks that via `mode`, and its default lives in
55
+ * `renderCertBlock` alone (`sidebar`). This comment used to claim the default was 'banner',
56
+ * which made it the third place a placement default was written down and the second one wrong.
57
+ */
53
58
  readonly layout?: string
54
59
  /** Custom-face composition — present only when layout === 'custom'. */
55
60
  readonly customFace?: { readonly placedFields: readonly string[] }
@@ -86,9 +91,22 @@ export interface BuilderCertChromeData {
86
91
  background: string | null
87
92
  /** The QA-SCREENED `verifiedExpertMemo` (never the raw in-flight `expertMemo`), or null. */
88
93
  memo: string | null
89
- /** The compensation cue (`Compensated expert`). Null for a pro-bono reviewer. */
94
+ /**
95
+ * The compensation cue (`Compensated expert`). Null for a pro-bono reviewer.
96
+ *
97
+ * COPY IS NOT CARRIED BY THIS FIELD — only the BOOLEAN it implies is. The face renders the
98
+ * locked `COMPENSATED_EXPERT_CUE` constant when the cue is present and NOTHING when it is
99
+ * null; a rewritten string here changes no pixel. Kept on the wire type because the
100
+ * pro-bono `null` is real signal, and because a brand's exporter payload already carries it.
101
+ */
90
102
  compensationCue: string | null
91
- /** The verbatim FTC scope disclaimer. Always present. */
103
+ /**
104
+ * The verbatim FTC scope disclaimer. Always present.
105
+ *
106
+ * Delivered for the brand's own records / non-CertREV surfaces. The face does NOT read it:
107
+ * it renders the locked `CERT_SCOPE_LINE` constant, whose curly apostrophe is pinned to
108
+ * MSA §3, so an edited copy here can never reach a rendered disclosure.
109
+ */
92
110
  scopeLine: string
93
111
  /** Reviewer photo URL (cert-frozen override ?? live), or null. */
94
112
  reviewerPhoto: string | null
@@ -117,8 +135,8 @@ export interface BuilderCertChromeData {
117
135
 
118
136
  /**
119
137
  * The ordered keys of `BuilderCertChromeData` — the SINGLE SOURCE the Builder
120
- * `inputs` array and the W3 exporter⇄inputs⇄wire-type parity lock derive from, so
121
- * the three can never silently drift again (POR-10721 W3). Adding a field to the
138
+ * `inputs` array and the exporter⇄inputs⇄wire-type parity lock derive from, so
139
+ * the three can never silently drift again. Adding a field to the
122
140
  * interface without adding it here (or vice-versa) fails the `keyof` exhaustiveness
123
141
  * check in `assertChromeKeysExhaustive` below.
124
142
  */
@@ -8,7 +8,7 @@
8
8
  * `BuilderCertChromeData` projection — so the visible cert chrome renders itself,
9
9
  * design-identical to the Shopify Liquid + WordPress PHP faces (same `renderCertBlock`
10
10
  * string renderer). This REPLACES the ~200-line hand-rolled component every headless
11
- * brand copied from the demo (POR-10721 W1).
11
+ * brand copied from the demo.
12
12
  *
13
13
  * Improvements over the canary demo's hand-rolled version:
14
14
  * - consumes the CANONICAL `BuilderCertChromeData` (the fused `credentialVerification`
@@ -31,11 +31,15 @@ import { BUILDER_CERT_CHROME_KEYS } from './cert-chrome-data.js'
31
31
  export const CERT_COMPONENT_NAME = 'CertREV Cert'
32
32
 
33
33
  export interface CertReviewCardProps extends Partial<BuilderCertChromeData> {
34
- /** Placement face — the ENTRY/editor chooses this, NOT the exporter (not a wire-type field). */
34
+ /**
35
+ * Placement face — the ENTRY/editor chooses this, NOT the exporter (not a wire-type field).
36
+ * Absent ⇒ whatever `renderCertBlock` defaults to (`sidebar`); this component names no
37
+ * fallback of its own, so there is exactly ONE place the default is written down.
38
+ */
35
39
  mode?: CertBlockMode | null
36
40
  /**
37
41
  * Banner memo placement (not a wire-type field — the ENTRY/editor chooses it). Passed
38
- * straight through to `renderCertBlock`'s first-class `part` (POR-10741): `'full'`
42
+ * straight through to `renderCertBlock`'s first-class `part`: `'full'`
39
43
  * (default) renders header + memo in one block; `'header'`/`'memo'` render only that
40
44
  * card so the memo can be placed as its own block under the article body. The old
41
45
  * string-splitting shim is gone — `renderCertBlock` composes each part directly.
@@ -43,59 +47,106 @@ export interface CertReviewCardProps extends Partial<BuilderCertChromeData> {
43
47
  part?: 'full' | 'header' | 'memo' | null
44
48
  }
45
49
 
50
+ /**
51
+ * Coerce ONE untyped option to renderable text.
52
+ *
53
+ * `CertReviewCardProps` is erased at runtime and these options are authored in a VISUAL
54
+ * EDITOR — a number, an object or an array can sit in any field, and `??` does nothing about
55
+ * it (`props.memo ?? ''` forwards the number 42 happily). `renderCertBlock` reads its facts as
56
+ * strings (`.trim()`, `.split()`), so a mistyped option threw INSIDE the SSR render; under
57
+ * `renderToReadableStream` that aborts the whole article route with zero HTML. A value that is
58
+ * not text is not copy, so it renders as nothing.
59
+ */
60
+ function text(value: unknown): string {
61
+ return typeof value === 'string' ? value : ''
62
+ }
63
+
64
+ /** The same rule where ABSENT and EMPTY are different renders (`authorTitle`, `bio`). */
65
+ function optionalText(value: unknown): string | undefined {
66
+ return text(value) || undefined
67
+ }
68
+
69
+ /**
70
+ * The fused credential pair — accepted ONLY when both halves are present AND both are strings.
71
+ *
72
+ * The R1 gate ("a credential never displays without its dated verification") is THIS FUNCTION,
73
+ * not the type: the pair arrives as untyped CMS JSON, and the old truthiness check keyed the
74
+ * whole pair off the OBJECT, so `{credential:'MD'}` forwarded `verifiedAt: undefined` into the
75
+ * renderer and crashed. Anything malformed — a half pair, a bare string, an array, a number
76
+ * where a date belongs — degrades to the name-only face, which is the honest render of a
77
+ * delivery whose verification cannot be read.
78
+ */
79
+ function fusedCredential(pair: BuilderCertChromeData['credentialVerification'] | undefined) {
80
+ if (!pair || typeof pair !== 'object') return null
81
+ const credential = text(pair.credential)
82
+ const verifiedAt = text(pair.verifiedAt)
83
+ return credential && verifiedAt ? { credential, verifiedAt } : null
84
+ }
85
+
46
86
  export function CertReviewCard(props: CertReviewCardProps) {
47
- const { reviewerName, verifyUrl } = props
48
- if (!reviewerName || !verifyUrl) return null // no chrome pre-cert
87
+ const reviewerName = text(props.reviewerName)
88
+ const verifyUrl = text(props.verifyUrl)
89
+ if (!reviewerName || !verifyUrl) return null // no chrome pre-cert (or an unreadable delivery)
49
90
 
50
- // The credential + its verification date arrive ONLY as the fused pair, so we can
51
- // never obtain one without the other (name-only face when the pair is null, or when
52
- // the brand set showCredentials:false). This IS the R1 gate, enforced by the type.
53
- const cv = props.credentialVerification ?? null
91
+ // Name-only face when the pair is unreadable, or when the brand set showCredentials:false.
54
92
  const showCred = props.display?.showCredentials !== false
55
- const withCred = cv && showCred ? cv : null
93
+ const withCred = showCred ? fusedCredential(props.credentialVerification) : null
56
94
 
57
95
  const tokens = props.renderDef?.tokens
58
96
  const theme: Partial<ResolvedBlockTheme> | undefined = tokens
59
97
  ? {
60
- accentColor: tokens.accentColor,
61
- surface: tokens.surface,
62
- cornerRadius: tokens.cornerRadius,
63
- fontSlot: tokens.fontSlot as ResolvedBlockTheme['fontSlot'] | undefined,
98
+ accentColor: optionalText(tokens.accentColor),
99
+ surface: optionalText(tokens.surface),
100
+ cornerRadius: optionalText(tokens.cornerRadius),
101
+ fontSlot: optionalText(tokens.fontSlot) as ResolvedBlockTheme['fontSlot'] | undefined,
64
102
  // v2 brand-ink (post-demo): the bar + free-content ink follow the def.
65
- barInk: tokens.barInk,
66
- inkColor: tokens.inkColor,
103
+ barInk: optionalText(tokens.barInk),
104
+ inkColor: optionalText(tokens.inkColor),
67
105
  }
68
106
  : undefined
69
107
 
70
108
  const html = renderCertBlock({
71
- mode: (props.mode as CertBlockMode) ?? 'banner',
109
+ // NO fallback here, deliberately: `renderCertBlock` owns the ONE placement default
110
+ // (absent or unrecognised ⇒ sidebar). This line used to say `?? 'banner'`, and it was
111
+ // one of THREE layers each naming a default — here, the registered input's helper text,
112
+ // and the wire type's own doc comment — while the renderer, the only place that actually
113
+ // paints a face, named none. Three copies of a default is three chances to disagree.
114
+ mode: props.mode ?? undefined,
72
115
  theme,
73
- // First-class banner memo split (POR-10741) — renderCertBlock composes the requested
74
- // part directly; no string-splitting. Ignored outside banner mode.
116
+ // First-class banner memo split — renderCertBlock composes the requested
117
+ // part directly; no string-splitting. Ignored outside banner mode, and an unrecognised
118
+ // value is normalised to 'full' there, same as `mode` — not here.
75
119
  ...(props.part ? { part: props.part } : {}),
76
120
  facts: {
77
- authorName: props.authorName ?? 'Editorial',
78
- authorTitle: props.authorTitle ?? undefined,
121
+ authorName: optionalText(props.authorName) ?? 'Editorial',
122
+ authorTitle: optionalText(props.authorTitle),
79
123
  reviewerName,
80
- credential: withCred ? withCred.credential : '',
81
- credentialVerifiedAt: withCred ? withCred.verifiedAt : '',
82
- certifiedAt: props.certifiedAt ?? '',
83
- memo: props.memo ?? '',
84
- bio: props.bio ?? undefined,
85
- profileUrl: props.reviewerProfileUrl ?? '',
124
+ credential: withCred?.credential ?? '',
125
+ credentialVerifiedAt: withCred?.verifiedAt ?? '',
126
+ certifiedAt: text(props.certifiedAt),
127
+ memo: text(props.memo),
128
+ bio: optionalText(props.bio),
129
+ profileUrl: text(props.reviewerProfileUrl),
86
130
  certificateUrl: verifyUrl,
87
- // Locked strings default byte-verbatim inside renderCertBlock; pass the
88
- // delivered cue/scope through so a pro-bono (null-cue) delivery is honored.
89
- ...(props.compensationCue ? { compensationCue: props.compensationCue } : {}),
90
- ...(props.scopeLine ? { scopeLine: props.scopeLine } : {}),
131
+ // The compliance disclosures are CONSTANTS the renderer owns — a delivered cue is
132
+ // IGNORED (an editor cannot retype the FTC copy into something friendlier), and the
133
+ // scope line is not forwarded at all. The ONE thing honored is an explicit `null`:
134
+ // the pro-bono reviewer has no compensation to disclose, so the cue is omitted
135
+ // rather than replaced by the compensated constant (the modal already gets this
136
+ // right — a card claiming "Compensated expert" over a volunteer is an FTC-facing
137
+ // falsehood, not a cosmetic default).
138
+ compensationCue: props.compensationCue === null ? null : undefined,
91
139
  },
92
140
  })
93
141
 
94
142
  return (
95
143
  <div
96
144
  data-certrev-cert-chrome=""
97
- data-certrev-strings-version={props.stringsVersion ?? undefined}
98
- data-certrev-def-version={props.renderDef?.v ?? undefined}
145
+ // The two provenance stamps the crawl monitor reads — emitted ONLY when the delivery
146
+ // carried the right type, so a mistyped option leaves the attribute off instead of
147
+ // stamping "[object Object]" as a version.
148
+ data-certrev-strings-version={optionalText(props.stringsVersion)}
149
+ data-certrev-def-version={typeof props.renderDef?.v === 'number' ? props.renderDef.v : undefined}
99
150
  // biome-ignore lint/security/noDangerouslySetInnerHtml: renderCertBlock is the escaping-safe SSR string renderer
100
151
  dangerouslySetInnerHTML={{ __html: html }}
101
152
  />
@@ -106,12 +157,28 @@ export function CertReviewCard(props: CertReviewCardProps) {
106
157
  // Builder.io registration — inputs SINGLE-SOURCED from the wire-type keys (W1/W3)
107
158
  // ─────────────────────────────────────────────────────────────────────────────
108
159
 
109
- /** The subset of the Builder.io input schema this kit emits. */
160
+ /**
161
+ * The subset of the Builder.io input schema this kit emits. Every field below is verified
162
+ * against `@builder.io/sdk-react`'s own `Input` declaration (`types/types/input.d.ts`, 5.2.7) —
163
+ * a field the SDK ignores would be worse than none, because it READS as configured.
164
+ */
110
165
  export interface BuilderInput {
111
166
  readonly name: string
112
167
  readonly type: 'string' | 'longText' | 'object' | 'boolean' | 'number'
113
168
  readonly friendlyName?: string
114
169
  readonly helperText?: string
170
+ /** The value Builder pre-fills a freshly-dropped block with (`Input.defaultValue`). */
171
+ readonly defaultValue?: string
172
+ /**
173
+ * Turns a `string` input into a DROPDOWN of exactly these choices (`Input.enum`). The SDK
174
+ * accepts bare strings or `{label, value}` pairs; the kit uses pairs so the editor reads a
175
+ * capitalised label while the component still receives its lowercase mode/part literal.
176
+ * The ARRAY is deliberately mutable (the entries are not): the SDK types `Input.enum` as a
177
+ * mutable array, and a `readonly` one here makes the whole registration un-assignable to
178
+ * `RegisteredComponent` — the same reason `inputs` itself is mutable below (verified by
179
+ * compiling this shape against the installed SDK's `RegisteredComponent`).
180
+ */
181
+ readonly enum?: { readonly label: string; readonly value: string }[]
115
182
  /** Tucks the input under the editor's "Advanced" reveal (Builder's `Input.advanced`). */
116
183
  readonly advanced?: boolean
117
184
  }
@@ -139,6 +206,23 @@ const WIRE_INPUT_TYPE: Record<(typeof BUILDER_CERT_CHROME_KEYS)[number], Builder
139
206
  stringsVersion: 'string',
140
207
  }
141
208
 
209
+ /**
210
+ * The COMPLIANCE-COPY inputs, called what they are.
211
+ *
212
+ * `advanced: true` is only a UI fold, so an editor who opens "Advanced" sees these two beside
213
+ * the rest and reads them as theirs to type in. They are NOT: the renderer paints the locked
214
+ * `COMPENSATED_EXPERT_CUE` / `CERT_SCOPE_LINE` constants and ignores whatever is here. They
215
+ * stay REGISTERED because they are wire-type keys — dropping them would change the contract
216
+ * the portal exporter writes, and would strand the `null` that signals a pro-bono reviewer.
217
+ * So the fix is truthful labelling, not removal.
218
+ */
219
+ const WIRE_INPUT_HELPER_TEXT: Partial<Record<(typeof BUILDER_CERT_CHROME_KEYS)[number], string>> = {
220
+ compensationCue:
221
+ 'System-populated by CertREV. Only its PRESENCE is read (absent = a pro-bono reviewer, so no cue renders); the cue itself is a locked constant, so editing this text changes nothing on the page — and clearing it does not remove the disclosure.',
222
+ scopeLine:
223
+ 'System-populated by CertREV. The FTC scope line renders byte-verbatim from a locked constant — editing this text changes nothing on the page.',
224
+ }
225
+
142
226
  /**
143
227
  * The wire-type inputs — DERIVED from `BUILDER_CERT_CHROME_KEYS`, so the 19-field
144
228
  * option set can never drift from the type (the W3 lock asserts name-parity). This
@@ -147,20 +231,45 @@ const WIRE_INPUT_TYPE: Record<(typeof BUILDER_CERT_CHROME_KEYS)[number], Builder
147
231
  export const WIRE_INPUTS: readonly BuilderInput[] = BUILDER_CERT_CHROME_KEYS.map((name) => ({
148
232
  name,
149
233
  type: WIRE_INPUT_TYPE[name],
234
+ ...(WIRE_INPUT_HELPER_TEXT[name] ? { helperText: WIRE_INPUT_HELPER_TEXT[name] } : {}),
150
235
  // Exporter-populated wire fields: collapsed under the editor's "Advanced" reveal so the
151
236
  // Options tab leads with the two placement choices an editor actually makes (mode/part).
152
237
  advanced: true,
153
238
  }))
154
239
 
155
- /** Placement inputs — chosen by the entry/editor, NOT emitted by the exporter (not wire-type keys). */
240
+ /**
241
+ * Placement inputs — chosen by the entry/editor, NOT emitted by the exporter (not wire-type keys).
242
+ *
243
+ * `defaultValue` + `enum` are what make these two REAL choices in the editor: a dropdown of the
244
+ * faces that exist, pre-filled with the same default the renderer applies. They were free-text
245
+ * with no default before, so the only description of the default was the helper text — which had
246
+ * drifted from the code (it advertised `banner`; the renderer's default is `sidebar`).
247
+ */
156
248
  export const PLACEMENT_INPUTS: readonly BuilderInput[] = [
157
249
  {
158
250
  name: 'mode',
159
251
  type: 'string',
160
252
  friendlyName: 'Placement',
161
- helperText: 'banner (default) · sidebar · floating',
253
+ helperText: 'sidebar (default) · banner · floating',
254
+ defaultValue: 'sidebar',
255
+ enum: [
256
+ { label: 'Sidebar', value: 'sidebar' },
257
+ { label: 'Banner', value: 'banner' },
258
+ { label: 'Floating', value: 'floating' },
259
+ ],
260
+ },
261
+ {
262
+ name: 'part',
263
+ type: 'string',
264
+ friendlyName: 'Banner part',
265
+ helperText: 'full (default) · header · memo — banner placement only',
266
+ defaultValue: 'full',
267
+ enum: [
268
+ { label: 'Full (header + memo)', value: 'full' },
269
+ { label: 'Header only', value: 'header' },
270
+ { label: 'Memo only', value: 'memo' },
271
+ ],
162
272
  },
163
- { name: 'part', type: 'string', friendlyName: 'Banner part', helperText: 'full (default) · header · memo' },
164
273
  ]
165
274
 
166
275
  /** A Builder.io custom-component registration (the general shape; the anchor uses the same). */
@@ -207,6 +316,6 @@ export const certRevCertComponent: CertReviewCardRegisteredComponent = {
207
316
  name: CERT_COMPONENT_NAME,
208
317
  component: CertReviewCard,
209
318
  description:
210
- 'CertREV expert-review cert card. Options are populated by the CertREV exporter — editors choose placement (mode/part) only.',
319
+ 'CertREV expert-review cert card. Options are populated by the CertREV exporter; editors choose placement (mode/part) only.',
211
320
  inputs: [...PLACEMENT_INPUTS, ...WIRE_INPUTS],
212
321
  }
@@ -1,25 +1,42 @@
1
1
  /**
2
2
  * @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
3
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:
4
+ * A visual page builder must NOT choose WHICH credential renders — that invites cross-article
5
+ * misuse and re-opens the wrong-subject surface. The ANCHOR path splits the two concerns:
7
6
  * • the CMS controls LAYOUT — an editor drags a ZERO-INPUT <CertRevAnchor/> to position the
8
7
  * badge on the page;
9
8
  * • 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.
9
+ * own URL, crypto-verifies it (fail-closed VerdictKernel), and supplies the verdict via
10
+ * context.
12
11
  * The anchor renders the current page's verified badge or NOTHING. It cannot be pointed at
13
12
  * another article, and there is no `placementId` input to forge or mis-select.
14
13
  *
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.)
14
+ * THE PUSH KIT DOES NOT INHERIT THAT PROPERTY, and this module exports it too. `CertReviewCard` /
15
+ * `certRevCertComponent` register the 19 wire-type fields — `reviewerName`,
16
+ * `credentialVerification`, `verifyUrl`, `memo` — as Builder INPUTS, and the card renders what
17
+ * those inputs hold; it runs no signature check and consults no verdict. `advanced: true` folds
18
+ * them out of the editor's default view, which is a UI reveal and not a lock, and `safeHttpUrl`
19
+ * bounds `verifyUrl` to the http(s) SCHEME rather than to a CertREV origin. So to the card a
20
+ * hand-typed option and an exporter-written one are the same string: the trust boundary on the
21
+ * PUSH path is WRITE ACCESS TO THE CMS SPACE, not a signature, and nothing in this package
22
+ * narrows it. See "PUSH trust model" in the package README for the full statement — including
23
+ * what a revocation upstream does and does not reach.
18
24
  *
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).
25
+ * Three things the card DOES enforce, and they are the limit of it: no chrome at all without a
26
+ * `reviewerName` and a `verifyUrl`; a credential renders only welded to its dated verification
27
+ * (the fused pair); and the FTC disclosures render from locked constants, so rewriting the
28
+ * `compensationCue` / `scopeLine` options changes no pixel. Use the anchor when the page itself
29
+ * must not be able to state a credential.
30
+ *
31
+ * JSON-LD is DECOUPLED: the anchor renders the BADGE only (`omitJsonLd`). The article template
32
+ * emits the schema.org JSON-LD server-side from the same verified verdict, so a page builder
33
+ * can't duplicate, move, or omit structured data. See "The article route example" in the package
34
+ * README for the route both halves meet in.
35
+ *
36
+ * EDGE-CACHE DISCIPLINE: the consumer's loader MUST verify per request as a dynamic edge
37
+ * subrequest and must NOT long-cache the rendered badge HTML — a revoked/expired credential
38
+ * sitting in a stale edge cache would defeat fail-closed. Bound any positive-verdict cache by
39
+ * min(expiry, revocation-TTL, content-version).
23
40
  *
24
41
  * cert-block carries no Builder dependency: the registration shape is declared structurally, and
25
42
  * `isEditing` is a passed-in flag (the consumer supplies Builder's `isPreviewing()`).
@@ -28,7 +45,7 @@ import { createContext, type ReactNode, useContext } from 'react'
28
45
  import { CertReview } from '../components/CertReview.js'
29
46
  import type { CertVerdict } from '../contract/kernel.js'
30
47
 
31
- /** The current page's credential, resolved + verified by the loader (Track-1 delivery). */
48
+ /** The current page's credential, resolved + verified by the consumer's loader. */
32
49
  export interface CurrentCredential {
33
50
  readonly verdict: CertVerdict
34
51
  /** Canonical page URL for JSON-LD @id alignment (used by the server-side JSON-LD, not the anchor). */
@@ -87,8 +104,8 @@ export function CertRevEditorPlaceholder() {
87
104
  /**
88
105
  * The Builder block. ZERO inputs — a layout anchor only. Renders the current page's VERIFIED
89
106
  * 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.
107
+ * NOTHING in production (fail-closed). The credential is whatever the loader delivered + verified
108
+ * for THIS page; the editor can neither choose nor forge it.
92
109
  */
93
110
  export function CertRevAnchor() {
94
111
  const { current, isEditing } = useContext(CertRevContext)
@@ -121,7 +138,7 @@ export const certRevAnchorComponent: CertRevBuilderRegistration = {
121
138
  inputs: [],
122
139
  }
123
140
 
124
- // ── POR-10721 W1/W3: the Builder.io PUSH kit (CertReviewCard) + single-sourced wire type ──
141
+ // ── The Builder.io PUSH kit (CertReviewCard) + the single-sourced wire type ──
125
142
  export {
126
143
  BUILDER_CERT_CHROME_KEYS,
127
144
  type BuilderCertChromeData,
@@ -133,10 +150,10 @@ export {
133
150
  type BuilderInput,
134
151
  CERT_COMPONENT_NAME,
135
152
  type CertBlockBuilderRegistration,
136
- certRevCertComponent,
137
153
  CertReviewCard,
138
154
  type CertReviewCardProps,
139
155
  type CertReviewCardRegisteredComponent,
156
+ certRevCertComponent,
140
157
  PLACEMENT_INPUTS,
141
158
  WIRE_INPUTS,
142
159
  } from './cert-review-card.js'