@certrev/cert-block 1.2.0-canary.6 → 1.2.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 (42) hide show
  1. package/CHANGELOG.md +120 -35
  2. package/README.md +116 -21
  3. package/dist/builder/card-reader-css.d.ts +14 -0
  4. package/dist/builder/card-reader-css.d.ts.map +1 -0
  5. package/dist/builder/card-reader-css.js +17 -0
  6. package/dist/builder/card-reader-css.js.map +1 -0
  7. package/dist/builder/cert-review-card.d.ts +8 -0
  8. package/dist/builder/cert-review-card.d.ts.map +1 -1
  9. package/dist/builder/cert-review-card.js +234 -41
  10. package/dist/builder/cert-review-card.js.map +1 -1
  11. package/dist/builder/delivery-runtime.d.ts +15 -1
  12. package/dist/builder/delivery-runtime.d.ts.map +1 -1
  13. package/dist/builder/delivery-runtime.js +31 -3
  14. package/dist/builder/delivery-runtime.js.map +1 -1
  15. package/dist/builder/ssr-seed.d.ts +155 -0
  16. package/dist/builder/ssr-seed.d.ts.map +1 -0
  17. package/dist/builder/ssr-seed.js +278 -0
  18. package/dist/builder/ssr-seed.js.map +1 -0
  19. package/dist/reader/face.d.ts +6 -2
  20. package/dist/reader/face.d.ts.map +1 -1
  21. package/dist/reader/face.js +53 -5
  22. package/dist/reader/face.js.map +1 -1
  23. package/dist/reader/render.d.ts.map +1 -1
  24. package/dist/reader/render.js +10 -10
  25. package/dist/reader/render.js.map +1 -1
  26. package/dist/verify/cache.d.ts +39 -2
  27. package/dist/verify/cache.d.ts.map +1 -1
  28. package/dist/verify/cache.js +61 -12
  29. package/dist/verify/cache.js.map +1 -1
  30. package/dist/verify/get-verified-delivery.d.ts +28 -3
  31. package/dist/verify/get-verified-delivery.d.ts.map +1 -1
  32. package/dist/verify/get-verified-delivery.js +46 -9
  33. package/dist/verify/get-verified-delivery.js.map +1 -1
  34. package/package.json +112 -112
  35. package/src/builder/card-reader-css.ts +19 -0
  36. package/src/builder/cert-review-card.tsx +308 -56
  37. package/src/builder/delivery-runtime.ts +33 -3
  38. package/src/builder/ssr-seed.ts +343 -0
  39. package/src/reader/face.ts +59 -6
  40. package/src/reader/render.ts +12 -12
  41. package/src/verify/cache.ts +87 -11
  42. package/src/verify/get-verified-delivery.ts +77 -11
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The server render's envelope seed (1.2.0): what lets a hydrated delivery card paint with NO browser read.
3
+ *
4
+ * Through 1.1.1 the browser re-read the Delivery API for every card it hydrated, because nothing handed the server's
5
+ * read to it. A storefront whose Content-Security-Policy `connect-src` does not name the CertREV origin (e.l.f.'s
6
+ * Hydrogen store is one) blocked that read, the read settled as a suppress, and the card the server had painted
7
+ * vanished. So the painted face now carries the envelope it was painted from, as one attribute on its wrapper:
8
+ * `data-certrev-envelope` = `{ v: 1, baseUrl, externalId, design?, payload, signature?, renderDef }`. `payload` is the
9
+ * signed envelope's payload as the Delivery API served it, less the fields the def hides (below); `renderDef` is the
10
+ * def the face was painted with; `design` is the design pin the read was made with (the canary replica's `?design=N`),
11
+ * present only on a pinned read. React escapes the attribute like any other, so no `<script>` and no CSP `script-src`
12
+ * are involved.
13
+ *
14
+ * HIDDEN FIELDS ARE ABSENT FROM THE PAGE, NOT JUST FROM THE FACE. A def's `hidden` set (`reviewerPhoto`, `memo`) is the
15
+ * brand's statement that the page does not show those fields; a seed that carried them would put the reviewer's photo
16
+ * URL or the memo text in the page source all the same. So `withoutHiddenContent` removes them from the payload
17
+ * BEFORE the card paints and before it serializes, and both the server's face and the browser's are painted from the
18
+ * stripped payload (so they are identical, and a hidden memo is not on the page in either place):
19
+ * - `reviewerPhoto` hides `content.expert.photoUrl` and the author's `content.author.photoUrl` (the reader face paints
20
+ * the author's photo under the same switch);
21
+ * - `memo` hides `content.memo`.
22
+ * `display.showExpertPhoto: false` is the payload's own switch for the same photos and is stripped the same way (the
23
+ * portal never writes the URL for it either): both photo URLs go, as for `reviewerPhoto`, whatever the def hides.
24
+ * `content.expert.compensationCue` is NEVER touched: the cue is a locked disclosure no def can hide, and the face
25
+ * paints "Compensated expert" from it in the banner, the memo and the sidebar (the floating pill's own markup has
26
+ * no disclosure line, as in the portal's snippets; its modal paints the cue from the same field).
27
+ *
28
+ * THE SIGNATURE IS LEFT OUT WHENEVER ANYTHING WAS STRIPPED. The signature signs the full payload, so a payload with a
29
+ * field removed no longer verifies against it, and a seed that carried both would hand a verifier a pair that fails.
30
+ * Leaving it out costs nothing: the browser never re-checks a seed's signature (next paragraph), it re-runs only
31
+ * `renderVerdict` (lifecycle and subject) at its own clock, and neither reads a field the strip removes. A seed whose
32
+ * payload was not stripped keeps the signature, so a verifier can still check the page's HTML against the issuer key.
33
+ *
34
+ * The browser trusts a seed as far as it trusts the server HTML the seed sits in: whoever can write that HTML can
35
+ * already write any card they like, so the signature is NOT re-verified here (which would make hydration async, and
36
+ * Ed25519 in WebCrypto is not in every browser). The client still re-runs the lifecycle and subject policy
37
+ * (`renderVerdict`) at its own clock once hydration is done; `cert-review-card.tsx` does that.
38
+ *
39
+ * A PINNED CARD TAKES ONLY A SEED WITH THE SAME PIN (and an unpinned card only an unpinned one): the pin is part of
40
+ * the seed's key, as it is part of the read's, so the canary replica's `?design=7` card never paints the design the
41
+ * page carried for another version.
42
+ *
43
+ * WHEN THE SERVER HAD NO ENVELOPE TO PAINT (1.2.0) the same attribute carries a fallback record instead of a seed:
44
+ * `{ v: 1, baseUrl, externalId, design?, face }`, where `face` is `'options'` (CertREV could not be reached, so the
45
+ * server painted the block's own options face) or `'none'` (the server painted nothing: revoked, expired, unverifiable,
46
+ * for another entry). A browser that renders the card again (hydration, or React's client re-render after a hydration
47
+ * error elsewhere on the page) paints what the record says, so it neither reads the Delivery API nor shows a card the
48
+ * server withheld. A record holds no payload, so `parseSeed` never takes one for a seed. A `'none'` record sits on a
49
+ * `hidden` element with nothing in it.
50
+ *
51
+ * React-free and browser-only in effect: with no `document` (a server) `pageSeedFor` finds nothing, so a server never
52
+ * reads or keeps a seed and a multi-tenant render process holds nothing per page.
53
+ */
54
+ import type { CertPayload, CertSignature } from '../contract/kernel.js';
55
+ import { type DesignPin } from '../verify/get-verified-delivery.js';
56
+ import type { CertChromeRenderDef } from './cert-chrome-data.js';
57
+ export declare const SEED_ATTRIBUTE = "data-certrev-envelope";
58
+ export interface DeliverySeed {
59
+ readonly baseUrl: string;
60
+ readonly externalId: string;
61
+ /** The design pin the read was made with; absent on an unpinned read (every production brand). */
62
+ readonly design?: DesignPin;
63
+ /** The payload as painted: with every field the def hides removed (`withoutHiddenContent`). */
64
+ readonly payload: CertPayload;
65
+ /**
66
+ * The envelope's signature as served; absent when the payload was stripped (it no longer verifies), and when a
67
+ * cache was filled by hand (a test, a custom cache).
68
+ */
69
+ readonly signature?: CertSignature;
70
+ readonly renderDef: CertChromeRenderDef | null;
71
+ }
72
+ /** A seed as read from the page: the parsed seed and the attribute's exact text (re-rendered verbatim on hydration). */
73
+ export interface PageSeed {
74
+ readonly seed: DeliverySeed;
75
+ readonly raw: string;
76
+ }
77
+ /** What the server painted with no verified envelope: the block's options face, or nothing. */
78
+ export type FallbackFace = 'options' | 'none';
79
+ /** The record a server writes where it had no envelope to paint (the file header). */
80
+ export interface DeliveryFallback {
81
+ readonly baseUrl: string;
82
+ readonly externalId: string;
83
+ readonly design?: DesignPin;
84
+ readonly face: FallbackFace;
85
+ }
86
+ /** A fallback record as read from the page, with the attribute's exact text. */
87
+ export interface PageFallback {
88
+ readonly fallback: DeliveryFallback;
89
+ readonly raw: string;
90
+ }
91
+ /** Whatever the server wrote for one placement: a seed or a fallback record. */
92
+ export type PageRecord = PageSeed | PageFallback;
93
+ /**
94
+ * The payload with the named people's photo URLs removed (`null`); the same object when neither had one. The card uses
95
+ * it for a photo the browser could not load (a brand's CSP `img-src`, a 404), so the face repaints with the initials.
96
+ */
97
+ export declare function withoutPhotos(payload: CertPayload, people: readonly ('expert' | 'author')[]): CertPayload;
98
+ /**
99
+ * The payload with every content field the def's `hidden` set controls removed, and whether anything was; the two
100
+ * photo URLs go as well when the payload's own `display.showExpertPhoto` is `false` (exactly `false`, as the reader
101
+ * face reads it: absent or `null` shows the photo).
102
+ *
103
+ * Only the ids in `HIDEABLE_FIELDS` count (a def may list others; the faces ignore them, so they hide nothing). A
104
+ * removed text field becomes `null`, so the content keeps the shape the contract types give it and every face reads it
105
+ * as absent. Nothing but those fields changes: the subject and lifecycle (what `renderVerdict` reads), the cue, the
106
+ * dates, the links and the names are the payload's own. Never throws on a hostile shape: a payload whose `expert` or
107
+ * `author` is not an object has no photo to strip.
108
+ */
109
+ export declare function withoutHiddenContent(payload: CertPayload, renderDef: CertChromeRenderDef | null): {
110
+ readonly payload: CertPayload;
111
+ readonly stripped: boolean;
112
+ };
113
+ /**
114
+ * The seed for a verified read: the payload stripped of what the def hides, and the signature only when nothing was
115
+ * stripped. The card paints from this seed's payload, on the server and in the browser alike.
116
+ */
117
+ export declare function seedFor(read: {
118
+ readonly baseUrl: string;
119
+ readonly externalId: string;
120
+ readonly design?: DesignPin;
121
+ readonly payload: CertPayload;
122
+ readonly signature?: CertSignature;
123
+ readonly renderDef: CertChromeRenderDef | null;
124
+ }): DeliverySeed;
125
+ export declare function serializeSeed(seed: DeliverySeed): string;
126
+ export declare function serializeFallback(fallback: DeliveryFallback): string;
127
+ /** The seed in one attribute value, or null for anything that is not one (never throws). */
128
+ export declare function parseSeed(raw: string): DeliverySeed | null;
129
+ /** A fallback record in one attribute value, or null for anything that is not one (a seed included; never throws). */
130
+ export declare function parseFallback(raw: string): DeliveryFallback | null;
131
+ /**
132
+ * The record the server wrote for this placement and pin: its seed, or its fallback record. By default: from the map,
133
+ * else from one scan of the document (which also records every other one on the page). With `live`, the map is not
134
+ * consulted: the document is scanned and decides, so what a hydrating card learns is what the server HTML in front of
135
+ * it carries, not what an earlier page in this process left behind. `undefined` when the page carries none, and always
136
+ * on a server.
137
+ */
138
+ export declare function pageRecordFor(baseUrl: string, externalId: string, design?: DesignPin, options?: {
139
+ readonly live?: boolean;
140
+ }): PageRecord | undefined;
141
+ /** `pageRecordFor`, seeds only: the seed the server painted for this placement and pin. */
142
+ export declare function pageSeedFor(baseUrl: string, externalId: string, design?: DesignPin, options?: {
143
+ readonly live?: boolean;
144
+ }): PageSeed | undefined;
145
+ /**
146
+ * Whether the live document holds server-rendered React HTML: any Suspense boundary marker. The delivery card sits in
147
+ * a `<Suspense>` of its own, so a server render of one always leaves a marker, a card the server painted nothing for
148
+ * (revoked, expired, unreachable) included, and a client render never does. This is how a card that is hydrating tells
149
+ * a browser hydrating server HTML (the document holds it) from a server render that merely runs where a `document`
150
+ * exists (a test's jsdom, a shim): there the document does not hold the HTML being produced.
151
+ */
152
+ export declare function pageHoldsServerHtml(): boolean;
153
+ /** Test seam: forget every seed (a new page). */
154
+ export declare function forgetPageSeedsForTests(): void;
155
+ //# sourceMappingURL=ssr-seed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr-seed.d.ts","sourceRoot":"","sources":["../../src/builder/ssr-seed.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,278 @@
1
+ /**
2
+ * The server render's envelope seed (1.2.0): what lets a hydrated delivery card paint with NO browser read.
3
+ *
4
+ * Through 1.1.1 the browser re-read the Delivery API for every card it hydrated, because nothing handed the server's
5
+ * read to it. A storefront whose Content-Security-Policy `connect-src` does not name the CertREV origin (e.l.f.'s
6
+ * Hydrogen store is one) blocked that read, the read settled as a suppress, and the card the server had painted
7
+ * vanished. So the painted face now carries the envelope it was painted from, as one attribute on its wrapper:
8
+ * `data-certrev-envelope` = `{ v: 1, baseUrl, externalId, design?, payload, signature?, renderDef }`. `payload` is the
9
+ * signed envelope's payload as the Delivery API served it, less the fields the def hides (below); `renderDef` is the
10
+ * def the face was painted with; `design` is the design pin the read was made with (the canary replica's `?design=N`),
11
+ * present only on a pinned read. React escapes the attribute like any other, so no `<script>` and no CSP `script-src`
12
+ * are involved.
13
+ *
14
+ * HIDDEN FIELDS ARE ABSENT FROM THE PAGE, NOT JUST FROM THE FACE. A def's `hidden` set (`reviewerPhoto`, `memo`) is the
15
+ * brand's statement that the page does not show those fields; a seed that carried them would put the reviewer's photo
16
+ * URL or the memo text in the page source all the same. So `withoutHiddenContent` removes them from the payload
17
+ * BEFORE the card paints and before it serializes, and both the server's face and the browser's are painted from the
18
+ * stripped payload (so they are identical, and a hidden memo is not on the page in either place):
19
+ * - `reviewerPhoto` hides `content.expert.photoUrl` and the author's `content.author.photoUrl` (the reader face paints
20
+ * the author's photo under the same switch);
21
+ * - `memo` hides `content.memo`.
22
+ * `display.showExpertPhoto: false` is the payload's own switch for the same photos and is stripped the same way (the
23
+ * portal never writes the URL for it either): both photo URLs go, as for `reviewerPhoto`, whatever the def hides.
24
+ * `content.expert.compensationCue` is NEVER touched: the cue is a locked disclosure no def can hide, and the face
25
+ * paints "Compensated expert" from it in the banner, the memo and the sidebar (the floating pill's own markup has
26
+ * no disclosure line, as in the portal's snippets; its modal paints the cue from the same field).
27
+ *
28
+ * THE SIGNATURE IS LEFT OUT WHENEVER ANYTHING WAS STRIPPED. The signature signs the full payload, so a payload with a
29
+ * field removed no longer verifies against it, and a seed that carried both would hand a verifier a pair that fails.
30
+ * Leaving it out costs nothing: the browser never re-checks a seed's signature (next paragraph), it re-runs only
31
+ * `renderVerdict` (lifecycle and subject) at its own clock, and neither reads a field the strip removes. A seed whose
32
+ * payload was not stripped keeps the signature, so a verifier can still check the page's HTML against the issuer key.
33
+ *
34
+ * The browser trusts a seed as far as it trusts the server HTML the seed sits in: whoever can write that HTML can
35
+ * already write any card they like, so the signature is NOT re-verified here (which would make hydration async, and
36
+ * Ed25519 in WebCrypto is not in every browser). The client still re-runs the lifecycle and subject policy
37
+ * (`renderVerdict`) at its own clock once hydration is done; `cert-review-card.tsx` does that.
38
+ *
39
+ * A PINNED CARD TAKES ONLY A SEED WITH THE SAME PIN (and an unpinned card only an unpinned one): the pin is part of
40
+ * the seed's key, as it is part of the read's, so the canary replica's `?design=7` card never paints the design the
41
+ * page carried for another version.
42
+ *
43
+ * WHEN THE SERVER HAD NO ENVELOPE TO PAINT (1.2.0) the same attribute carries a fallback record instead of a seed:
44
+ * `{ v: 1, baseUrl, externalId, design?, face }`, where `face` is `'options'` (CertREV could not be reached, so the
45
+ * server painted the block's own options face) or `'none'` (the server painted nothing: revoked, expired, unverifiable,
46
+ * for another entry). A browser that renders the card again (hydration, or React's client re-render after a hydration
47
+ * error elsewhere on the page) paints what the record says, so it neither reads the Delivery API nor shows a card the
48
+ * server withheld. A record holds no payload, so `parseSeed` never takes one for a seed. A `'none'` record sits on a
49
+ * `hidden` element with nothing in it.
50
+ *
51
+ * React-free and browser-only in effect: with no `document` (a server) `pageSeedFor` finds nothing, so a server never
52
+ * reads or keeps a seed and a multi-tenant render process holds nothing per page.
53
+ */
54
+ import { HIDEABLE_FIELDS } from '../components/render-def.js';
55
+ import { parseDesignPin, parseRenderDef } from '../verify/get-verified-delivery.js';
56
+ export const SEED_ATTRIBUTE = 'data-certrev-envelope';
57
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
58
+ /** A value worth hiding: anything but nothing (absent, null, the empty string). */
59
+ const hasValue = (v) => v !== undefined && v !== null && v !== '';
60
+ /**
61
+ * The payload with the named people's photo URLs removed (`null`); the same object when neither had one. The card uses
62
+ * it for a photo the browser could not load (a brand's CSP `img-src`, a 404), so the face repaints with the initials.
63
+ */
64
+ export function withoutPhotos(payload, people) {
65
+ let content = { ...payload.content };
66
+ let stripped = false;
67
+ for (const person of people) {
68
+ const fields = content[person];
69
+ if (isRecord(fields) && hasValue(fields.photoUrl)) {
70
+ content = { ...content, [person]: { ...fields, photoUrl: null } };
71
+ stripped = true;
72
+ }
73
+ }
74
+ return stripped ? { ...payload, content: content } : payload;
75
+ }
76
+ /**
77
+ * The payload with every content field the def's `hidden` set controls removed, and whether anything was; the two
78
+ * photo URLs go as well when the payload's own `display.showExpertPhoto` is `false` (exactly `false`, as the reader
79
+ * face reads it: absent or `null` shows the photo).
80
+ *
81
+ * Only the ids in `HIDEABLE_FIELDS` count (a def may list others; the faces ignore them, so they hide nothing). A
82
+ * removed text field becomes `null`, so the content keeps the shape the contract types give it and every face reads it
83
+ * as absent. Nothing but those fields changes: the subject and lifecycle (what `renderVerdict` reads), the cue, the
84
+ * dates, the links and the names are the payload's own. Never throws on a hostile shape: a payload whose `expert` or
85
+ * `author` is not an object has no photo to strip.
86
+ */
87
+ export function withoutHiddenContent(payload, renderDef) {
88
+ const hidden = (renderDef?.hidden ?? []).filter((id) => HIDEABLE_FIELDS.includes(id));
89
+ const display = payload.content?.display;
90
+ const photoOff = isRecord(display) && display.showExpertPhoto === false;
91
+ if (hidden.length === 0 && !photoOff)
92
+ return { payload, stripped: false };
93
+ let next = photoOff || hidden.includes('reviewerPhoto') ? withoutPhotos(payload, ['expert', 'author']) : payload;
94
+ if (hidden.includes('memo') && hasValue(next.content.memo)) {
95
+ next = { ...next, content: { ...next.content, memo: null } };
96
+ }
97
+ return { payload: next, stripped: next !== payload };
98
+ }
99
+ /**
100
+ * The seed for a verified read: the payload stripped of what the def hides, and the signature only when nothing was
101
+ * stripped. The card paints from this seed's payload, on the server and in the browser alike.
102
+ */
103
+ export function seedFor(read) {
104
+ const { payload, stripped } = withoutHiddenContent(read.payload, read.renderDef);
105
+ return {
106
+ baseUrl: read.baseUrl,
107
+ externalId: read.externalId,
108
+ ...(read.design ? { design: read.design } : {}),
109
+ payload,
110
+ ...(read.signature && !stripped ? { signature: read.signature } : {}),
111
+ renderDef: read.renderDef,
112
+ };
113
+ }
114
+ const serializedPin = (design) => design ? { design: { version: design.version, ...(design.brand === undefined ? {} : { brand: design.brand }) } } : {};
115
+ export function serializeSeed(seed) {
116
+ return JSON.stringify({
117
+ v: 1,
118
+ baseUrl: seed.baseUrl,
119
+ externalId: seed.externalId,
120
+ ...serializedPin(seed.design),
121
+ payload: seed.payload,
122
+ ...(seed.signature ? { signature: seed.signature } : {}),
123
+ renderDef: seed.renderDef,
124
+ });
125
+ }
126
+ export function serializeFallback(fallback) {
127
+ return JSON.stringify({
128
+ v: 1,
129
+ baseUrl: fallback.baseUrl,
130
+ externalId: fallback.externalId,
131
+ ...serializedPin(fallback.design),
132
+ face: fallback.face,
133
+ });
134
+ }
135
+ /** A pin a record names, judged by the same grammar the card's own pin is: undefined when absent, null when bad. */
136
+ function parsedPin(design) {
137
+ if (design === undefined)
138
+ return undefined;
139
+ const judged = isRecord(design)
140
+ ? parseDesignPin({ design: design.version, ...(design.brand === undefined ? {} : { designBrand: design.brand }) })
141
+ : 'invalid';
142
+ return judged === 'invalid' || judged === undefined ? null : judged;
143
+ }
144
+ /** The seed in one attribute value, or null for anything that is not one (never throws). */
145
+ export function parseSeed(raw) {
146
+ let value;
147
+ try {
148
+ value = JSON.parse(raw);
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ if (!isRecord(value) || value.v !== 1)
154
+ return null;
155
+ const { baseUrl, externalId, design, payload, signature, renderDef } = value;
156
+ if (typeof baseUrl !== 'string' || typeof externalId !== 'string')
157
+ return null;
158
+ if (!isRecord(payload) || !isRecord(payload.subject) || !isRecord(payload.lifecycle) || !isRecord(payload.content)) {
159
+ return null;
160
+ }
161
+ // A pin the seed names is judged by the same grammar the card's own pin is; one that fails it is not a seed.
162
+ const pin = parsedPin(design);
163
+ if (pin === null)
164
+ return null;
165
+ return {
166
+ baseUrl,
167
+ externalId,
168
+ ...(pin ? { design: pin } : {}),
169
+ payload: payload,
170
+ ...(isRecord(signature) ? { signature: signature } : {}),
171
+ renderDef: parseRenderDef(renderDef),
172
+ };
173
+ }
174
+ /** A fallback record in one attribute value, or null for anything that is not one (a seed included; never throws). */
175
+ export function parseFallback(raw) {
176
+ let value;
177
+ try {
178
+ value = JSON.parse(raw);
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ if (!isRecord(value) || value.v !== 1 || 'payload' in value)
184
+ return null;
185
+ const { baseUrl, externalId, design, face } = value;
186
+ if (typeof baseUrl !== 'string' || typeof externalId !== 'string')
187
+ return null;
188
+ if (face !== 'options' && face !== 'none')
189
+ return null;
190
+ const pin = parsedPin(design);
191
+ if (pin === null)
192
+ return null;
193
+ return { baseUrl, externalId, ...(pin ? { design: pin } : {}), face };
194
+ }
195
+ /** One placement's key: the origin, the entry and the pin (the read's own key, so a pin never shares a seed). */
196
+ function seedKey(baseUrl, externalId, design) {
197
+ return design ? `${baseUrl}|${externalId}|design=${design.version}|${design.brand ?? ''}` : `${baseUrl}|${externalId}`;
198
+ }
199
+ /**
200
+ * The seeds (and fallback records) this page's server HTML carried, by placement, for the page's lifetime: a card
201
+ * mounted later by a client-side render (an in-app navigation back to a server-rendered article, or React's client
202
+ * re-render after a hydration error) paints what the server painted, as the hydrated card it replaces would have.
203
+ */
204
+ const pageSeeds = new Map();
205
+ const recordKey = (record) => {
206
+ const r = 'seed' in record ? record.seed : record.fallback;
207
+ return seedKey(r.baseUrl, r.externalId, r.design);
208
+ };
209
+ /** Every seed and fallback record the live document carries, in document order. */
210
+ function scanDocument() {
211
+ const found = [];
212
+ for (const el of Array.from(document.querySelectorAll(`[${SEED_ATTRIBUTE}]`))) {
213
+ const raw = el.getAttribute(SEED_ATTRIBUTE);
214
+ if (!raw)
215
+ continue;
216
+ const seed = parseSeed(raw);
217
+ const fallback = seed ? null : parseFallback(raw);
218
+ if (seed)
219
+ found.push({ seed, raw });
220
+ else if (fallback)
221
+ found.push({ fallback, raw });
222
+ }
223
+ return found;
224
+ }
225
+ /**
226
+ * The record the server wrote for this placement and pin: its seed, or its fallback record. By default: from the map,
227
+ * else from one scan of the document (which also records every other one on the page). With `live`, the map is not
228
+ * consulted: the document is scanned and decides, so what a hydrating card learns is what the server HTML in front of
229
+ * it carries, not what an earlier page in this process left behind. `undefined` when the page carries none, and always
230
+ * on a server.
231
+ */
232
+ export function pageRecordFor(baseUrl, externalId, design, options = {}) {
233
+ const key = seedKey(baseUrl, externalId, design);
234
+ if (typeof document === 'undefined')
235
+ return undefined;
236
+ const known = options.live ? undefined : pageSeeds.get(key);
237
+ if (known)
238
+ return known;
239
+ let inDocument = false;
240
+ for (const found of scanDocument()) {
241
+ const k = recordKey(found);
242
+ if (k === key)
243
+ inDocument = true;
244
+ if (options.live || !pageSeeds.has(k))
245
+ pageSeeds.set(k, found);
246
+ }
247
+ // A live read answers for the document alone: a record an earlier page left in the map is not one this page carries.
248
+ return options.live && !inDocument ? undefined : pageSeeds.get(key);
249
+ }
250
+ /** `pageRecordFor`, seeds only: the seed the server painted for this placement and pin. */
251
+ export function pageSeedFor(baseUrl, externalId, design, options = {}) {
252
+ const record = pageRecordFor(baseUrl, externalId, design, options);
253
+ return record && 'seed' in record ? record : undefined;
254
+ }
255
+ /** A Suspense boundary's comment marker as React's server render writes it: `$` (done), `$?` (pending), `$!`, `$~`. */
256
+ const BOUNDARY_MARKER = /^\$[?!~]?$/;
257
+ /**
258
+ * Whether the live document holds server-rendered React HTML: any Suspense boundary marker. The delivery card sits in
259
+ * a `<Suspense>` of its own, so a server render of one always leaves a marker, a card the server painted nothing for
260
+ * (revoked, expired, unreachable) included, and a client render never does. This is how a card that is hydrating tells
261
+ * a browser hydrating server HTML (the document holds it) from a server render that merely runs where a `document`
262
+ * exists (a test's jsdom, a shim): there the document does not hold the HTML being produced.
263
+ */
264
+ export function pageHoldsServerHtml() {
265
+ if (typeof document === 'undefined' || !document.documentElement)
266
+ return false;
267
+ const walker = document.createTreeWalker(document.documentElement, 128 /* NodeFilter.SHOW_COMMENT */);
268
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
269
+ if (BOUNDARY_MARKER.test(node.data))
270
+ return true;
271
+ }
272
+ return false;
273
+ }
274
+ /** Test seam: forget every seed (a new page). */
275
+ export function forgetPageSeedsForTests() {
276
+ pageSeeds.clear();
277
+ }
278
+ //# sourceMappingURL=ssr-seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr-seed.js","sourceRoot":"","sources":["../../src/builder/ssr-seed.ts"],"names":[],"mappings":"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"}
@@ -2,11 +2,15 @@ export type ReaderFaceSize = '' | 'sm' | 'lg';
2
2
  export interface ReaderFaceInput {
3
3
  /** The photo URL on the envelope. */
4
4
  readonly photo: unknown;
5
- /** Whether to ATTEMPT the photo: false never reads the URL (the silhouette shows). */
5
+ /** Whether to ATTEMPT the photo: false never reads the URL (the initials show). */
6
6
  readonly show: boolean;
7
+ /** The person's display name, the initials' source; a hostile type reads as no name. */
8
+ readonly name: unknown;
7
9
  readonly size: ReaderFaceSize;
8
10
  /** The pixel width the resizer is asked for. */
9
11
  readonly width: number;
10
12
  }
11
- export declare function readerFace({ photo, show, size, width }: ReaderFaceInput): string;
13
+ /** The initials of a display name by the spec above: '' when the name leaves no first character to take. */
14
+ export declare function initialsOf(name: unknown): string;
15
+ export declare function readerFace({ photo, show, name, size, width }: ReaderFaceInput): string;
12
16
  //# sourceMappingURL=face.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"face.d.ts","sourceRoot":"","sources":["../../src/reader/face.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;AAE7C,MAAM,WAAW,eAAe;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACtB;AAOD,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,eAAe,GAAG,MAAM,CAUhF"}
1
+ {"version":3,"file":"face.d.ts","sourceRoot":"","sources":["../../src/reader/face.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;AAE7C,MAAM,WAAW,eAAe;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACtB;AAoBD,4GAA4G;AAC5G,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAWhD;AAQD,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,eAAe,GAAG,MAAM,CAatF"}
@@ -1,19 +1,67 @@
1
1
  /**
2
- * `certrev-reader-face`: the round avatar on a reader byline: the person's photo when there is one to show, the
3
- * silhouette when there is not. The span carries a size class (`sm` for the author, none for the banner reviewer,
4
- * `lg` for the memo and sidebar reviewer); the image is requested at `width` square through the asset resizer.
2
+ * `certrev-reader-face`: the round avatar on a reader byline: the person's photo when there is one to paint, else their
3
+ * initials, else (a person with no name to take them from) the silhouette. The span carries a size class (`sm` for the
4
+ * author, none for the banner reviewer, `lg` for the memo and sidebar reviewer); the image is requested at `width`
5
+ * square through the asset resizer.
6
+ *
7
+ * THE INITIALS ARE A BYTE SPEC the portal's Liquid snippets and the WordPress renderer repeat, so all three print the
8
+ * same bytes: trim the name and split it on single spaces (`' '`) SKIPPING empty parts (two spaces in a row are one
9
+ * gap: "Jane Doe" gives "JD"); drop the FIRST part when, lowercased, it is one of the honorifics dr, mr, mrs, ms,
10
+ * prof, mx (each with or without its trailing dot); take the first character, by code point, of each of the first two
11
+ * parts left; uppercase them. Non-empty initials paint exactly
12
+ * `<svg viewBox="0 0 40 40" aria-hidden="true" focusable="false"><text x="20" y="20" text-anchor="middle"
13
+ * dominant-baseline="central" font-size="15" font-weight="700">JD</text></svg>` (one line, the initials escaped);
14
+ * empty ones paint the silhouette as before.
15
+ *
16
+ * "A photo to paint" is `show` with a URL that passes `safeUrl`. A def that hides `reviewerPhoto`, or
17
+ * `display.showExpertPhoto: false`, passes `show: false`: the URL is never read, so it never reaches the page, and the
18
+ * avatar is the initials (a hidden photo used to remove the avatar altogether; it removes the photo now).
5
19
  */
20
+ import { asText } from '../components/escape.js';
6
21
  import { avatarUrl, cleaned, esc, safeUrl } from './helpers.js';
7
22
  const SILHOUETTE = '<svg viewBox="0 0 40 40" aria-hidden="true" focusable="false"><circle cx="20" cy="14.8" r="7"/><path d="M20 23.4c-6.9 0-12.6 4.8-13.8 11.2a20 20 0 0 0 27.6 0C32.6 28.2 26.9 23.4 20 23.4Z"/></svg>';
23
+ const HONORIFICS = new Set([
24
+ 'dr',
25
+ 'dr.',
26
+ 'mr',
27
+ 'mr.',
28
+ 'mrs',
29
+ 'mrs.',
30
+ 'ms',
31
+ 'ms.',
32
+ 'prof',
33
+ 'prof.',
34
+ 'mx',
35
+ 'mx.',
36
+ ]);
37
+ /** The initials of a display name by the spec above: '' when the name leaves no first character to take. */
38
+ export function initialsOf(name) {
39
+ const parts = asText(name)
40
+ .trim()
41
+ .split(' ')
42
+ .filter((part) => part !== '');
43
+ if (HONORIFICS.has((parts[0] ?? '').toLowerCase()))
44
+ parts.shift();
45
+ return parts
46
+ .slice(0, 2)
47
+ .map((part) => Array.from(part)[0] ?? '')
48
+ .join('')
49
+ .toUpperCase();
50
+ }
51
+ const initialsSvg = (initials) => '<svg viewBox="0 0 40 40" aria-hidden="true" focusable="false"><text x="20" y="20" text-anchor="middle" ' +
52
+ `dominant-baseline="central" font-size="15" font-weight="700">${esc(initials)}</text></svg>`;
8
53
  const DIMENSION = { '': 42, sm: 36, lg: 46 };
9
- export function readerFace({ photo, show, size, width }) {
54
+ export function readerFace({ photo, show, name, size, width }) {
10
55
  const safe = show ? cleaned(safeUrl(photo)) : '';
11
56
  const src = cleaned(avatarUrl(safe, width));
12
57
  const dim = DIMENSION[size];
13
58
  const cls = size === '' ? 'certrev-r-face' : `certrev-r-face ${esc(size)}`;
59
+ const initials = src !== '' ? '' : initialsOf(name);
14
60
  const inner = src !== ''
15
61
  ? `<img src="${esc(src)}" alt="" width="${dim}" height="${dim}" loading="lazy" decoding="async">`
16
- : SILHOUETTE;
62
+ : initials !== ''
63
+ ? initialsSvg(initials)
64
+ : SILHOUETTE;
17
65
  return `<span class="${cls}">${inner}</span>`;
18
66
  }
19
67
  //# sourceMappingURL=face.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"face.js","sourceRoot":"","sources":["../../src/reader/face.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAc/D,MAAM,UAAU,GACf,qMAAqM,CAAA;AAEtM,MAAM,SAAS,GAA6C,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAA;AAEtF,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAmB;IACvE,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;IAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,kBAAkB,GAAG,CAAC,IAAI,CAAC,EAAE,CAAA;IAC1E,MAAM,KAAK,GACV,GAAG,KAAK,EAAE;QACT,CAAC,CAAC,aAAa,GAAG,CAAC,GAAG,CAAC,mBAAmB,GAAG,aAAa,GAAG,oCAAoC;QACjG,CAAC,CAAC,UAAU,CAAA;IACd,OAAO,gBAAgB,GAAG,KAAK,KAAK,SAAS,CAAA;AAC9C,CAAC"}
1
+ {"version":3,"file":"face.js","sourceRoot":"","sources":["../../src/reader/face.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAgB/D,MAAM,UAAU,GACf,qMAAqM,CAAA;AAEtM,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC;IAC/C,IAAI;IACJ,KAAK;IACL,IAAI;IACJ,KAAK;IACL,KAAK;IACL,MAAM;IACN,IAAI;IACJ,KAAK;IACL,MAAM;IACN,OAAO;IACP,IAAI;IACJ,KAAK;CACL,CAAC,CAAA;AAEF,4GAA4G;AAC5G,MAAM,UAAU,UAAU,CAAC,IAAa;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;SACxB,IAAI,EAAE;SACN,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAA;IAC/B,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAAE,KAAK,CAAC,KAAK,EAAE,CAAA;IACjE,OAAO,KAAK;SACV,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACxC,IAAI,CAAC,EAAE,CAAC;SACR,WAAW,EAAE,CAAA;AAChB,CAAC;AAED,MAAM,WAAW,GAAG,CAAC,QAAgB,EAAU,EAAE,CAChD,yGAAyG;IACzG,gEAAgE,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAA;AAE7F,MAAM,SAAS,GAA6C,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAA;AAEtF,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAmB;IAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;IAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,kBAAkB,GAAG,CAAC,IAAI,CAAC,EAAE,CAAA;IAC1E,MAAM,QAAQ,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,KAAK,GACV,GAAG,KAAK,EAAE;QACT,CAAC,CAAC,aAAa,GAAG,CAAC,GAAG,CAAC,mBAAmB,GAAG,aAAa,GAAG,oCAAoC;QACjG,CAAC,CAAC,QAAQ,KAAK,EAAE;YAChB,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC;YACvB,CAAC,CAAC,UAAU,CAAA;IACf,OAAO,gBAAgB,GAAG,KAAK,KAAK,SAAS,CAAA;AAC9C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/reader/render.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAmBxD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAA;AAE5D,wHAAwH;AACxH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEnD,MAAM,WAAW,eAAe;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,sGAAsG;IACtG,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAA;IAC1B,2GAA2G;IAC3G,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,8FAA8F;IAC9F,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAC/B;AAiQD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAQ/D"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/reader/render.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAmBxD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAA;AAE5D,wHAAwH;AACxH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEnD,MAAM,WAAW,eAAe;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,sGAAsG;IACtG,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAA;IAC1B,2GAA2G;IAC3G,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,8FAA8F;IAC9F,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAC/B;AA+PD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAQ/D"}