@atelic-action/ui 0.15.0 → 0.15.1

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.
package/README.md CHANGED
@@ -226,7 +226,7 @@ The two pieces a reader sees are ordinary components, in `@atelic-action/ui/comp
226
226
 
227
227
  `@atelic-action/ui/artifact` is everything the artifact template shared across its cuts, so a fix reaches every business's documents with one version bump:
228
228
 
229
- - **The documents:** `Writeup`, `Report`, and `Gate` (the sealed document, with its `GatePayload` type), plus the `GradeChip` and `SignOff` the first two share, and every content type they take (`WriteupProps`, `ReportProps`, `GateProps`, and the rest).
229
+ - **The documents:** `Writeup`, `Report`, and `Gate` (the sealed document, with its `GatePayload` type; a reader's address is the key, so a payload never lists it, and the component identifies a reader to HubSpot by the address they typed), plus the `GradeChip` and `SignOff` the first two share, and every content type they take (`WriteupProps`, `ReportProps`, `GateProps`, and the rest).
230
230
  - **The shell:** `ArtifactShell`, the `.mkt` scope with the skip link, the dark header, the credit band, and the watermark around a page.
231
231
  - **The contract and the head:** the `ArtifactConfig` types, and `buildPageHead` and `canonicalUrl` with their `PageMeta` type.
232
232
  - **Who opened it:** `mintToken`, `sealReader`, `openReader`, `readerId`, `readerLink`, and `whoIsReading`. The same six are also built and exported alone as `@atelic-action/ui/artifact/readers`, because a Playwright spec and a script run under Node, which compiles nothing inside `node_modules`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Shared UI for the Atelic templates: site chrome, base styles, the component library, and the access gate they install",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -19,10 +19,13 @@ export interface GatePerson {
19
19
  /** Stable handle for the reader, used in the payload and in `onUnlock`. */
20
20
  id: string;
21
21
  /**
22
- * The reader's email address: the secret their key derives from, and,
23
- * when it matches `identifyDomain`, the identity pushed to HubSpot.
22
+ * Never read, and a payload sealed today does not carry it. The reader's
23
+ * address is the secret their key derives from, so a payload that lists
24
+ * it hands the key to anyone who reads the page's script; payloads sealed
25
+ * before 2026-10-08 did. The address a reader types is the one this
26
+ * component uses.
24
27
  */
25
- email: string;
28
+ email?: string;
26
29
  /** Base64: this reader's PBKDF2 salt, GCM nonce, and wrapped content key. */
27
30
  salt: string;
28
31
  iv: string;
@@ -83,6 +86,8 @@ function fromBase64(value: string): Uint8Array {
83
86
 
84
87
  interface Opened {
85
88
  person: GatePerson;
89
+ /** The address that opened the document, as the reader gave it. */
90
+ email: string;
86
91
  html: string;
87
92
  }
88
93
 
@@ -137,7 +142,7 @@ async function openDocument(payload: GatePayload, rawEmail: string): Promise<Ope
137
142
  contentKey,
138
143
  fromBase64(payload.ct) as BufferSource,
139
144
  );
140
- return { person, html: new TextDecoder().decode(plain) };
145
+ return { person, email: rawEmail, html: new TextDecoder().decode(plain) };
141
146
  } catch {
142
147
  // Wrong email for this reader. Try the next one; say nothing.
143
148
  }
@@ -145,11 +150,18 @@ async function openDocument(payload: GatePayload, rawEmail: string): Promise<Ope
145
150
  return null;
146
151
  }
147
152
 
148
- function identifyReader(person: GatePerson, domain: string | undefined) {
149
- if (!domain || !person.email) return;
150
- if (!person.email.toLowerCase().endsWith(`@${domain.toLowerCase()}`)) return;
153
+ /**
154
+ * Tells HubSpot who opened the document, from the address the reader gave:
155
+ * the payload names no one. The key forgives what the reader types (case,
156
+ * spaces, even a missing at sign), so the address is sent only when it reads
157
+ * as one, and only on the domain the site names.
158
+ */
159
+ function identifyReader(rawEmail: string, domain: string | undefined) {
160
+ const email = rawEmail.trim().toLowerCase();
161
+ if (!domain || !/^[^\s@]+@[^\s@]+$/.test(email)) return;
162
+ if (!email.endsWith(`@${domain.toLowerCase()}`)) return;
151
163
  window._hsq = window._hsq || [];
152
- window._hsq.push(["identify", { email: person.email }]);
164
+ window._hsq.push(["identify", { email }]);
153
165
  window._hsq.push(["setPath", window.location.pathname]);
154
166
  window._hsq.push(["trackPageView"]);
155
167
  }
@@ -238,7 +250,7 @@ export function Gate({
238
250
  if (cancelled) return;
239
251
  if (opened) {
240
252
  writeStored(storageKey, fromQuery);
241
- identifyReader(opened.person, identifyDomain);
253
+ identifyReader(opened.email, identifyDomain);
242
254
  setHtml(opened.html);
243
255
  onUnlockRef.current?.(opened.person);
244
256
  return;
@@ -252,7 +264,7 @@ export function Gate({
252
264
  if (!stored) return;
253
265
  const opened = await openDocument(payload, stored);
254
266
  if (cancelled || !opened) return;
255
- identifyReader(opened.person, identifyDomain);
267
+ identifyReader(opened.email, identifyDomain);
256
268
  setHtml(opened.html);
257
269
  onUnlockRef.current?.(opened.person);
258
270
  }
@@ -275,7 +287,7 @@ export function Gate({
275
287
  return;
276
288
  }
277
289
  writeStored(storageKey, value);
278
- identifyReader(opened.person, identifyDomain);
290
+ identifyReader(opened.email, identifyDomain);
279
291
  setHtml(opened.html);
280
292
  onUnlockRef.current?.(opened.person);
281
293
  }