@supersuit/artifacts 0.20.0 → 0.21.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 +18 -0
- package/README.md +11 -0
- package/lib/routes/artifacts.d.ts +9 -0
- package/lib/routes/artifacts.js +12 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.21.0 (2026-10-08)
|
|
7
|
+
|
|
8
|
+
**A page's past versions are for its admins.** Every reader who could open a page could also open
|
|
9
|
+
its History and read every earlier version at `/<id>/v/<n>`, so a passage edited out of a page
|
|
10
|
+
(a name taken out, a figure corrected) stayed readable one click away.
|
|
11
|
+
|
|
12
|
+
- New config `admins` (sign-in emails) and `history: 'admins' | 'everyone'`. The default,
|
|
13
|
+
`'admins'`, shows the History control and serves `/<id>/v/<n>` only to a signed-in reader whose
|
|
14
|
+
email is `ownerEmail` or in `admins`, compared without case. To everyone else the control is
|
|
15
|
+
absent and a past version is not found. "Version N · Updated <minute>" still shows to everyone.
|
|
16
|
+
- DETECTOR: after the bump, a reader who is not an admin no longer sees "History" under a page's
|
|
17
|
+
summary. A host that configures neither `ownerEmail` nor `admins` hides history from everyone.
|
|
18
|
+
- REMEDY: pass `admins` (and `ownerEmail`) for the people who should keep seeing history, or
|
|
19
|
+
`history: 'everyone'` to keep the 0.20.0 behaviour.
|
|
20
|
+
- Tests: signed out, a signed-in non-admin, the owner in another case and a listed admin, a past
|
|
21
|
+
version 404ing for non-admins, nobody configured, and `'everyone'` restoring the old page. The
|
|
22
|
+
earlier version and golden tests pin `history: 'everyone'`, so they still prove 0.20.0's page.
|
|
23
|
+
|
|
6
24
|
## 0.20.0 (2026-10-07)
|
|
7
25
|
|
|
8
26
|
**A host can hear when a confidential page is read.** The reader record answered `GET /reads`
|
package/README.md
CHANGED
|
@@ -123,6 +123,17 @@ phone, a side panel on a desk), so opening it moves no text. Each past version i
|
|
|
123
123
|
the same password or confidential door as the page itself. A version that does not exist is a
|
|
124
124
|
404; on a shut page the door comes first, so it never says which versions exist.
|
|
125
125
|
|
|
126
|
+
**Past versions are for admins by default (0.21.0).** An earlier draft can hold what the current
|
|
127
|
+
one was edited to remove, so only the page's admins see the History control or open
|
|
128
|
+
`/<id>/v/<n>`: the reader signed in as `ownerEmail` or any address in `admins` (case does not
|
|
129
|
+
matter). Everyone else still sees "Version N · Updated <minute>", and a past version is a 404 to
|
|
130
|
+
them. With neither configured, nobody does. `history: 'everyone'` in the config restores the old
|
|
131
|
+
behaviour, where every reader who can open the page reads its history.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
createArtifactRoutes({ ..., ownerEmail: 'owner@example.com', admins: ['editor@example.com'] })
|
|
135
|
+
```
|
|
136
|
+
|
|
126
137
|
**Do not write a "Version history" section in the markdown.** Say what changed when you publish:
|
|
127
138
|
|
|
128
139
|
- `POST /api/artifacts?id=<id>¬e=<one line>`, or a `change:` line in the front matter. The query
|
|
@@ -64,6 +64,15 @@ export type ArtifactRoutesConfig = {
|
|
|
64
64
|
* its writer's name, whatever `comments_visible:` says. Without it nobody is the owner, and
|
|
65
65
|
* the publisher reads comments through /responses. */
|
|
66
66
|
ownerEmail?: string;
|
|
67
|
+
/** Sign-in emails, besides `ownerEmail`, of the people who administer this host's pages. They
|
|
68
|
+
* see a page's version history; with `history: 'admins'` (the default) nobody else does. */
|
|
69
|
+
admins?: readonly string[];
|
|
70
|
+
/** Who sees a page's past versions: the History control under the summary and every
|
|
71
|
+
* `/<id>/v/<n>`. `'admins'` (default): only `ownerEmail` and `admins`, signed in; to everyone
|
|
72
|
+
* else the History control is absent and a past version is not found, because an earlier draft
|
|
73
|
+
* can hold what the current one was edited to remove. `'everyone'`: every reader who can open
|
|
74
|
+
* the page, which was the only behaviour before 0.21.0. */
|
|
75
|
+
history?: 'admins' | 'everyone';
|
|
67
76
|
/** Turns a reader's voice memo into text on the server, so the service's key never reaches a
|
|
68
77
|
* browser: `deepgramTranscriber(key)` or `openaiTranscriber(key)` from `./artifacts`, or your
|
|
69
78
|
* own. Without it the transcribe route answers 404 and readers get the browser's live
|
package/lib/routes/artifacts.js
CHANGED
|
@@ -60,6 +60,9 @@ export function createArtifactRoutes(config) {
|
|
|
60
60
|
const signInOrigin = config.signInOrigin;
|
|
61
61
|
const owner = config.owner ?? brand.name;
|
|
62
62
|
const ownerEmail = config.ownerEmail?.trim().toLowerCase();
|
|
63
|
+
const admins = new Set([ownerEmail, ...(config.admins ?? []).map((e) => e.trim().toLowerCase())].filter((e) => !!e));
|
|
64
|
+
/** Whether this reader may see a page's past versions (config `history`). */
|
|
65
|
+
const seesHistory = (reader) => config.history === 'everyone' || (!!reader && admins.has(reader.email.trim().toLowerCase()));
|
|
63
66
|
const readerSecret = () => config.readerSecret?.();
|
|
64
67
|
const signOutUrl = (id) => `/api/reader/leave?to=${encodeURIComponent(pagePath(id))}`;
|
|
65
68
|
// Signing out alone cannot change the account: the authority still holds the Google session
|
|
@@ -195,7 +198,7 @@ export function createArtifactRoutes(config) {
|
|
|
195
198
|
if (!a)
|
|
196
199
|
notFound();
|
|
197
200
|
const { pack, brand } = lookOf(a);
|
|
198
|
-
const render = (top, reader) => (n === null ? Body(a, { top, reader }) : VersionBody(a, n, { top }));
|
|
201
|
+
const render = (top, reader) => (n === null ? Body(a, { top, reader }) : VersionBody(a, n, { top, reader }));
|
|
199
202
|
if (a.access)
|
|
200
203
|
return GatedPage(a, id, render, n);
|
|
201
204
|
// A password shuts the body, never the title: the header stays so the reader knows which
|
|
@@ -273,8 +276,8 @@ export function createArtifactRoutes(config) {
|
|
|
273
276
|
return store.history(id).catch(() => null);
|
|
274
277
|
}
|
|
275
278
|
/** "Version N · Updated <minute> · History": the line under the summary. */
|
|
276
|
-
function VersionLine({ a, n, at, history }) {
|
|
277
|
-
return (_jsxs("p", { "data-nospeak": true, className: "mt-4 text-xs opacity-60", children: [`Version ${n} · Updated `, _jsx(UpdatedTime, { iso: at }), history && history.length > 1 ? (_jsxs(_Fragment, { children: [' · ', _jsx(VersionHistory, { items: history, base: pagePath(a.id), viewing: n })] })) : null] }));
|
|
279
|
+
function VersionLine({ a, n, at, history, reader }) {
|
|
280
|
+
return (_jsxs("p", { "data-nospeak": true, className: "mt-4 text-xs opacity-60", children: [`Version ${n} · Updated `, _jsx(UpdatedTime, { iso: at }), history && history.length > 1 && seesHistory(reader) ? (_jsxs(_Fragment, { children: [' · ', _jsx(VersionHistory, { items: history, base: pagePath(a.id), viewing: n })] })) : null] }));
|
|
278
281
|
}
|
|
279
282
|
/** The title block and the body, in the pack's Article when it has one. Without one this is
|
|
280
283
|
* exactly the markup every page had before packs could frame it. */
|
|
@@ -284,8 +287,11 @@ export function createArtifactRoutes(config) {
|
|
|
284
287
|
return (_jsxs(_Fragment, { children: [cover, _jsxs("article", { className: "mx-auto max-w-2xl px-6 pb-24", children: [toc, children] })] }));
|
|
285
288
|
}
|
|
286
289
|
/** A past version: its own body and title, read-only (no narration, no answers), under a banner. */
|
|
287
|
-
async function VersionBody(a, n, { top }) {
|
|
290
|
+
async function VersionBody(a, n, { top, reader }) {
|
|
288
291
|
const { pack, brand } = lookOf(a);
|
|
292
|
+
// Past versions are for admins unless the host says otherwise: to anyone else none exists.
|
|
293
|
+
if (!seesHistory(reader))
|
|
294
|
+
notFound();
|
|
289
295
|
const v = store.version ? await store.version(a.id, n) : null;
|
|
290
296
|
if (!v)
|
|
291
297
|
notFound();
|
|
@@ -296,7 +302,7 @@ export function createArtifactRoutes(config) {
|
|
|
296
302
|
const title = v.title ?? a.title;
|
|
297
303
|
const summary = v.summary ?? a.summary;
|
|
298
304
|
const toc = showToc(v.markdown, a.toc) ? tocOf(v.markdown) : [];
|
|
299
|
-
return (_jsxs(_Fragment, { children: [toc.length ? _jsx(TocRail, { items: toc }) : null, _jsx("div", { "data-nospeak": true, className: `mx-auto max-w-2xl px-6 ${top ? 'pt-20 sm:pt-24' : 'pt-8'}`, children: _jsxs("p", { "data-version-banner": true, role: "status", className: "rounded-lg border px-4 py-3 text-sm", style: { borderColor: brand.accent }, children: [`You are reading version ${n} of ${of}. `, _jsx("a", { href: pagePath(a.id), className: "underline underline-offset-4", style: { color: brand.accent }, children: "Read the current version" })] }) }), _jsx(Framed, { pack: pack, toc: toc.length ? _jsx(TocInline, { items: toc }) : null, cover: pack.Cover ? (_jsx(pack.Cover, { title: title, subtitle: v.subtitle, summary: summary, to: v.to, markdown: v.markdown, kicker: _jsx(Kicker, { to: v.to, brand: brand }), meta: _jsx(VersionLine, { a: a, n: n, at: v.at, history: history }), version: n, updatedAt: v.at, createdAt: a.createdAt })) : (_jsxs("div", { className: `mx-auto max-w-2xl px-6 ${top ? 'pt-10 sm:pt-12' : 'pt-10'} pb-8 text-center`, children: [_jsx(Kicker, { to: v.to, brand: brand }), _jsx("h1", { className: "text-4xl sm:text-5xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: title }), v.subtitle ? (_jsx("p", { className: "mx-auto mt-4 max-w-xl text-xl sm:text-2xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: v.subtitle })) : null, _jsx("p", { className: "mx-auto mt-6 max-w-xl text-lg italic opacity-80", children: summary }), _jsx(VersionLine, { a: a, n: n, at: v.at, history: history })] })), children: _jsx(ArtifactMarkdown, { markdown: v.markdown, definitions: a.definitions, Section: pack.Section, figure: pack.figure }) })] }));
|
|
305
|
+
return (_jsxs(_Fragment, { children: [toc.length ? _jsx(TocRail, { items: toc }) : null, _jsx("div", { "data-nospeak": true, className: `mx-auto max-w-2xl px-6 ${top ? 'pt-20 sm:pt-24' : 'pt-8'}`, children: _jsxs("p", { "data-version-banner": true, role: "status", className: "rounded-lg border px-4 py-3 text-sm", style: { borderColor: brand.accent }, children: [`You are reading version ${n} of ${of}. `, _jsx("a", { href: pagePath(a.id), className: "underline underline-offset-4", style: { color: brand.accent }, children: "Read the current version" })] }) }), _jsx(Framed, { pack: pack, toc: toc.length ? _jsx(TocInline, { items: toc }) : null, cover: pack.Cover ? (_jsx(pack.Cover, { title: title, subtitle: v.subtitle, summary: summary, to: v.to, markdown: v.markdown, kicker: _jsx(Kicker, { to: v.to, brand: brand }), meta: _jsx(VersionLine, { a: a, n: n, at: v.at, history: history, reader: reader }), version: n, updatedAt: v.at, createdAt: a.createdAt })) : (_jsxs("div", { className: `mx-auto max-w-2xl px-6 ${top ? 'pt-10 sm:pt-12' : 'pt-10'} pb-8 text-center`, children: [_jsx(Kicker, { to: v.to, brand: brand }), _jsx("h1", { className: "text-4xl sm:text-5xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: title }), v.subtitle ? (_jsx("p", { className: "mx-auto mt-4 max-w-xl text-xl sm:text-2xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: v.subtitle })) : null, _jsx("p", { className: "mx-auto mt-6 max-w-xl text-lg italic opacity-80", children: summary }), _jsx(VersionLine, { a: a, n: n, at: v.at, history: history, reader: reader })] })), children: _jsx(ArtifactMarkdown, { markdown: v.markdown, definitions: a.definitions, Section: pack.Section, figure: pack.figure }) })] }));
|
|
300
306
|
}
|
|
301
307
|
/** The page itself, shared by open and gated pages. */
|
|
302
308
|
async function Body(a, { top, reader }) {
|
|
@@ -317,7 +323,7 @@ export function createArtifactRoutes(config) {
|
|
|
317
323
|
const mode = config.state && a.state && a.comments && a.comments !== 'off' && a.state.slots[COMMENTS_SLOT] ? a.comments : 'off';
|
|
318
324
|
return (_jsxs(_Fragment, { children: [toc.length ? _jsx(TocRail, { items: toc }) : null, _jsx("div", { id: "artifact-narration-root", children: await (async () => {
|
|
319
325
|
const n = a.version ?? (a.versions?.length ?? 0) + 1;
|
|
320
|
-
const meta = (_jsxs(_Fragment, { children: [_jsx(VersionLine, { a: a, n: n, at: a.updatedAt, history: await historyOf(a.id) }), top && reader ? (_jsxs("p", { "data-nospeak": true, "data-reader-line": true, className: "mt-2 text-xs opacity-60", children: [`Signed in as ${reader.email} · `, _jsx("a", { href: switchUrl(a.id), className: "underline", children: "Use a different account" }), ' · ', _jsx("a", { href: signOutUrl(a.id), className: "underline", children: "Sign out" })] })) : null] }));
|
|
326
|
+
const meta = (_jsxs(_Fragment, { children: [_jsx(VersionLine, { a: a, n: n, at: a.updatedAt, history: await historyOf(a.id), reader: reader }), top && reader ? (_jsxs("p", { "data-nospeak": true, "data-reader-line": true, className: "mt-2 text-xs opacity-60", children: [`Signed in as ${reader.email} · `, _jsx("a", { href: switchUrl(a.id), className: "underline", children: "Use a different account" }), ' · ', _jsx("a", { href: signOutUrl(a.id), className: "underline", children: "Sign out" })] })) : null] }));
|
|
321
327
|
const header = pack.Cover ? (_jsx(pack.Cover, { title: a.title, subtitle: a.subtitle, summary: a.summary, to: a.to, markdown: a.markdown, kicker: _jsx(Kicker, { to: a.to, brand: brand }), meta: meta, version: n, updatedAt: a.updatedAt, createdAt: a.createdAt })) : (_jsxs("div", { className: `mx-auto max-w-2xl px-6 ${top ? 'pt-24 sm:pt-28' : 'pt-12'} pb-8 text-center`, children: [_jsx(Kicker, { to: a.to, brand: brand }), _jsx("h1", { className: "text-4xl sm:text-5xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: a.title }), a.subtitle ? (_jsx("p", { className: "mx-auto mt-4 max-w-xl text-xl sm:text-2xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: a.subtitle })) : null, _jsx("p", { className: "mx-auto mt-6 max-w-xl text-lg italic opacity-80", children: a.summary }), meta] }));
|
|
322
328
|
return (_jsx(Framed, { pack: pack, toc: toc.length ? _jsx(TocInline, { items: toc }) : null, cover: _jsxs(_Fragment, { children: [header, a.cover ? (_jsx("div", { className: "mx-auto max-w-2xl px-6 pb-8", children: _jsx("img", { src: a.cover, alt: "", className: "w-full rounded-xl border border-[color:var(--a-line)]" }) })) : null] }), children: _jsx(ArtifactMarkdown, { markdown: a.markdown, definitions: a.definitions, notes: config.state && a.state ? { artifactId: a.id, accent: brand.accent } : undefined, Section: pack.Section, figure: pack.figure }) }));
|
|
323
329
|
})() }), _jsx(CommentLayer, { artifactId: a.id, rootId: "artifact-narration-root", ownerName: owner, comments: mode, canShare: mode !== 'off' && (reader !== null || effectiveWriters(a.state, a.access, COMMENTS_SLOT) === 'anyone'), signedIn: reader !== null, isOwner: !!reader && !!ownerEmail && reader.email === ownerEmail, version: a.version ?? (a.versions?.length ?? 0) + 1, accent: brand.accent, ground: brand.ground, signIn: signInOrigin ? signInUrl(signInOrigin, pageUrl(a.id)) : null }), a.narration && words.length > 0 ? (_jsx(ArtifactReader, { src: a.narration, words: words, rootId: "artifact-narration-root", label: brand.narratorLabel(a.voice), accent: brand.accent, ground: brand.ground })) : null /* No recorded narration: no player. Never the browser's own voice (the operator,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|