@supersuit/artifacts 0.31.1 → 0.32.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,25 @@
|
|
|
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.32.0
|
|
7
|
+
|
|
8
|
+
**The page's owner and the hub's admins always get past a locked page's door** (Gary, 2026-10-11).
|
|
9
|
+
Before, an admin who set a page to "only the list" without being on it landed on the door at their
|
|
10
|
+
next reload. `decide()` takes a fourth argument, `staff`, which opens the page with
|
|
11
|
+
`why: 'staff'`; the page, the agreement, the reading heartbeat and the state API all pass it.
|
|
12
|
+
|
|
13
|
+
**Play from here stays inside the page's text column.** 0.31.1 bounded the bar by clipping
|
|
14
|
+
ancestors, but the company host's card does not clip that way, so the pill still ran 2px past the
|
|
15
|
+
card's edge at iPhone width. It is now also bounded by the narration root, the page as the reader
|
|
16
|
+
sees it.
|
|
17
|
+
|
|
18
|
+
- DETECTOR: as a hub admin not on a list-only page's list, open it: the door means 0.31.1. At iPhone
|
|
19
|
+
width on the company host, tap a word at the right edge: a pill past the card's edge means 0.31.1.
|
|
20
|
+
- REMEDY: update the dependency. A host passing `admins` or `ownerEmail` needs nothing else.
|
|
21
|
+
- Tests: `reader.test.ts` (staff opens signed in, never signed out), `gated.test.tsx` (an admin and
|
|
22
|
+
the owner read a list-only page they are not on; anyone else is still refused),
|
|
23
|
+
`artifact-reader-controls.test.tsx` (the bar stays 8px inside the text column).
|
|
24
|
+
|
|
6
25
|
## 0.31.1
|
|
7
26
|
|
|
8
27
|
**Play from here stays inside the page card, not only the screen.** 0.30.2 kept the bar inside
|
|
@@ -38,6 +38,10 @@ export type Decision = {
|
|
|
38
38
|
open: true;
|
|
39
39
|
why: 'member';
|
|
40
40
|
reason: string;
|
|
41
|
+
} | {
|
|
42
|
+
open: true;
|
|
43
|
+
why: 'staff';
|
|
44
|
+
reason: string;
|
|
41
45
|
} | {
|
|
42
46
|
open: false;
|
|
43
47
|
why: 'signed-out';
|
|
@@ -48,10 +52,13 @@ export type Decision = {
|
|
|
48
52
|
};
|
|
49
53
|
export declare const MEMBER_REASON = "you are a Freedom user";
|
|
50
54
|
export declare const LISTED_REASON = "you were given access to it by name";
|
|
55
|
+
export declare const STAFF_REASON = "you are its owner or an admin of this hub";
|
|
51
56
|
/** Whether this reader may open a page with this access level. The allowlist opens both
|
|
52
57
|
* levels; `freedom` also opens to any active Freedom account. A listed person's own reason
|
|
53
58
|
* wins over the membership one, because it is the truer account of why they are reading. */
|
|
54
|
-
|
|
59
|
+
/** `staff`: the reader is the page's owner or one of the hub's admins, who always get in, so an
|
|
60
|
+
* admin who narrows a page to its list can never lock themselves out of it. */
|
|
61
|
+
export declare function decide(access: Access, reader: Reader | null, allow: AllowEntry[], staff?: boolean): Decision;
|
|
55
62
|
/** The name the banner addresses: the allowlist's name, then the account's, first word only. */
|
|
56
63
|
export declare function firstName(reader: Reader, allow: AllowEntry[]): string | null;
|
|
57
64
|
/** Where a reader goes to sign in, carrying the page they asked for. `switchAccount` tells the
|
package/lib/artifacts/reader.js
CHANGED
|
@@ -92,12 +92,17 @@ export function verifyGrant(secret, grant, now = Date.now()) {
|
|
|
92
92
|
}
|
|
93
93
|
export const MEMBER_REASON = 'you are a Freedom user';
|
|
94
94
|
export const LISTED_REASON = 'you were given access to it by name';
|
|
95
|
+
export const STAFF_REASON = 'you are its owner or an admin of this hub';
|
|
95
96
|
/** Whether this reader may open a page with this access level. The allowlist opens both
|
|
96
97
|
* levels; `freedom` also opens to any active Freedom account. A listed person's own reason
|
|
97
98
|
* wins over the membership one, because it is the truer account of why they are reading. */
|
|
98
|
-
|
|
99
|
+
/** `staff`: the reader is the page's owner or one of the hub's admins, who always get in, so an
|
|
100
|
+
* admin who narrows a page to its list can never lock themselves out of it. */
|
|
101
|
+
export function decide(access, reader, allow, staff = false) {
|
|
99
102
|
if (!reader)
|
|
100
103
|
return { open: false, why: 'signed-out' };
|
|
104
|
+
if (staff)
|
|
105
|
+
return { open: true, why: 'staff', reason: STAFF_REASON };
|
|
101
106
|
const listed = allow.find((a) => a.email.trim().toLowerCase() === reader.email);
|
|
102
107
|
if (listed)
|
|
103
108
|
return { open: true, why: 'listed', reason: listed.reason?.trim() || LISTED_REASON };
|
|
@@ -186,13 +186,19 @@ export function ArtifactReader({ src, words, rootId, label, accent, ground, }) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
+
// And inside the page's own text column, which is what a reader sees as the page.
|
|
190
|
+
const col = document.getElementById(rootId)?.getBoundingClientRect();
|
|
191
|
+
if (col && col.width > 0) {
|
|
192
|
+
lo = Math.max(lo, col.left);
|
|
193
|
+
hi = Math.min(hi, col.right);
|
|
194
|
+
}
|
|
189
195
|
const vw = hi;
|
|
190
196
|
const r = el.getBoundingClientRect();
|
|
191
197
|
// No layout to measure (a server render, a test DOM): leave the placement as computed.
|
|
192
198
|
const shift = r.width === 0 ? 0 : r.right > vw - 8 ? vw - 8 - r.right : r.left < lo + 8 ? lo + 8 - r.left : 0;
|
|
193
199
|
// Once per tap: measured again, an element with no layout would move forever.
|
|
194
200
|
setOffer({ ...offer, left: offer.left + (Math.abs(shift) >= 1 ? shift : 0), nudged: true });
|
|
195
|
-
}, [offer]);
|
|
201
|
+
}, [offer, rootId]);
|
|
196
202
|
useEffect(() => {
|
|
197
203
|
const root = document.getElementById(rootId);
|
|
198
204
|
if (!root)
|
package/lib/routes/artifacts.js
CHANGED
|
@@ -314,7 +314,7 @@ export function createArtifactRoutes(config) {
|
|
|
314
314
|
// Only a host that signs readers in can know one; the read never fails the page.
|
|
315
315
|
const reader = readerSecret() ? verifyGrant(readerSecret(), await readCookie(GRANT_COOKIE).catch(() => undefined)) : null;
|
|
316
316
|
const allow = config.readers && reader ? await config.readers.allowList(id) : [];
|
|
317
|
-
const d = config.readers ? decide(a.access, reader, allow) : { open: false, why: 'signed-out' };
|
|
317
|
+
const d = config.readers ? decide(a.access, reader, allow, isAdmin(reader)) : { open: false, why: 'signed-out' };
|
|
318
318
|
const header = (_jsxs("div", { className: "mx-auto max-w-2xl px-6 pt-24 pb-8 text-center sm:pt-28", 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 })] }));
|
|
319
319
|
if (!d.open) {
|
|
320
320
|
if (d.why === 'not-allowed' && config.readers) {
|
|
@@ -455,7 +455,7 @@ export function createArtifactRoutes(config) {
|
|
|
455
455
|
const a = await store.get(id);
|
|
456
456
|
if (!reader || !a?.access)
|
|
457
457
|
return back;
|
|
458
|
-
if (!decide(a.access, reader, await config.readers.allowList(id)).open)
|
|
458
|
+
if (!decide(a.access, reader, await config.readers.allowList(id), isAdmin(reader)).open)
|
|
459
459
|
return back;
|
|
460
460
|
await config.readers.acknowledge({ artifactId: id, reader, text: ackText(owner), country: request.headers.get('x-vercel-ip-country') ?? undefined });
|
|
461
461
|
return back;
|
|
@@ -487,7 +487,7 @@ export function createArtifactRoutes(config) {
|
|
|
487
487
|
if (!a?.access)
|
|
488
488
|
return new NextResponse(null, { status: 404 });
|
|
489
489
|
const allow = await config.readers.allowList(id);
|
|
490
|
-
if (!decide(a.access, reader, allow).open)
|
|
490
|
+
if (!decide(a.access, reader, allow, isAdmin(reader)).open)
|
|
491
491
|
return new NextResponse(null, { status: 403 });
|
|
492
492
|
const country = request.headers.get('x-vercel-ip-country') ?? undefined;
|
|
493
493
|
if (b.kind === 'beat') {
|
|
@@ -73,7 +73,7 @@ export function createStateRoutes(ctx) {
|
|
|
73
73
|
return { error: json({ error: 'this page is closed' }, 403) };
|
|
74
74
|
if (!reader)
|
|
75
75
|
return { error: json({ error: 'sign in to answer', signIn }, 401) };
|
|
76
|
-
if (!decide(a.access, reader, await ctx.readers.allowList(id)).open)
|
|
76
|
+
if (!decide(a.access, reader, await ctx.readers.allowList(id), isOwner(reader)).open)
|
|
77
77
|
return { error: json({ error: 'this page is not open to you' }, 403) };
|
|
78
78
|
// The page shows its body only after the agreement, so its answers wait for it too.
|
|
79
79
|
if (!(await ctx.readers.acknowledged(id, reader.email)))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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",
|