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

A signed-out visitor sees Make it yours on every public app in /explore, and it carries them through sign-in to the plan of their copy #7862

Merged opened this

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

Merged

torque/forge:torque/7748-a-signed-out-visitor-sees-make-it-yours → main 4 commits fork 2 threads · 2 unresolved

  • Merged. torque/7748-a-signed-out-visitor-sees-make-it-yours was merged into main as 474b970.

Review

Sign in to review this change.
5 files changed +61 −3
README.md +2
@@ -324,6 +324,8 @@ An unknown `kind:` or `sort:` word is dropped and named after "Read as:", with
324324 the words it takes, on the page, in the terminal and in a `note` field of
325325 `/api/v1/explore` and the MCP `explore` tool; results still show.
326326
327 A signed-out visitor to `/explore` sees **Make it yours** on every public app (not on a private one, nor one only its writers may enter); it leads to sign-in and back to the plan of their copy, and nothing is copied until they confirm. Behind a platform edge the sign-in is the edge's; a newcomer it vouched for is sent to `/signup` and may need to come back. API and MCP rows say `copyable: true` for a guest too; making the copy needs a signed-in caller, and the terminal is always signed in and offers `Y` on the repository screen.
328
327329 ## On your machine
328330
329331 The `forge` command keeps a local copy of the parts you work on: **[docs/ON-YOUR-MACHINE.md](docs/ON-YOUR-MACHINE.md)**.
docs/STUDIO.md +3 −1
@@ -469,7 +469,9 @@ count of apps links there), `archived:yes` (put-away repositories too),
469469 and words to match. An agent's working fork is how the crew builds, not a
470470 find, and is left out; a private app's hours are told only to those who may
471471 enter it. Every app row offers **Make it yours** (a fork) to someone signed
472 in (in the terminal, `Y` on a repository screen: name, `tab` for visibility, the plan, then a confirm). The same rows: `GET /api/v1/explore?q=…` and the MCP tool `explore`
472 in, and a public app's row offers it to a signed-out visitor too, by way of
473 sign-in; the README says how. In the terminal, `Y` on a repository screen makes
474 it yours: name, `tab` for visibility, the plan, then a confirm. The same rows: `GET /api/v1/explore?q=…` and the MCP tool `explore`
473475 (`actions/explore.ts`). It is in the account menu and the palette, and the
474476 front page's "All public apps" leads here.
475477
src/actions/explore.ts +4 −1
@@ -380,7 +380,10 @@ function rank(
380380 };
381381
382382 const copyable = (repo: Repo): boolean => {
383 if (viewer === undefined || viewer.kind === "agent" || !isActive(viewer)) return false;
383 // A signed-out visitor is offered only public apps; the page they land on
384 // sends them to sign in first, and the copy is made after.
385 if (viewer === undefined) return records.get(repo.id)!.access === "public";
386 if (viewer.kind === "agent" || !isActive(viewer)) return false;
384387 if (repo.ownerId === viewer.id) return false;
385388 const record = records.get(repo.id)!;
386389 return (
test/edge-auth.test.ts +13
@@ -6,6 +6,7 @@ import { createApp, type App } from "../src/app.ts";
66 import { edgeIdentity, edgeLoginUrl, edgeLogoutUrl } from "../src/http/edge.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { createUser, getUserByName } from "../src/core/users.ts";
9 import { createRepo } from "../src/actions/repos.ts";
910 import { mintInvite } from "../src/core/invites.ts";
1011 import { setSignup } from "../src/settings/signup.ts";
1112 import { seedAdmin } from "./helpers/admin.ts";
@@ -89,6 +90,18 @@ describe("a forge behind the platform edge", () => {
8990 expect(page).not.toContain('href="/signin"');
9091 });
9192
93 test("a signed-out visitor on Make it yours is sent to the edge's sign-in, back to the same path; a vouched newcomer to /signup", async () => {
94 const olive = getUserByName(app.db, "olive")!;
95 const made = await createRepo(app, olive, { name: "loved", visibility: "public" });
96 if (!made.ok) throw new Error(String(made.error));
97 const path = "/olive/loved/make-it-yours";
98 const anon = await fetch(`${base}${path}`, { redirect: "manual" });
99 expect(anon.headers.get("location")).toBe(`/_plat/login?rd=${encodeURIComponent(path)}`);
100 const stranger = { "x-plat-user": "newcomer", "x-plat-edge-token": TOKEN };
101 const vouched = await fetch(`${base}${path}`, { headers: stranger, redirect: "manual" });
102 expect(vouched.headers.get("location")).toBe("/signup");
103 });
104
92105 test("an unknown edge login creates no user and is offered /signup", async () => {
93106 const stranger = { "x-plat-user": "newcomer", "x-plat-edge-token": TOKEN };
94107 expect((await fetch(`${base}/api/v1/user`, { headers: stranger })).status).toBe(401);
test/explore.test.ts +39 −1
@@ -232,7 +232,7 @@ describe("explore", () => {
232232 const page = await (await fetch(`${base}/explore?q=${encodeURIComponent("kind:app sort:ships")}`)).text();
233233 expect(page).toContain('value="kind:app sort:ships"');
234234 expect(page.indexOf('href="/bob/busy"')).toBeLessThan(page.indexOf('href="/alice/loved"'));
235 expect(page).not.toContain("Make it yours");
235 expect(page).toContain("Make it yours");
236236 expect(page).not.toContain("alice/secret");
237237 const signedIn = await (
238238 await fetch(`${base}/explore`, {
@@ -312,6 +312,44 @@ describe("explore", () => {
312312 expect(asAlice).not.toContain('href="/alice/loved/make-it-yours"');
313313 });
314314
315 test("Make it yours is offered to a signed-out visitor on public apps only, linking to the make-it-yours path", async () => {
316 const rows = (await api("kind:app")).items;
317 expect(rows.find((r) => r.name === "alice/loved").copyable).toBe(true);
318 // A public repository whose app only its writers enter is shown, not
319 // copyable; a private app is not among a guest's rows at all.
320 expect(rows.find((r) => r.name === "alice/gated").copyable).toBe(false);
321 expect(rows.find((r) => r.name === "alice/secret")).toBeUndefined();
322 const page = await (await fetch(`${base}/explore`)).text();
323 expect(page).toContain('href="/alice/loved/make-it-yours"');
324 expect(page).not.toContain('href="/alice/gated/make-it-yours"');
outdated

Vague: a guest must not be shown alice/secret at all. Assert secret is undefined.

325 expect(page).not.toContain("alice/secret");
326 });
327
328 test("the signed-out Make it yours link carries the visitor through sign-in to the plan of their copy, and makes no copy", async () => {
329 const path = "/alice/loved/make-it-yours";
330 const next = encodeURIComponent(path);
331 const res = await fetch(`${base}${path}`, { redirect: "manual" });
332 expect(res.status).toBe(303);
333 expect(res.headers.get("location")).toBe(`/signin?next=${next}`);
334 const copies = () => (app.db.query(`SELECT COUNT(*) AS n FROM repo WHERE owner_id = ?1`).get(bob.id) as { n: number }).n;
335 const before = copies();
336 const signedIn = await fetch(`${base}/signin?next=${next}`, {
337 method: "POST",
338 body: new URLSearchParams({ username: "bob", password: "a-good-passphrase" }),
339 redirect: "manual",
340 });
341 expect(signedIn.status).toBe(303);
342 expect(signedIn.headers.get("location")).toBe(path);
343 const cookie = (signedIn.headers.get("set-cookie") ?? "").split(";")[0]!;
344 const plan = await fetch(`${base}${signedIn.headers.get("location")}`, { headers: { cookie } });
345 expect(plan.status).toBe(200);
346 const body = await plan.text();
347 expect(body).toContain("Make it yours");
348 expect(body).toContain("Yours as");
349 // Only the plan: the copy is made by the POST.
350 expect(copies()).toBe(before);
351 });
352
315353 test("updated: narrows to what moved in the window, on the API and the page chips", async () => {
316354 const ago = async (name: string, days: number) => {
317355 const repo = await appOf(alice, name);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs