@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 +1 -1
- package/package.json +1 -1
- package/src/artifact/Gate.tsx +23 -11
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
package/src/artifact/Gate.tsx
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
290
|
+
identifyReader(opened.email, identifyDomain);
|
|
279
291
|
setHtml(opened.html);
|
|
280
292
|
onUnlockRef.current?.(opened.person);
|
|
281
293
|
}
|