Public: anyone can read this; the crew can change it.

Revoking an invite link in the terminal asks first: `x` names the link, `y` revokes, `n` or esc keeps it open #7965

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/7960-revoking-an-invite-link-in-the-terminal → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/7960-revoking-an-invite-link-in-the-terminal was merged into main as 9c6861c.

Review

Sign in to review this change.
2 files changed +43 −4
src/tui/screens/invites.ts +16 −3
@@ -3,7 +3,7 @@
33 * state, when it was made and when it expires, as Invite links on /admin/limits
44 * shows them. `m` mints one from a number of days (1 to 30, 7 by default) and
55 * prints the link once, on this screen, until the next key of any kind; `x` revokes the
6 * selected open one. Both go through the actions, so the gate is theirs: an
6 * selected open one after a confirm (`y` revokes, `n` or esc keeps it). Both go through the actions, so the gate is theirs: an

This line is much longer than its neighbours. Reflow the paragraph.

77 * admin who is a person; anyone else sees nothing and mints nothing. The top
88 * line says who may sign up now, as MCP `signup` does; `s` picks open, invite
99 * or closed, shows from → to and changes it only after a confirm.
@@ -120,8 +120,21 @@ export function invitesScreen(f: Session, ctx: Ctx): Screen {
120120 } else if (e.key === "x") {
121121 const row = all[state.index];
122122 if (row === undefined) return false;
123 note = revoke(f.app, f.user, row.id).ok ? `Revoked ${row.id}.` : INVITE_NOT_OPEN;
124 ctx.invalidate();
123 if (row.state !== "open") note = INVITE_NOT_OPEN;
124 else
125 ctx.push(
126 confirmScreen(ctx, screen, {
127 title: `Revoke invite ${row.id}?`,
128 body: [`Expires ${row.expiresAt.slice(0, 10)}; holder can no longer sign up.`],
129 actions: [{ label: "Revoke", variant: "danger" }, { label: "Keep" }],
130 yesNo: true,
131 onChoose: (i) => {
132 if (i !== 0) return;
133 note = revoke(f.app, f.user, row.id).ok ? `Revoked ${row.id}.` : INVITE_NOT_OPEN;
134 ctx.invalidate();
135 },
136 }),
137 );
125138 } else if (e.key === "s") {
126139 const now = signup();
127140 if (!now.ok) return false;
test/tui-invites.test.ts +27 −1
@@ -91,8 +91,34 @@ describe("invite links in the terminal", () => {
9191 await h.press("j");
9292 expect(h.text()).not.toContain("invite_");
9393 await h.press("x");
94 expect(listInvites(forge.db)[0]!.state).toBe("revoked");
94 expect(h.text()).toContain(`Revoke invite ${made!.id}?`);
9595 expect(h.text()).not.toContain("invite_");
96 expect(listInvites(forge.db)[0]!.state).toBe("open");
97 await h.press("y");
98 expect(listInvites(forge.db)[0]!.state).toBe("revoked");
99 expect(h.text()).toContain(`Revoked ${made!.id}.`);
100 expect(h.text()).not.toContain("invite_");
101 await h.press("x");
102 expect(h.text()).not.toContain("Revoke invite");
103 expect(h.text()).toContain("not open");
104 h.stop();
105 });
106
107 test("n or esc on the revoke confirm keeps the link open", async () => {
108 const h = boot("alice");
109 await open(h);
110 await h.press("m");
111 await h.press("enter");
112 const made = listInvites(forge.db).find((i) => i.state === "open")!;
113 for (const k of ["n", "esc"]) {
114 await h.press("x");
115 expect(h.text()).toContain(`Revoke invite ${made.id}?`);
116 expect(h.text()).toContain(`Expires ${made.expiresAt.slice(0, 10)}; holder can no longer sign up.`);
117 expect(h.text()).not.toContain("invite_");
118 await h.press(k);
119 expect(h.text()).not.toContain("Revoke invite");
120 expect(listInvites(forge.db).find((i) => i.id === made.id)!.state).toBe("open");
121 }
96122 h.stop();
97123 });
98124

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs