snagset 0.1.35 → 0.1.36

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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Who in a studio may do what, the `member` seat wired.
3
+ *
4
+ * ONE TABLE, read by the server's routes and the dashboard alike, so the
5
+ * button a page draws and the answer the route gives cannot come from two
6
+ * ideas of a role. Until the rule was string compares scattered across
7
+ * a dozen route files (`role === "owner" || role === "admin"`), and every
8
+ * route that compared nothing quietly meant "anyone with a cookie", which was
9
+ * harmless only while every cookie belonged to an owner or an admin.
10
+ *
11
+ * The acts are coarse on purpose. A route asks one question in its own words
12
+ * ("may this person change the site's settings?"), and the finer rule that
13
+ * only a route can know — a member deletes their OWN comment, an admin
14
+ * invites only members, an admin revokes only their own agent token — stays
15
+ * in that route beside the data it needs.
16
+ *
17
+ * review comment, reply, edit and delete one's own words, change
18
+ * status, assign, start a round, move comments between rounds,
19
+ * add reviewers, sign in from the widget, keep one's own agent
20
+ * tokens. Everyone in the studio but a viewer.
21
+ * moderate delete somebody else's comment or thread.
22
+ * settings a site's settings and existence (add, delete, addresses,
23
+ * domain, audience, widget switch, guest status), its rounds'
24
+ * shape (rename, close, reopen, archive, restore, delete), its
25
+ * tracker connections and the issues made through them.
26
+ * invite mint, mail and withdraw invites. An admin's are for members
27
+ * only (`invitable`).
28
+ * tokens see every agent token in the studio, not only one's own.
29
+ * people change a role, remove somebody, invite an owner or an admin.
30
+ * studio rename the studio; what the instance is connected to.
31
+ *
32
+ * `viewer` is in the enum since 0001 and holds nothing here: a read-only
33
+ * seat is still undesigned (01 F-27), and an empty row is the honest way to
34
+ * say so — every route that asks refuses it.
35
+ */
36
+ export type StudioRole = "owner" | "admin" | "member" | "viewer";
37
+ export type StudioAct = "review" | "moderate" | "settings" | "invite" | "tokens" | "people" | "studio";
38
+ /** Whether a role may do an act. An unknown role (a value from a newer server) may do nothing. */
39
+ export declare function can(role: string | null | undefined, act: StudioAct): boolean;
40
+ /** The roles somebody may be given — by invite or by a role change — and in the order a picker lists them. */
41
+ export declare const ASSIGNABLE_ROLES: readonly ["owner", "admin", "member"];
42
+ export type AssignableRole = (typeof ASSIGNABLE_ROLES)[number];
43
+ /** Which roles this person may put on an invite: an owner any of the three, an admin a member, anyone else none. */
44
+ export declare function invitable(role: string | null | undefined): AssignableRole[];
45
+ /** One line per role, for the invite dialog and the role picker. */
46
+ export declare const ROLE_SUMMARY: Record<AssignableRole, string>;
47
+ /** "an owner", "an admin", "a member" — for a sentence. */
48
+ export declare function withArticle(role: string): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Who in a studio may do what, the `member` seat wired.
3
+ *
4
+ * ONE TABLE, read by the server's routes and the dashboard alike, so the
5
+ * button a page draws and the answer the route gives cannot come from two
6
+ * ideas of a role. Until the rule was string compares scattered across
7
+ * a dozen route files (`role === "owner" || role === "admin"`), and every
8
+ * route that compared nothing quietly meant "anyone with a cookie", which was
9
+ * harmless only while every cookie belonged to an owner or an admin.
10
+ *
11
+ * The acts are coarse on purpose. A route asks one question in its own words
12
+ * ("may this person change the site's settings?"), and the finer rule that
13
+ * only a route can know — a member deletes their OWN comment, an admin
14
+ * invites only members, an admin revokes only their own agent token — stays
15
+ * in that route beside the data it needs.
16
+ *
17
+ * review comment, reply, edit and delete one's own words, change
18
+ * status, assign, start a round, move comments between rounds,
19
+ * add reviewers, sign in from the widget, keep one's own agent
20
+ * tokens. Everyone in the studio but a viewer.
21
+ * moderate delete somebody else's comment or thread.
22
+ * settings a site's settings and existence (add, delete, addresses,
23
+ * domain, audience, widget switch, guest status), its rounds'
24
+ * shape (rename, close, reopen, archive, restore, delete), its
25
+ * tracker connections and the issues made through them.
26
+ * invite mint, mail and withdraw invites. An admin's are for members
27
+ * only (`invitable`).
28
+ * tokens see every agent token in the studio, not only one's own.
29
+ * people change a role, remove somebody, invite an owner or an admin.
30
+ * studio rename the studio; what the instance is connected to.
31
+ *
32
+ * `viewer` is in the enum since 0001 and holds nothing here: a read-only
33
+ * seat is still undesigned (01 F-27), and an empty row is the honest way to
34
+ * say so — every route that asks refuses it.
35
+ */
36
+ const ACTS = {
37
+ owner: ["review", "moderate", "settings", "invite", "tokens", "people", "studio"],
38
+ admin: ["review", "moderate", "settings", "invite", "tokens"],
39
+ member: ["review"],
40
+ viewer: [],
41
+ };
42
+ /** Whether a role may do an act. An unknown role (a value from a newer server) may do nothing. */
43
+ export function can(role, act) {
44
+ return !!role && (ACTS[role]?.includes(act) ?? false);
45
+ }
46
+ /** The roles somebody may be given — by invite or by a role change — and in the order a picker lists them. */
47
+ export const ASSIGNABLE_ROLES = ["owner", "admin", "member"];
48
+ /** Which roles this person may put on an invite: an owner any of the three, an admin a member, anyone else none. */
49
+ export function invitable(role) {
50
+ if (can(role, "people"))
51
+ return [...ASSIGNABLE_ROLES];
52
+ if (can(role, "invite"))
53
+ return ["member"];
54
+ return [];
55
+ }
56
+ /** One line per role, for the invite dialog and the role picker. */
57
+ export const ROLE_SUMMARY = {
58
+ owner: "Everything, including who is in the studio and what the instance is connected to.",
59
+ admin: "Everything about sites, rounds, integrations and comments; invites members.",
60
+ member: "Comments, status, assignees and rounds on every site; no settings.",
61
+ };
62
+ /** "an owner", "an admin", "a member" — for a sentence. */
63
+ export function withArticle(role) {
64
+ return role === "owner" ? "an owner" : role === "admin" ? "an admin" : role === "member" ? "a member" : `a ${role}`;
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snagset",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Click-to-comment review for a live website. One script tag, or one import. React, Vue, Next.js, Vite, Astro.",