@certrev/cert-block 1.0.2 → 1.1.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 (53) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +6 -1
  3. package/dist/builder/cert-chrome-data.d.ts +26 -1
  4. package/dist/builder/cert-chrome-data.d.ts.map +1 -1
  5. package/dist/builder/cert-chrome-data.js +1 -0
  6. package/dist/builder/cert-chrome-data.js.map +1 -1
  7. package/dist/builder/cert-review-card.d.ts +41 -4
  8. package/dist/builder/cert-review-card.d.ts.map +1 -1
  9. package/dist/builder/cert-review-card.js +133 -7
  10. package/dist/builder/cert-review-card.js.map +1 -1
  11. package/dist/builder/delivery-runtime.d.ts +23 -0
  12. package/dist/builder/delivery-runtime.d.ts.map +1 -0
  13. package/dist/builder/delivery-runtime.js +38 -0
  14. package/dist/builder/delivery-runtime.js.map +1 -0
  15. package/dist/builder/envelope-face.d.ts +63 -0
  16. package/dist/builder/envelope-face.d.ts.map +1 -0
  17. package/dist/builder/envelope-face.js +151 -0
  18. package/dist/builder/envelope-face.js.map +1 -0
  19. package/dist/builder/index.d.ts +15 -3
  20. package/dist/builder/index.d.ts.map +1 -1
  21. package/dist/builder/index.js +15 -3
  22. package/dist/builder/index.js.map +1 -1
  23. package/dist/builder/issuer-key.d.ts +12 -0
  24. package/dist/builder/issuer-key.d.ts.map +1 -0
  25. package/dist/builder/issuer-key.js +14 -0
  26. package/dist/builder/issuer-key.js.map +1 -0
  27. package/dist/components/memo-toggle.d.ts +42 -0
  28. package/dist/components/memo-toggle.d.ts.map +1 -0
  29. package/dist/components/memo-toggle.js +60 -0
  30. package/dist/components/memo-toggle.js.map +1 -0
  31. package/dist/components/render-cert-block.d.ts +9 -0
  32. package/dist/components/render-cert-block.d.ts.map +1 -1
  33. package/dist/components/render-cert-block.js +80 -19
  34. package/dist/components/render-cert-block.js.map +1 -1
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +3 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/verify/get-verified-delivery.d.ts +91 -0
  40. package/dist/verify/get-verified-delivery.d.ts.map +1 -0
  41. package/dist/verify/get-verified-delivery.js +157 -0
  42. package/dist/verify/get-verified-delivery.js.map +1 -0
  43. package/package.json +5 -1
  44. package/src/builder/cert-chrome-data.ts +27 -0
  45. package/src/builder/cert-review-card.tsx +170 -10
  46. package/src/builder/delivery-runtime.ts +54 -0
  47. package/src/builder/envelope-face.ts +173 -0
  48. package/src/builder/index.tsx +24 -2
  49. package/src/builder/issuer-key.ts +13 -0
  50. package/src/components/memo-toggle.ts +63 -0
  51. package/src/components/render-cert-block.ts +121 -22
  52. package/src/index.ts +28 -0
  53. package/src/verify/get-verified-delivery.ts +211 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-verified-delivery.js","sourceRoot":"","sources":["../../src/verify/get-verified-delivery.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certrev/cert-block",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
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
5
  "repository": {
6
6
  "type": "git",
@@ -23,6 +23,10 @@
23
23
  "types": "./dist/builder/index.d.ts",
24
24
  "default": "./dist/builder/index.js"
25
25
  },
26
+ "./builder/face": {
27
+ "types": "./dist/builder/envelope-face.d.ts",
28
+ "default": "./dist/builder/envelope-face.js"
29
+ },
26
30
  "./webcomponent": {
27
31
  "types": "./dist/webcomponent/certrev-badge.d.ts",
28
32
  "default": "./dist/webcomponent/certrev-badge.js"
@@ -60,6 +60,25 @@ export interface CertChromeRenderDef {
60
60
  readonly customFace?: { readonly placedFields: readonly string[] }
61
61
  }
62
62
 
63
+ /**
64
+ * The DELIVERY MARKER (1.1.0): the exporter's statement that this block's cert is served,
65
+ * signed, by the CertREV Delivery API at `baseUrl`. When it is present the card no longer
66
+ * paints the other option keys: it takes its `externalId` from its own Builder entry id,
67
+ * fetches `GET {baseUrl}/api/cert/v1/delivery/builder/{externalId}`, verifies the envelope
68
+ * against the baked issuer key, and paints from the verified facts plus the brand's render
69
+ * def served beside them. A revoked, expired or unverifiable cert renders nothing.
70
+ *
71
+ * `baseUrl` is honoured ONLY when it is `https:` on `certrev.com` or a subdomain of it. The
72
+ * marker is an option like any other, so an editor can type into it; a foreign origin would
73
+ * let a page answer its own revocation check, and so renders nothing (never the options).
74
+ */
75
+ export interface CertDeliveryMarker {
76
+ /** Marker shape version. Only `1` is understood; anything else renders nothing. */
77
+ readonly v: 1
78
+ /** The CertREV app origin that serves the Delivery API (e.g. `https://portal.certrev.com`). */
79
+ readonly baseUrl: string
80
+ }
81
+
63
82
  /**
64
83
  * The OPTIONS a brand's native cert component receives. Delivered per certified
65
84
  * article; until the brand registers the component the block renders nothing.
@@ -131,6 +150,13 @@ export interface BuilderCertChromeData {
131
150
  renderDef: CertChromeRenderDef | null
132
151
  /** Version of the delivered string set (`data-certrev-strings-version` for the crawl monitor). */
133
152
  stringsVersion: string
153
+ /**
154
+ * 1.1.0: where the signed envelope for this block is served, or null for an options-only
155
+ * block. Present ⇒ the card paints from the verified envelope + def and ignores the keys
156
+ * above; they stay on the wire so installs older than 1.1.0 keep painting during the
157
+ * transition. See `CertDeliveryMarker`.
158
+ */
159
+ delivery: CertDeliveryMarker | null
134
160
  }
135
161
 
136
162
  /**
@@ -160,6 +186,7 @@ export const BUILDER_CERT_CHROME_KEYS = [
160
186
  'display',
161
187
  'renderDef',
162
188
  'stringsVersion',
189
+ 'delivery',
163
190
  ] as const
164
191
 
165
192
  export type BuilderCertChromeKey = (typeof BUILDER_CERT_CHROME_KEYS)[number]
@@ -18,10 +18,16 @@
18
18
  * - honors `display.showCredentials`.
19
19
  */
20
20
 
21
+ import * as React from 'react'
22
+ import { Suspense } from 'react'
23
+ import { memoToggleFor, toggleMemo } from '../components/memo-toggle.js'
21
24
  import { type CertBlockMode, renderCertBlock } from '../components/render-cert-block.js'
22
- import type { ResolvedBlockTheme } from '../components/render-def.js'
25
+ import { CERT_STRINGS_VERSION, type ResolvedBlockTheme } from '../components/render-def.js'
26
+ import { getVerifiedDelivery, peekVerifiedDelivery, type VerifiedDelivery } from '../verify/get-verified-delivery.js'
23
27
  import type { BuilderCertChromeData } from './cert-chrome-data.js'
24
28
  import { BUILDER_CERT_CHROME_KEYS } from './cert-chrome-data.js'
29
+ import { cardDeliveryRuntime } from './delivery-runtime.js'
30
+ import { envelopeCardInput } from './envelope-face.js'
25
31
 
26
32
  /**
27
33
  * The canonical Builder block name. A brand's registration + the portal
@@ -33,8 +39,12 @@ export const CERT_COMPONENT_NAME = 'CertREV Cert'
33
39
  export interface CertReviewCardProps extends Partial<BuilderCertChromeData> {
34
40
  /**
35
41
  * 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.
42
+ *
43
+ * Absent ⇒ `banner`, via the compatibility bridge in the render body below. That is NOT the
44
+ * package default: a direct `renderCertBlock` caller omitting `mode` gets `sidebar`, and
45
+ * `sidebar` is the registered input's `defaultValue`, so blocks created in the editor carry
46
+ * an explicit `mode`. The bridge covers only entries exported before the exporter emitted
47
+ * `mode` at all, and is deleted once those are backfilled. See the render body for why.
38
48
  */
39
49
  mode?: CertBlockMode | null
40
50
  /**
@@ -45,6 +55,13 @@ export interface CertReviewCardProps extends Partial<BuilderCertChromeData> {
45
55
  * string-splitting shim is gone — `renderCertBlock` composes each part directly.
46
56
  */
47
57
  part?: 'full' | 'header' | 'memo' | null
58
+ /**
59
+ * 1.1.0: what `@builder.io/sdk-react` passes because the registration sets
60
+ * `shouldReceiveBuilderProps: { builderContext: true }`. Only `content.id` is read: it is the
61
+ * Builder entry this block renders inside, which IS the Delivery placement's `externalId`, so a
62
+ * block copied onto another entry asks for that entry's cert and never shows this one's.
63
+ */
64
+ builderContext?: { readonly content?: { readonly id?: unknown } | null } | null
48
65
  }
49
66
 
50
67
  /**
@@ -83,7 +100,121 @@ function fusedCredential(pair: BuilderCertChromeData['credentialVerification'] |
83
100
  return credential && verifiedAt ? { credential, verifiedAt } : null
84
101
  }
85
102
 
103
+ /**
104
+ * The delivery marker, accepted only as `{ v: 1, baseUrl }` with `baseUrl` an `https:` ORIGIN on
105
+ * `certrev.com` or one of its subdomains (no path, query, credentials or port). Anything else is
106
+ * null, and a block carrying a marker that is not accepted renders NOTHING: falling back to the
107
+ * options would let an edit to the marker switch revocation off.
108
+ */
109
+ export function acceptedDeliveryMarker(value: unknown): { readonly baseUrl: string } | null {
110
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
111
+ const { v, baseUrl } = value as { v?: unknown; baseUrl?: unknown }
112
+ if (v !== 1 || typeof baseUrl !== 'string') return null
113
+ let url: URL
114
+ try {
115
+ url = new URL(baseUrl)
116
+ } catch {
117
+ return null
118
+ }
119
+ if (url.protocol !== 'https:' || url.username || url.password || url.port) return null
120
+ if (url.pathname !== '/' || url.search || url.hash) return null
121
+ const host = url.hostname.toLowerCase()
122
+ if (host !== 'certrev.com' && !host.endsWith('.certrev.com')) return null
123
+ return { baseUrl: url.origin }
124
+ }
125
+
126
+ /**
127
+ * Suspend on a Delivery read. React 19 gets `use` with ONE promise per placement while it is in
128
+ * flight; React 18 (no `use`) gets the promise thrown, the Suspense protocol it supports. Either
129
+ * way the retry finds the settled read in the cache through `peekVerifiedDelivery`, so it never
130
+ * suspends on a second promise.
131
+ */
132
+ const inflightReads = new Map<string, Promise<VerifiedDelivery>>()
133
+ function suspendOn(key: string, start: () => Promise<VerifiedDelivery>): VerifiedDelivery {
134
+ let pending = inflightReads.get(key)
135
+ if (!pending) {
136
+ pending = start().finally(() => inflightReads.delete(key))
137
+ inflightReads.set(key, pending)
138
+ }
139
+ const use = (React as unknown as { use?: <T>(p: Promise<T>) => T }).use
140
+ if (typeof use === 'function') return use(pending)
141
+ throw pending
142
+ }
143
+
144
+ /**
145
+ * The painted envelope face, with the phone memo toggle bound. One native click listener on the
146
+ * wrapper (delegated, so it survives a re-render of the HTML) flips the memo box the button sits
147
+ * in; the button is native, so Enter and Space reach it as clicks. Bound after hydration only: the
148
+ * server HTML is the collapsed face, which is also what a reader without JS keeps (the whole memo
149
+ * is still in the text for a screen reader, and the desktop never clamps it).
150
+ */
151
+ function EnvelopeFace(props: { readonly html: string; readonly defVersion: number | undefined }) {
152
+ const ref = React.useRef<HTMLDivElement>(null)
153
+ React.useEffect(() => {
154
+ const el = ref.current
155
+ if (!el) return
156
+ const onClick = (e: MouseEvent) => {
157
+ const button = memoToggleFor(e.target)
158
+ if (button && el.contains(button)) toggleMemo(button)
159
+ }
160
+ el.addEventListener('click', onClick)
161
+ return () => el.removeEventListener('click', onClick)
162
+ }, [])
163
+ return (
164
+ <div
165
+ ref={ref}
166
+ data-certrev-cert-chrome=""
167
+ // Marks a face painted from the verified envelope (the migration dry-run and crawl monitor
168
+ // tell the two faces apart by it). The strings are the package's locked constants here.
169
+ data-certrev-delivery=""
170
+ data-certrev-strings-version={CERT_STRINGS_VERSION}
171
+ data-certrev-def-version={props.defVersion}
172
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: renderCertBlock is the escaping-safe SSR string renderer
173
+ dangerouslySetInnerHTML={{ __html: props.html }}
174
+ />
175
+ )
176
+ }
177
+
178
+ function EnvelopeCertReviewCard(props: {
179
+ readonly baseUrl: string
180
+ readonly externalId: string
181
+ readonly mode: unknown
182
+ readonly part: unknown
183
+ }) {
184
+ const rt = cardDeliveryRuntime()
185
+ // The clock is read per render, so the lifecycle is judged NOW, not when the envelope was fetched.
186
+ const context = { platform: 'builder', externalId: props.externalId, now: new Date(rt.now()) }
187
+ const read = { baseUrl: props.baseUrl, platform: 'builder', externalId: props.externalId, context, cache: rt.cache }
188
+ const settled =
189
+ peekVerifiedDelivery(read) ??
190
+ suspendOn(`${props.baseUrl}|${props.externalId}`, () =>
191
+ getVerifiedDelivery({ ...read, resolveKid: rt.resolveKid, fetchImpl: rt.fetchImpl(), timeoutMs: rt.timeoutMs }),
192
+ )
193
+ // Revoked, expired, unverifiable, for another entry, or unreachable: nothing at all.
194
+ if (settled.verdict.decision !== 'render') return null
195
+ const input = envelopeCardInput(settled.verdict.payload.content, settled.renderDef, {
196
+ mode: props.mode,
197
+ part: props.part,
198
+ })
199
+ if (!input) return null
200
+ return <EnvelopeFace html={renderCertBlock(input)} defVersion={settled.renderDef ? settled.renderDef.v : undefined} />
201
+ }
202
+
86
203
  export function CertReviewCard(props: CertReviewCardProps) {
204
+ // 1.1.0: a block that carries the delivery marker paints from the signed envelope and the
205
+ // brand's def, never from the options beside it. No marker (every entry exported before 1.1.0)
206
+ // takes the options path below, unchanged from 1.0.3.
207
+ if (props.delivery != null) {
208
+ const marker = acceptedDeliveryMarker(props.delivery)
209
+ const entryId = text(props.builderContext?.content?.id)
210
+ if (!marker || !entryId) return null
211
+ return (
212
+ <Suspense fallback={null}>
213
+ <EnvelopeCertReviewCard baseUrl={marker.baseUrl} externalId={entryId} mode={props.mode} part={props.part} />
214
+ </Suspense>
215
+ )
216
+ }
217
+
87
218
  const reviewerName = text(props.reviewerName)
88
219
  const verifyUrl = text(props.verifyUrl)
89
220
  if (!reviewerName || !verifyUrl) return null // no chrome pre-cert (or an unreadable delivery)
@@ -106,12 +237,28 @@ export function CertReviewCard(props: CertReviewCardProps) {
106
237
  : undefined
107
238
 
108
239
  const html = renderCertBlock({
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,
240
+ // COMPATIBILITY BRIDGE, NOT THE DESIGN. Delete this fallback once the exporter emits an
241
+ // explicit `mode` and existing entries have been backfilled; until then it must stay.
242
+ //
243
+ // `renderCertBlock` owns the ONE placement default (absent or unrecognised ⇒ sidebar) and
244
+ // that decision STANDS: it is what a direct caller gets, and `sidebar` is the registered
245
+ // input's `defaultValue`, so every block created in the editor from now on carries an
246
+ // explicit `mode` and never reaches this line.
247
+ //
248
+ // The problem is the entries that already exist. `mode` is a PLACEMENT input, not one of
249
+ // the 20 wire keys (`BUILDER_CERT_CHROME_KEYS`), and the exporter emits only the wire
250
+ // projection (`options: { ...certData }`), so every entry exported to date has NO `mode`
251
+ // key at all. Through 0.5.5 this line read `?? 'banner'` and those entries rendered the
252
+ // banner face. Removing it in 1.0.0 did not give them the renderer's default in the
253
+ // abstract; it silently repainted live pages from banner to sidebar, with no entry edit
254
+ // and no version pin to protect them, because their install line is unpinned.
255
+ //
256
+ // So this is a bridge for pre-existing entries, NOT a reversal of the sidebar default.
257
+ // The three-copies-of-a-default problem that motivated removing it is still real and is
258
+ // still fixed: the helper text and the wire type's doc comment no longer name a default,
259
+ // and this line names one only for input the renderer can no longer distinguish from a
260
+ // deliberate choice. The exporter writing `mode` is what retires it.
261
+ mode: props.mode ?? 'banner',
115
262
  theme,
116
263
  // First-class banner memo split — renderCertBlock composes the requested
117
264
  // part directly; no string-splitting. Ignored outside banner mode, and an unrecognised
@@ -204,6 +351,7 @@ const WIRE_INPUT_TYPE: Record<(typeof BUILDER_CERT_CHROME_KEYS)[number], Builder
204
351
  display: 'object',
205
352
  renderDef: 'object',
206
353
  stringsVersion: 'string',
354
+ delivery: 'object',
207
355
  }
208
356
 
209
357
  /**
@@ -221,10 +369,12 @@ const WIRE_INPUT_HELPER_TEXT: Partial<Record<(typeof BUILDER_CERT_CHROME_KEYS)[n
221
369
  '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
370
  scopeLine:
223
371
  'System-populated by CertREV. The FTC scope line renders byte-verbatim from a locked constant: editing this text changes nothing on the page.',
372
+ delivery:
373
+ 'System-populated by CertREV. When set, the card shows the certificate CertREV serves for this entry, verified, and ignores the other fields; if CertREV revokes it or it expires, the card shows nothing. Do not edit.',
224
374
  }
225
375
 
226
376
  /**
227
- * The wire-type inputs — DERIVED from `BUILDER_CERT_CHROME_KEYS`, so the 19-field
377
+ * The wire-type inputs — DERIVED from `BUILDER_CERT_CHROME_KEYS`, so the 20-field
228
378
  * option set can never drift from the type (the W3 lock asserts name-parity). This
229
379
  * kills the demo's hand-maintained 17-entry array.
230
380
  */
@@ -279,6 +429,8 @@ export interface CertBlockBuilderRegistration {
279
429
  readonly name: string
280
430
  readonly component: typeof CertReviewCard
281
431
  readonly inputs: readonly BuilderInput[]
432
+ /** 1.1.0: asks the SDK for `builderContext`, whose `content.id` the delivery path reads. */
433
+ readonly shouldReceiveBuilderProps: { readonly builderContext: true }
282
434
  }
283
435
 
284
436
  /**
@@ -293,6 +445,7 @@ export const BUILDER_REGISTRATION: CertBlockBuilderRegistration = {
293
445
  name: CERT_COMPONENT_NAME,
294
446
  component: CertReviewCard,
295
447
  inputs: [...PLACEMENT_INPUTS, ...WIRE_INPUTS],
448
+ shouldReceiveBuilderProps: { builderContext: true },
296
449
  }
297
450
 
298
451
  /**
@@ -306,6 +459,12 @@ export interface CertReviewCardRegisteredComponent {
306
459
  name: string
307
460
  description?: string
308
461
  inputs: BuilderInput[]
462
+ /**
463
+ * 1.1.0: `@builder.io/sdk-react`'s `RegisteredComponent.shouldReceiveBuilderProps`. The SDK then
464
+ * passes the `builderContext` prop, and the delivery path takes its placement from
465
+ * `builderContext.content.id`.
466
+ */
467
+ shouldReceiveBuilderProps: { builderContext: boolean }
309
468
  }
310
469
 
311
470
  /**
@@ -320,4 +479,5 @@ export const certRevCertComponent: CertReviewCardRegisteredComponent = {
320
479
  description:
321
480
  'CertREV expert-review cert card. Options are populated by the CertREV exporter; editors choose placement (mode/part) only.',
322
481
  inputs: [...PLACEMENT_INPUTS, ...WIRE_INPUTS],
482
+ shouldReceiveBuilderProps: { builderContext: true },
323
483
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The Builder card's Delivery runtime (1.1.0): the trust root, fetch, cache and clock the
3
+ * envelope-backed `CertReviewCard` reads with.
4
+ *
5
+ * INTERNAL. This module is deliberately NOT on the package `exports` map, so a consumer cannot
6
+ * swap the trust root through the published API: the card verifies against the baked
7
+ * `cert-issuer-1` key, the same key the storefront badge bundle ships. The package's own tests
8
+ * reach the setter by relative import to run the real kernel against fixture-signed envelopes.
9
+ */
10
+
11
+ import type { ResolvePublicKeyByKid } from '../contract/kernel.js'
12
+ import { TtlCache } from '../verify/cache.js'
13
+ import type { CachedDelivery } from '../verify/get-verified-delivery.js'
14
+ import { CERT_ISSUER_KID, CERT_ISSUER_PUBLIC_KEY_PEM } from './issuer-key.js'
15
+
16
+ export interface CardDeliveryRuntime {
17
+ readonly resolveKid: ResolvePublicKeyByKid
18
+ readonly fetchImpl: () => typeof fetch
19
+ readonly cache: TtlCache<CachedDelivery>
20
+ readonly now: () => number
21
+ readonly timeoutMs?: number
22
+ }
23
+
24
+ const bakedResolveKid: ResolvePublicKeyByKid = (kid) =>
25
+ kid === CERT_ISSUER_KID ? { format: 'pem', pem: CERT_ISSUER_PUBLIC_KEY_PEM } : null
26
+
27
+ function defaultRuntime(): CardDeliveryRuntime {
28
+ return {
29
+ resolveKid: bakedResolveKid,
30
+ fetchImpl: () => globalThis.fetch,
31
+ cache: new TtlCache<CachedDelivery>({ ttlMs: 60_000, negativeTtlMs: 5_000 }),
32
+ now: Date.now,
33
+ }
34
+ }
35
+
36
+ let runtime: CardDeliveryRuntime = defaultRuntime()
37
+
38
+ export function cardDeliveryRuntime(): CardDeliveryRuntime {
39
+ return runtime
40
+ }
41
+
42
+ /** Test seam: replace parts of the runtime (a fresh cache unless one is given). Returns a restore. */
43
+ export function setCardDeliveryRuntimeForTests(overrides: Partial<CardDeliveryRuntime>): () => void {
44
+ const previous = runtime
45
+ const now = overrides.now ?? previous.now
46
+ runtime = {
47
+ ...previous,
48
+ cache: new TtlCache<CachedDelivery>({ ttlMs: 60_000, negativeTtlMs: 5_000, now }),
49
+ ...overrides,
50
+ }
51
+ return () => {
52
+ runtime = previous
53
+ }
54
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * `envelopeCardInput` — the Builder card's face, from VERIFIED facts + the brand's def (1.1.0)
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * The one mapping from a verified payload's `content` and the Delivery API's `renderDef` sibling
7
+ * to the `renderCertBlock` input the registered `CertReviewCard` paints. It is its own module,
8
+ * react-free, and exported on the `./builder/face` subpath so the portal's server bundle (which
9
+ * cannot load `./builder`: that entry creates a React context at import) renders its preview
10
+ * through THIS function instead of a copy of it. A preview and the live card that share the
11
+ * mapping cannot disagree about it.
12
+ *
13
+ * The facts are read exactly as the Shopify app embed reads the same envelope:
14
+ * - the credential is each verified credential's `cardLabel` (else `abbreviation`), joined, and
15
+ * only with the dated verification beside it; no date, no credential (the name-only face);
16
+ * - the compensation cue's PRESENCE is the only thing read; the words are the locked constant,
17
+ * and an absent or null cue is the pro-bono face with no cue;
18
+ * - the def's `hidden` set removes only what is hideable (`HIDEABLE_FIELDS`); a def cannot hide
19
+ * the scope line, the credential verification or the certificate link.
20
+ *
21
+ * The face is the PHONE-aware one (`responsive`): on a narrow viewport the banner header stacks and
22
+ * a long memo collapses behind a toggle the card binds (`memo-toggle.ts`); a wider viewport paints
23
+ * exactly what it would without it.
24
+ *
25
+ * The caller has already run the kernel. This function never decides WHETHER to render a cert;
26
+ * it only returns null when the verified content cannot make a face (no reviewer, no verify URL).
27
+ */
28
+
29
+ import type {
30
+ CertBlockFace,
31
+ CertBlockLayout,
32
+ CertBlockMode,
33
+ RenderCertBlockInput,
34
+ } from '../components/render-cert-block.js'
35
+ import { accentFg, HIDEABLE_FIELDS, isOpaqueHexColor, type ResolvedBlockTheme } from '../components/render-def.js'
36
+ import type { CertContent } from '../contract/kernel.js'
37
+ import type { CertChromeRenderDef } from './cert-chrome-data.js'
38
+
39
+ /**
40
+ * Every field id `renderCertBlock` places, in its canonical order. A def-driven face places all of
41
+ * them except what the def hides; `renderCertBlock` still gates each on its fact being present.
42
+ */
43
+ export const CERT_FACE_FIELDS = [
44
+ 'label',
45
+ 'authorName',
46
+ 'authorTitle',
47
+ 'reviewerPhoto',
48
+ 'bylineCredentialed',
49
+ 'bylinePlain',
50
+ 'bio',
51
+ 'memo',
52
+ 'profileLink',
53
+ 'certificateLink',
54
+ 'scopeLine',
55
+ 'compensationCue',
56
+ ] as const
57
+
58
+ const MODES: readonly CertBlockMode[] = ['banner', 'sidebar', 'floating']
59
+
60
+ function isMode(v: unknown): v is CertBlockMode {
61
+ return typeof v === 'string' && (MODES as readonly string[]).includes(v)
62
+ }
63
+
64
+ function text(v: unknown): string {
65
+ return typeof v === 'string' ? v : ''
66
+ }
67
+
68
+ function optionalText(v: unknown): string | undefined {
69
+ return text(v).trim() !== '' ? (v as string) : undefined
70
+ }
71
+
72
+ export interface EnvelopeCardOptions {
73
+ /** The placement the entry/editor chose. A recognised mode wins over the def's layout. */
74
+ readonly mode?: unknown
75
+ /** Banner memo placement, passed through as on the options path. */
76
+ readonly part?: unknown
77
+ }
78
+
79
+ /** The def's theme tokens as `renderCertBlock` theme input, with the accent ink derived. */
80
+ export function themeFromDef(def: CertChromeRenderDef | null): Partial<ResolvedBlockTheme> | undefined {
81
+ const tokens = def?.tokens
82
+ if (!tokens) return undefined
83
+ const accentColor = optionalText(tokens.accentColor)
84
+ return {
85
+ accentColor,
86
+ surface: optionalText(tokens.surface),
87
+ cornerRadius: optionalText(tokens.cornerRadius),
88
+ fontSlot: optionalText(tokens.fontSlot) as ResolvedBlockTheme['fontSlot'] | undefined,
89
+ // The ink painted over the accent fill, derived the way the portal derives it, so a black
90
+ // accent carries white ink. Omitted for an accent `resolveBlockTheme` would reject anyway.
91
+ ...(accentColor && isOpaqueHexColor(accentColor) ? { accentFg: accentFg(accentColor) } : {}),
92
+ barInk: optionalText(tokens.barInk),
93
+ inkColor: optionalText(tokens.inkColor),
94
+ }
95
+ }
96
+
97
+ /** The face a def asks for: its custom composition, or every field minus the hideable ones it hides. */
98
+ export function faceFromDef(def: CertChromeRenderDef | null, layout: CertBlockLayout): CertBlockFace | undefined {
99
+ if (!def) return undefined
100
+ if (layout === 'custom') return { rung: def.rung, placedFields: [...(def.customFace?.placedFields ?? [])] }
101
+ const hidden = new Set(def.hidden.filter((id) => (HIDEABLE_FIELDS as readonly string[]).includes(id)))
102
+ return { rung: def.rung, placedFields: CERT_FACE_FIELDS.filter((id) => !hidden.has(id)) }
103
+ }
104
+
105
+ /**
106
+ * The layout: an editor-chosen mode, else the def's own layout (a custom face only when the def
107
+ * carries one), else banner, which is what the Shopify embed paints for a def with no layout and
108
+ * what every exported Builder entry has painted since 0.5.
109
+ */
110
+ export function layoutFor(def: CertChromeRenderDef | null, mode: unknown): CertBlockLayout {
111
+ if (isMode(mode)) return mode
112
+ if (def?.layout === 'custom' && def.customFace) return 'custom'
113
+ if (isMode(def?.layout)) return def.layout
114
+ return 'banner'
115
+ }
116
+
117
+ /** The fused credential label the envelope's verified credentials stand for, or '' (name-only). */
118
+ export function credentialLabel(expert: Record<string, unknown>): { credential: string; verifiedAt: string } | null {
119
+ const verifiedAt = optionalText(expert.credentialVerifiedAt)
120
+ if (!verifiedAt) return null
121
+ const creds = Array.isArray(expert.credentials) ? expert.credentials : []
122
+ const credential = creds
123
+ .map((c) => {
124
+ const rec = (typeof c === 'object' && c !== null ? c : {}) as Record<string, unknown>
125
+ return optionalText(rec.cardLabel) ?? optionalText(rec.abbreviation)
126
+ })
127
+ .filter((l): l is string => l !== undefined)
128
+ .join(', ')
129
+ return credential ? { credential, verifiedAt } : null
130
+ }
131
+
132
+ /**
133
+ * The exact `renderCertBlock` input for a verified envelope's content and the brand's def, or
134
+ * null when the content cannot make a face.
135
+ */
136
+ export function envelopeCardInput(
137
+ content: CertContent,
138
+ renderDef: CertChromeRenderDef | null,
139
+ options: EnvelopeCardOptions = {},
140
+ ): RenderCertBlockInput | null {
141
+ const expert = (content?.expert ?? {}) as unknown as Record<string, unknown>
142
+ const author = (content?.author ?? {}) as unknown as Record<string, unknown>
143
+ const reviewerName = text(expert.displayName)
144
+ const verifyUrl = text(content?.verifyUrl)
145
+ if (!reviewerName || !verifyUrl) return null
146
+ const cred = credentialLabel(expert)
147
+ const layout = layoutFor(renderDef, options.mode)
148
+ const face = faceFromDef(renderDef, layout)
149
+ const theme = themeFromDef(renderDef)
150
+ const part =
151
+ options.part === 'full' || options.part === 'header' || options.part === 'memo' ? options.part : undefined
152
+ return {
153
+ mode: layout,
154
+ ...(theme ? { theme } : {}),
155
+ ...(face ? { face } : {}),
156
+ ...(part ? { part } : {}),
157
+ responsive: true,
158
+ facts: {
159
+ authorName: optionalText(author.name) ?? 'Editorial',
160
+ authorTitle: optionalText(author.title),
161
+ reviewerName,
162
+ credential: cred?.credential ?? '',
163
+ credentialVerifiedAt: cred?.verifiedAt ?? '',
164
+ certifiedAt: text(content.certifiedAt),
165
+ memo: text(content.memo),
166
+ bio: optionalText(expert.bio),
167
+ profileUrl: text(expert.profileUrl),
168
+ certificateUrl: verifyUrl,
169
+ // Presence only: a cue string renders the locked constant; absent or null is pro-bono.
170
+ compensationCue: optionalText(expert.compensationCue) ? undefined : null,
171
+ },
172
+ }
173
+ }
@@ -12,7 +12,7 @@
12
12
  * another article, and there is no `placementId` input to forge or mis-select.
13
13
  *
14
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`,
15
+ * `certRevCertComponent` register the 20 wire-type fields — `reviewerName`,
16
16
  * `credentialVerification`, `verifyUrl`, `memo` — as Builder INPUTS, and the card renders what
17
17
  * those inputs hold; it runs no signature check and consults no verdict. `advanced: true` folds
18
18
  * them out of the editor's default view, which is a UI reveal and not a lock, and `safeHttpUrl`
@@ -22,6 +22,17 @@
22
22
  * narrows it. See "PUSH trust model" in the package README for the full statement — including
23
23
  * what a revocation upstream does and does not reach.
24
24
  *
25
+ * 1.1.0 NARROWS THAT FOR A BLOCK CARRYING THE `delivery` MARKER (`{ v: 1, baseUrl }`, which the
26
+ * CertREV exporter writes). Such a block ignores every other option: it reads the signed envelope
27
+ * for the entry it renders inside (`builderContext.content.id`) from the CertREV Delivery API on
28
+ * `baseUrl` (an https certrev.com origin, nothing else), verifies it against the baked
29
+ * `cert-issuer-1` key, re-judges revocation and expiry at every render, and paints the brand's
30
+ * render def served beside it. A revoked, expired, unverifiable or unreachable cert, a marker that
31
+ * is not exactly that shape, or a missing entry id renders nothing. A block WITHOUT the marker
32
+ * (every entry exported before 1.1.0) renders from its options exactly as 1.0.3 did, so the
33
+ * paragraph above still describes it: an editor who deletes the marker gets the options face back,
34
+ * which is the 1.0.3 trust boundary, not a new one.
35
+ *
25
36
  * Three things the card DOES enforce, and they are the limit of it: no chrome at all without a
26
37
  * `reviewerName` and a `verifyUrl`; a credential renders only welded to its dated verification
27
38
  * (the fused pair); and the FTC disclosures render from locked constants, so rewriting the
@@ -95,7 +106,7 @@ export function CertRevEditorPlaceholder() {
95
106
  background: '#fff5fa',
96
107
  }}
97
108
  >
98
- CertREV Review — <b>resolves at publish</b> from issuer verification. The expert badge appears here once this
109
+ CertREV Review: <b>resolves at publish</b> from issuer verification. The expert badge appears here once this
99
110
  page’s article is certified, and only while the credential is valid.
100
111
  </div>
101
112
  )
@@ -144,8 +155,10 @@ export {
144
155
  type BuilderCertChromeData,
145
156
  type BuilderCertChromeKey,
146
157
  type CertChromeRenderDef,
158
+ type CertDeliveryMarker,
147
159
  } from './cert-chrome-data.js'
148
160
  export {
161
+ acceptedDeliveryMarker,
149
162
  BUILDER_REGISTRATION,
150
163
  type BuilderInput,
151
164
  CERT_COMPONENT_NAME,
@@ -157,3 +170,12 @@ export {
157
170
  PLACEMENT_INPUTS,
158
171
  WIRE_INPUTS,
159
172
  } from './cert-review-card.js'
173
+ export {
174
+ CERT_FACE_FIELDS,
175
+ credentialLabel,
176
+ type EnvelopeCardOptions,
177
+ envelopeCardInput,
178
+ faceFromDef,
179
+ layoutFor,
180
+ themeFromDef,
181
+ } from './envelope-face.js'
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The CertREV issuer PUBLIC key the envelope-backed Builder card verifies against (1.1.0).
3
+ *
4
+ * A copy of `CERT_ISSUER_KID` / `CERT_ISSUER_PUBLIC_KEY_PEM` in `modal/certrev-cert.entry.ts`, not
5
+ * an import of it: that entry registers a custom element and starts the storefront DOM passes at
6
+ * import, which a React server render must not do, and moving the constant out of it would change
7
+ * the storefront bundle the Shopify extension ships byte-for-byte. The two copies are pinned equal by
8
+ * `builder/__tests__/issuer-key.test.ts`, so a rotation that updates one and not the other fails.
9
+ */
10
+ export const CERT_ISSUER_KID = 'cert-issuer-1'
11
+ export const CERT_ISSUER_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
12
+ MCowBQYDK2VwAyEAMWN956IOPjpAq900dL428VzA28TO/pVXnq3brwqUmwM=
13
+ -----END PUBLIC KEY-----`