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

The secrets list and a person's sign-ins read name their Server-Timing phases #5018

Merged opened this

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

Merged

scout/forge:scout/5006-the-secrets-list-and-a-person-s-sign-ins → main 1 commit fork

  • Merged. scout/5006-the-secrets-list-and-a-person-s-sign-ins was merged into main as a9319f9.

Review

Sign in to review this change.
2 files changed +85 −12
src/http/api.ts +20 −12
@@ -2119,16 +2119,21 @@ export function apiRoutes(router: Router<Handler>): void {
21192119 // Where an account is signed in: its person, or the forge's owner for any
21202120 // person (`actions/signins.ts`). Anyone else finds no such account.
21212121 router.add("GET", "/api/v1/users/:username/signins", (ctx) => {
2122 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
21222123 const denied = requireAccount(ctx, { adminToken: true });
21232124 if (denied) return denied;
2124 const seen = accountSignIns(
2125 ctx.app,
2126 ctx.auth.user,
2127 ctx.params.username!,
2128 cookieValue(ctx.request, "forge_session"),
2125 const seen = timePhase(ctx.request, "signins", () =>
2126 accountSignIns(
2127 ctx.app,
2128 ctx.auth.user,
2129 ctx.params.username!,
2130 cookieValue(ctx.request, "forge_session"),
2131 ),
21292132 );
21302133 if (!seen.ok) return notFound();
2131 return json({ tokens: seen.value.tokens, sessions: seen.value.sessions });
2134 return timePhase(ctx.request, "render", () =>
2135 json({ tokens: seen.value.tokens, sessions: seen.value.sessions }),
2136 );
21322137 });
21332138
21342139 router.add("DELETE", "/api/v1/users/:username/sessions/:id", (ctx) => {
@@ -5662,16 +5667,19 @@ export function apiRoutes(router: Router<Handler>): void {
56625667 // value. `ref` names the owner: an org's name, or a repository's
56635668 // `owner/name`; the forge scope takes none.
56645669 router.add("GET", "/api/v1/secrets/:scope", (ctx) => {
5670 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
56655671 const denied = requireScope(ctx, "admin");
56665672 if (denied) return denied;
5667 const listed = listSecrets(
5668 ctx.app,
5669 ctx.auth.user,
5670 ctx.params.scope ?? "",
5671 ctx.url.searchParams.get("ref") ?? "",
5673 const listed = timePhase(ctx.request, "secrets", () =>
5674 listSecrets(
5675 ctx.app,
5676 ctx.auth.user,
5677 ctx.params.scope ?? "",
5678 ctx.url.searchParams.get("ref") ?? "",
5679 ),
56725680 );
56735681 if (!listed.ok) return respondAccess(listed.error, 422);
5674 return json({ items: listed.value, next_cursor: null });
5682 return timePhase(ctx.request, "render", () => json({ items: listed.value, next_cursor: null }));
56755683 });
56765684
56775685 router.add("PUT", "/api/v1/secrets/:scope/:name", async (ctx) => {
test/secrets-signins-timing.test.ts added +65
@@ -0,0 +1,65 @@
1 /** The secrets list and a person's sign-ins name their Server-Timing phases. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { serve, type Servers } from "../src/http/server.ts";
8 import { seedAdmin } from "./helpers/admin.ts";
9
10 let dataDir = "";
11 let app: App, servers: Servers, bearer = "";
12
13 beforeAll(async () => {
14 dataDir = await mkdtemp(join(tmpdir(), "forge-sstime-"));
15 app = createApp({ dataDir, port: 0 });
16 servers = await serve(app);
17 await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
18 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
19 method: "POST",
20 headers: { "content-type": "application/json" },
21 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
22 });
23 bearer = ((await minted.json()) as { token: string }).token;
24 });
25
26 afterAll(async () => {
27 servers.stop();
28 app.close();
29 await rm(dataDir, { recursive: true, force: true });
30 });
31
32 async function phases(path: string, auth = true): Promise<{ status: number; names: string[] }> {
33 const res = await fetch(`${servers.url}${path}`, auth ? { headers: { authorization: `Bearer ${bearer}` } } : {});
34 await res.arrayBuffer();
35 const names = (res.headers.get("server-timing") ?? "").split(", ").filter(Boolean).map((p) => p.split(";")[0]!);
36 return { status: res.status, names };
37 }
38
39 test("the secrets list names session, secrets, render in order", async () => {
40 const got = await phases("/api/v1/secrets/forge");
41 expect(got.status).toBe(200);
42 expect(got.names.filter((n) => ["session", "secrets", "render"].includes(n))).toEqual(["session", "secrets", "render"]);
43 });
44
45 test("a refused secrets list still names session", async () => {
46 const got = await phases("/api/v1/secrets/forge", false);
47 expect(got.status).toBeGreaterThanOrEqual(401);
48 expect(got.names).toContain("session");
49 expect(got.names).not.toContain("secrets");
50 });
51
52 test("a person's sign-ins name session, signins, render in order", async () => {
53 const got = await phases("/api/v1/users/alice/signins");
54 expect(got.status).toBe(200);
55 expect(got.names.filter((n) => ["session", "signins", "render"].includes(n))).toEqual(["session", "signins", "render"]);
56 });
57
58 test("a refused or missing sign-ins read still names session", async () => {
59 const anon = await phases("/api/v1/users/alice/signins", false);
60 expect(anon.status).toBe(401);
61 expect(anon.names).toContain("session");
62 const none = await phases("/api/v1/users/nobody/signins");
63 expect(none.status).toBe(404);
64 expect(none.names).toContain("session");
65 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs