@supersuit/artifacts 0.57.0 → 0.58.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.
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
3
3
|
`@supersuit/artifacts`. One entry per version, newest first. Each entry says what changed, how a
|
|
4
4
|
host can tell whether it is affected (DETECTOR), what a host does about it (REMEDY), and the tests.
|
|
5
5
|
|
|
6
|
+
## 0.58.0 (2026-10-27)
|
|
7
|
+
|
|
8
|
+
**A link's provenance can name its reader by an opaque person id.** Superlinks step 6: a funnel
|
|
9
|
+
suggestion ("Ada read the proposal to the end: move her to proposed?") has to know whose opens these
|
|
10
|
+
are. A label is a name and cannot be trusted as an identity; the sender's opaque person id can.
|
|
11
|
+
|
|
12
|
+
- `provenance.subject`: a person id, `p_` and 8 characters from the sender's records. Anything else
|
|
13
|
+
(a name, a different shape) is refused 400 and nothing is added. Kept on the row and answered back
|
|
14
|
+
like the rest of the provenance.
|
|
15
|
+
- DETECTOR: a sender that posts `provenance.subject` and reads it back missing is on 0.57.x or older.
|
|
16
|
+
- REMEDY: update the host.
|
|
17
|
+
- Tests: recipients-api.test.tsx (subject kept; a name and malformed ids refused).
|
|
18
|
+
|
|
19
|
+
## 0.57.2 (2026-10-26)
|
|
20
|
+
|
|
21
|
+
**0.57.1, published.** 0.57.1 was tagged but never reached npm: its publish run refused it, because
|
|
22
|
+
a second test (`src/routes/page-analytics-mount.test.tsx`) still expected the label as the PostHog
|
|
23
|
+
name. That test now asserts the identity carries no `name`. Everything in the 0.57.1 entry below
|
|
24
|
+
ships in this version.
|
|
25
|
+
|
|
26
|
+
## 0.57.1 (2026-10-26, never published: see 0.57.2)
|
|
27
|
+
|
|
28
|
+
**Page analytics no longer send a per-reader link's label to PostHog.** A per-reader page identified its
|
|
29
|
+
reader as `recipient:<rid>` with `name` set to the label the publisher gave the link, which is the
|
|
30
|
+
person's name, so every reader's name sat in the analytics provider. The operator's ruling for
|
|
31
|
+
superlinks (2026-10-26): analytics gets the link's id, never who it is for.
|
|
32
|
+
|
|
33
|
+
- The identity is `{ distinctId: 'recipient:<rid>' }`, no `name`. A signed-in reader on a gated page is
|
|
34
|
+
unchanged.
|
|
35
|
+
- DETECTOR: a PostHog person whose distinct id starts `recipient:` carries a `name` property.
|
|
36
|
+
- REMEDY: update the host. Names already sent stay in PostHog until deleted there.
|
|
37
|
+
- Tests: src/reader/analytics-plan.test.ts (the per-reader identity is the id alone).
|
|
38
|
+
|
|
6
39
|
## 0.57.0 (2026-10-26)
|
|
7
40
|
|
|
8
41
|
**`GET /api/artifacts/<id>` says what a link to the page lets its reader do.** A sender approves a
|
package/README.md
CHANGED
|
@@ -1085,7 +1085,7 @@ mark is counted as before-send; senders mark as soon as delivery is confirmed.
|
|
|
1085
1085
|
### Who minted a link (0.53.0)
|
|
1086
1086
|
|
|
1087
1087
|
Every link can say who made it, on whose authority, and when: `add: [{ label, provenance: { actor,
|
|
1088
|
-
authority, at?, link?, funnel?, stage? } }]
|
|
1088
|
+
authority, at?, link?, funnel?, stage?, subject? } }]`; `subject` (0.58.0) is the sender's opaque person id (`p_` and 8 characters), never a name. `actor` is a person acting by hand or "<person>'s
|
|
1089
1089
|
Pluma"; `authority` names what they acted under ("approved by <person> on the send card", "on
|
|
1090
1090
|
<person>'s explicit request", "under the recurrent <name>"), with `link` to the session or message
|
|
1091
1091
|
where one exists (https only); `at` defaults to now. `funnel` (a slug) and `stage` (an id, only with
|
|
@@ -1279,7 +1279,7 @@ Who is recorded, and as whom (`analyticsPlan`, `src/reader/analytics-plan.ts`):
|
|
|
1279
1279
|
- Never the owner or an admin (signed in, previewing `?as=reader`, or on the owner's own per-reader
|
|
1280
1280
|
link), never a link-preview fetcher, never a past version.
|
|
1281
1281
|
- A gated page identifies the signed-in reader by email.
|
|
1282
|
-
- A per-reader link identifies as `recipient:<rid>`
|
|
1282
|
+
- A per-reader link identifies as `recipient:<rid>` and nothing more: never the label the publisher gave it, which names the person (0.57.1). The id-to-person map stays with the publisher, in the recipients store.
|
|
1283
1283
|
- A public or password page stays anonymous.
|
|
1284
1284
|
- Every page, whatever its kind, records replays with all text and all inputs masked
|
|
1285
1285
|
(`RECORDING_MASK`), and autocapture collects no element text. There is no opt-out.
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// - The owner and the host's admins are never recorded, signed in or on their own per-reader link.
|
|
7
7
|
// - A link-preview fetcher is never handed the script.
|
|
8
8
|
// - A reader is identified only where the page already knows who they are: a gated page by the
|
|
9
|
-
// signed-in email, a per-reader link by its opaque recipient id
|
|
10
|
-
// it (never a phone or an email
|
|
9
|
+
// signed-in email, a per-reader link by its opaque recipient id alone: never the label the
|
|
10
|
+
// publisher gave it, which names the person (2026-10-26), and never a phone or an email. A public or password
|
|
11
11
|
// page stays anonymous, even when its reader happens to be signed in.
|
|
12
12
|
export const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com';
|
|
13
13
|
export function analyticsPlan(input) {
|
|
@@ -20,7 +20,8 @@ export function analyticsPlan(input) {
|
|
|
20
20
|
identity = { distinctId: email, email, ...(input.reader.name ? { name: input.reader.name } : {}) };
|
|
21
21
|
}
|
|
22
22
|
else if (input.kind === 'per-reader' && input.recipient?.rid) {
|
|
23
|
-
|
|
23
|
+
// The link's id alone: who it is for stays with the publisher (superlinks, 2026-10-26).
|
|
24
|
+
identity = { distinctId: `recipient:${input.recipient.rid}` };
|
|
24
25
|
}
|
|
25
26
|
return {
|
|
26
27
|
key,
|
|
@@ -127,7 +127,7 @@ export type ArtifactRoutesConfig = {
|
|
|
127
127
|
* autocapture, scroll depth on leave, and section_viewed / narration_* events. Without a key no
|
|
128
128
|
* page loads any analytics script. Never records the owner or an admin, never a link-preview
|
|
129
129
|
* fetcher, never a past version; identifies a reader only on a gated page (their email) or a
|
|
130
|
-
* per-reader link (its recipient id
|
|
130
|
+
* per-reader link (its recipient id alone, never the label). See reader/analytics-plan.ts. */
|
|
131
131
|
analytics?: AnalyticsConfig | null;
|
|
132
132
|
};
|
|
133
133
|
/** Ids are 8 chars from the safe alphabet; anything else is not a page and never reaches the store. */
|
|
@@ -18,7 +18,7 @@ const SLUG = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
|
18
18
|
* now), and the funnel step it serves. -> the clean record, or the reason it is refused. */
|
|
19
19
|
export function readProvenance(v, now = new Date()) {
|
|
20
20
|
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
21
|
-
return { ok: false, why: 'provenance is { actor, authority, at?, link?, funnel?, stage? }' };
|
|
21
|
+
return { ok: false, why: 'provenance is { actor, authority, at?, link?, funnel?, stage?, subject? }' };
|
|
22
22
|
const p = v;
|
|
23
23
|
const text = (x, max) => (typeof x === 'string' && x.trim() ? x.trim().slice(0, max) : '');
|
|
24
24
|
const actor = text(p.actor, 120), authority = text(p.authority, 300);
|
|
@@ -42,8 +42,12 @@ export function readProvenance(v, now = new Date()) {
|
|
|
42
42
|
return { ok: false, why: 'provenance.funnel must be a funnel slug' };
|
|
43
43
|
if (p.stage !== undefined && (typeof p.stage !== 'string' || !SLUG.test(p.stage) || p.funnel === undefined))
|
|
44
44
|
return { ok: false, why: 'provenance.stage must be a stage id, with its funnel' };
|
|
45
|
+
// The reader by the sender's opaque person id (superlinks step 6), so a funnel can tell whose opens
|
|
46
|
+
// these are without a name ever reaching the host.
|
|
47
|
+
if (p.subject !== undefined && (typeof p.subject !== 'string' || !/^p_[0-9abcdefghjkmnpqrstvwxyz]{8}$/.test(p.subject)))
|
|
48
|
+
return { ok: false, why: 'provenance.subject must be an opaque person id (p_ and 8 characters), never a name' };
|
|
45
49
|
const at = typeof p.at === 'string' ? new Date(p.at).toISOString() : now.toISOString();
|
|
46
|
-
return { ok: true, value: { actor, authority, at, ...(link ? { link } : {}), ...(p.funnel ? { funnel: p.funnel } : {}), ...(p.stage ? { stage: p.stage } : {}) } };
|
|
50
|
+
return { ok: true, value: { actor, authority, at, ...(link ? { link } : {}), ...(p.funnel ? { funnel: p.funnel } : {}), ...(p.stage ? { stage: p.stage } : {}), ...(p.subject ? { subject: p.subject } : {}) } };
|
|
47
51
|
}
|
|
48
52
|
const none = () => new NextResponse(null, { status: 204, headers: { 'cache-control': 'no-store' } });
|
|
49
53
|
const json = (b, status = 200) => NextResponse.json(b, { status, headers: { 'cache-control': 'no-store' } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Publish markdown as branded, read-aloud pages from a Next.js app: the artifacts store, the route factory, the read-along reader, brand packs, password and confidential pages, and share cards.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|