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

The first-login jargon test covers the site menu and command palette labels, and what it finds is reworded #10815

Merged opened this

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

Merged

mason/forge:mason/10791-the-first-login-jargon-test-covers-the-s → main 8 commits fork 10 threads · 10 unresolved

  • Merged. mason/10791-the-first-login-jargon-test-covers-the-s was merged into main as 7f00b36.

Review

Sign in to review this change.
54 files changed +167 −197
docs/design/areas/palette.md +1 −1
@@ -1,4 +1,4 @@
1 # Command palette
1 # Jump to anywhere
22
33 The command palette is the keyboard-first fuzzy-find overlay (`⌘K` / `Ctrl K`, or the topbar's search button): the §5 `.palette` dialog, with its input, list and foot drawn by `layout.ts` and its results drawn by `openPalette()` in `src/web/client.ts`. Without script it degrades to `/search`. The file finder (`openFinder()`, `.dialog.palette.finder`) draws its rows and its empty note with the same classes. The rules live in `style/components.ts`.
44
docs/design/areas/settings.md +2 −2
@@ -94,8 +94,8 @@ accounts are `row()`s in `<ul class="list setting-list" id="accounts-list">` und
9494 (Sign-ins, Verify, Suspend, Remove) in the row's `after` slot; the mail, ownerless-organization, sign-in reach and
9595 confirm/refusal notices are plain `<section class="settings-block">` blocks with an `<h2>`. `test/accounts-rows.test.ts` holds the shape.
9696
97 Settings › Origins (`src/web/pages/origins.ts`) adds no component class. As on the home page it is headings over rows: Connected
98 origins, Frozen mirrors, Trusted internal hosts (admin) and Connect an origin are each a `<section class="settings-block">`
97 Settings › Connected accounts (`src/web/pages/origins.ts`) adds no component class. As on the home page it is headings over rows: Connected
98 accounts, Frozen mirrors, Trusted internal hosts (admin) and Connect an account are each a `<section class="settings-block">`
9999 with an `<h2>`; each origin and each frozen mirror is a `row()` in a `.list`; with no origins, one plain sentence says how to
100100 connect one. An origin's error, notes (re-authorize, another forge, writes paused) and acts sit in the row's `after`
101101 slot; the list carries the attribute `data-origins` (not a class), which stacks that slot as a full-width block under
src/actions/migrate.ts +1 −1
@@ -269,7 +269,7 @@ export function migrateMessage(error: MigrateError, limit = AT_REPOSITORY_LIMIT)
269269 case "no_such_origin":
270270 return "That origin is not one you have connected, or it has no such repository.";
271271 case "unreachable":
272 return "The origin could not be reached. Try again, or reconnect it under Settings → Origins.";
272 return "The origin could not be reached. Try again, or reconnect it under Settings → Connected accounts.";
273273 case "no_such_agent":
274274 return NO_SUCH_AGENT;
275275 case "not_a_builder":
src/actions/origin_words.ts +1 −1
@@ -1,6 +1,6 @@
11 /**
22 * What an origin's sync and disconnect say, in one place so the page in
3 * Settings › Origins and the terminal's Origins screen say the same thing.
3 * Settings › Connected accounts and the terminal's Connected accounts screen say the same thing.
44 */
55
66 export const NO_SUCH_ORIGIN = "No such origin.";
src/apps/feature.ts +1
@@ -34,6 +34,7 @@ const BUILT_IN_MENU_LABELS = [
3434 "your profile", "repositories", "apps", "notes", "talk", "explore", "packs", "displays", "map", "features",
3535 "new app", "new repository", "new issue", "new organization", "migrate", "settings", "access tokens", "ssh keys",
3636 "origins", "boards", "crew", "system memory", "help", "sign out", "log out", "sign in", "admin", "administration",
37 "access codes", "computer keys", "connected accounts", "jump to anywhere",
3738 "theme", "create", "new forge", "create new", "appearance and help", "features from apps", "keyboard shortcuts",
3839 ];
3940
src/http/mcp/tools/origins.ts +1 −1
@@ -7,7 +7,7 @@ import { good, bad, type Tool } from "../shared.ts";
77
88 export const tool: Tool = {
99 name: "origins",
10 doc: "`{ action, id?, hosts? }`: `list` your connected origins, `repos` the repositories one holds (`id`; `mirroredAs` says which are already here), `sync` one now, or `hosts`: with no `hosts`, read the internal hosts an origin may live on (a forge admin, `fromEnv` read-only), with `hosts` (one per line) replace them as Settings → Origins does, a rejected line named in the error; the same as `GET /api/v1/user/origins`, `…/:id/repos` and `POST …/:id/sync`, for you only, an admin token as there; never a credential in an answer",
10 doc: "`{ action, id?, hosts? }`: `list` your connected origins, `repos` the repositories one holds (`id`; `mirroredAs` says which are already here), `sync` one now, or `hosts`: with no `hosts`, read the internal hosts an origin may live on (a forge admin, `fromEnv` read-only), with `hosts` (one per line) replace them as Settings → Connected accounts does, a rejected line named in the error; the same as `GET /api/v1/user/origins`, `…/:id/repos` and `POST …/:id/sync`, for you only, an admin token as there; never a credential in an answer",
1111 description:
1212 "Origins: list, repos, sync, hosts.",
1313 inputSchema: {
src/http/mcp/trusted_hosts.ts +1 −1
@@ -5,7 +5,7 @@ import { good, bad, type Answer, type Args, type Tool } from "./shared.ts";
55
66 type Ctx = Parameters<Tool["run"]>[0];
77
8 /** The `origins` tool's `hosts` action: Settings → Origins' internal-host list, for a forge admin's admin-scope token. */
8 /** The `origins` tool's `hosts` action: Settings → Connected accounts' internal-host list, for a forge admin's admin-scope token. */
99 export function trustedHosts(ctx: Ctx, user: User, args: Args): Answer {
1010 if (!mayEditTrustedHosts(user)) return bad("forbidden", "Only a forge admin sets this.");
1111 if (!scopeAllows(ctx.auth.scope ?? "ro", "admin"))
src/origins/credential.ts +1 −1
@@ -255,7 +255,7 @@ export const READ_ONLY =
255255
256256 /** What a write through a paused origin is told, everywhere. */
257257 export const WRITES_PAUSED =
258 "This forge could not verify this connection against the operator's rules, so writes through it are paused. Reads and sync go on. Check it again under Settings → Origins.";
258 "This forge could not verify this connection against the operator's rules, so writes through it are paused. Reads and sync go on. Check it again under Settings → Connected accounts.";
259259
260260 /**
261261 * Why writes through this origin are paused, or undefined when they are
src/tui/forge.ts +5 −5
@@ -403,7 +403,7 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] {
403403 if (session.signedIn)
404404 commands.push({
405405 id: "ssh-keys",
406 title: "SSH keys",
406 title: "Computer keys",
407407 hint: "list, add and remove the keys that sign in as you",
408408 run: () => ctx.push(sshKeysScreen(session, ctx)),
409409 });
@@ -429,11 +429,11 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] {
429429 hint: "your browsers and tokens; d signs a browser out",
430430 run: () => ctx.push(signInsScreen(session, ctx)),
431431 },
432 // Settings → Origins' OAuth apps and MCP `origin_apps` (#7291).
432 // Settings → Connected accounts' OAuth apps and MCP `origin_apps` (#7291).
433433 {
434434 id: "origin-apps",
435 title: "Origin apps",
436 hint: "your OAuth apps for connecting origins; a adds, d removes",
435 title: "Connection apps",
436 hint: "your OAuth apps for connecting accounts; a adds, d removes",
437437 run: () => ctx.push(originAppsScreen(session, ctx)),
438438 },
439439 );
@@ -538,7 +538,7 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] {
538538 hint: "declared flags and their live values",
539539 run: () => ctx.push(flagsScreen(session, ctx)),
540540 });
541 // Settings → Origins' trusted internal hosts: the same admin gate as the page.
541 // Settings → Connected accounts' trusted internal hosts: the same admin gate as the page.
542542 if (mayEditTrustedHosts(session.user))
543543 commands.push({
544544 id: "trusted-hosts",
src/tui/screens/connections.ts +7 −7
@@ -1,6 +1,6 @@
11 /**
2 * Origins in the terminal: the hosts your mirrors come from, as
3 * Settings › Origins lists them. `s` syncs the selected one now, `d` disconnects
2 * Connected accounts in the terminal: the hosts your mirrors come from, as
3 * Settings › Connected accounts lists them. `s` syncs the selected one now, `d` disconnects
outdated

Stale comment: 'Settings › Origins' should now read Connected accounts.

44 * it after a confirm — through `actions/origins.ts`, in `origin_words.ts`'s words.
55 */
66
@@ -27,7 +27,7 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen {
2727 const counts = () => (f.user === undefined ? new Map<string, { count: number }>() : mirrorsByOrigin(f.app.db, f.user.id));
2828
2929 const screen: Screen = {
30 title: "Origins",
30 title: "Connected accounts",
3131 keys: [binding(["j", "k"], "move cursor"), binding(["c"], "connect"), binding(["s"], "sync now"), binding(["v"], "verify again"), binding(["d"], "disconnect"), binding(["esc"], "back")],
3232 render(area, s) {
3333 const c = ctx.theme.c;
@@ -68,14 +68,14 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen {
6868 const user = f.user;
6969 ctx.push(
7070 promptScreen(ctx, screen, {
71 title: "Connect an origin",
71 title: "Connect an account",
7272 label: "Host or address (a forge, Forgejo, Gitea, GitHub)",
7373 placeholder: "forge.example.com",
7474 onSubmit: (hostText) => {
7575 if (hostText === "") return "Type the address of the forge.";
7676 return later(ctx, () =>
7777 promptScreen(ctx, screen, {
78 title: "Connect an origin",
78 title: "Connect an account",
7979 label: `Access token for ${hostText}`,
8080 hint: "Made in that forge's settings; kept encrypted here.",
8181 mask: true,
@@ -83,7 +83,7 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen {
8383 if (token.trim() === "") return "Paste the token.";
8484 return later(ctx, () =>
8585 promptScreen(ctx, screen, {
86 title: "Connect an origin",
86 title: "Connect an account",
8787 label: "Read-only? Type yes to never write back to it",
8888 hint: "Leave empty to connect the ordinary way.",
8989 onSubmit: (answer) => {
@@ -162,7 +162,7 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen {
162162 /** Origins: a signed-in person, not an agent. */
163163 export const entry: PaletteEntry = {
164164 id: "origins",
165 title: "Origins",
165 title: "Connected accounts",
166166 hint: "the hosts your mirrors come from",
167167 offered: (session) => session.user !== undefined && session.user.kind !== "agent",
168168 run: (session, ctx) => ctx.push(connectionsScreen(session, ctx)),
src/tui/screens/migrate.ts +1 −1
@@ -311,7 +311,7 @@ export function migrateScreen(f: Session, ctx: Ctx): Screen {
311311 : rows.length === 0 && unreachable.length > 0
312312 ? `${down} Try again, or press u to paste a URL.`
313313 : rows.length === 0
314 ? "No connected origin has repositories. Press u to paste a URL, or connect one on the web under Settings → Origins."
314 ? "No connected origin has repositories. Press u to paste a URL, or connect one on the web under Settings → Connected accounts."
315315 : "Nothing matches.",
316316 list!.w,
317317 ),
src/tui/screens/originapps.ts +1 −1
@@ -93,7 +93,7 @@ export function originAppsScreen(f: Session, ctx: Ctx): Screen {
9393 }));
9494
9595 const screen: Screen = {
96 title: "Origin apps",
96 title: "Connection apps",
9797 keys: [binding(["a"], "register one"), binding(["d"], "remove"), binding(["j", "k"], "move cursor")],
9898 render(area, s) {
9999 const c = ctx.theme.c;
src/tui/screens/signins.ts +7 −7
@@ -104,7 +104,7 @@ function hardWrap(text: string, width: number): string[] {
104104 function secretScreen(ctx: Ctx, secret: string, market: { add: string; install: string } | null): Screen {
105105 let copied = "";
106106 return {
107 title: "New access token",
107 title: "New access code",
108108 keys: [
109109 binding(["y"], "copy it"),
110110 ...(market === null ? [] : [binding(["a"], "copy the marketplace line"), binding(["i"], "copy the install line")]),
@@ -166,7 +166,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
166166
167167 const end = (picked: SignIn): void => {
168168 if (picked.kind === "terminal") {
169 note = "This terminal cannot be ended from here: disconnect, or remove its key in Settings → SSH keys.";
169 note = "This terminal cannot be ended from here: disconnect, or remove its key in Settings → Computer keys.";
170170 ctx.invalidate();
171171 return;
172172 }
@@ -222,7 +222,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
222222 const confirmAdmin = (name: string) =>
223223 later(ctx, () =>
224224 confirmScreen(ctx, screen, {
225 title: "Make an admin access token",
225 title: "Make an admin access code",
226226 body: [`${name} will act as admin: y to make it, n to cancel.`],
227227 actions: [{ label: "Make it", variant: "danger" }, { label: "Cancel", variant: "ghost" }],
228228 yesNo: true,
@@ -237,7 +237,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
237237 const askScope = (name: string, isAdmin: boolean) =>
238238 later(ctx, () =>
239239 promptScreen(ctx, screen, {
240 title: "Make an access token",
240 title: "Make an access code",
241241 label: isAdmin ? "Scope: ro, rw or admin" : "Scope: ro or rw",
242242 value: "ro",
243243 onSubmit: (word) => {
@@ -252,7 +252,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
252252
253253 const askName = (isAdmin: boolean) =>
254254 promptScreen(ctx, screen, {
255 title: "Make an access token",
255 title: "Make an access code",
256256 label: "Name",
257257 value: "token",
258258 onSubmit: (name) => {
@@ -323,7 +323,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
323323 keys: [
324324 binding(["j", "k"], "move cursor"),
325325 binding(["d"], "sign a browser out or revoke a token"),
326 binding(["m"], "make an access token"),
326 binding(["m"], "make an access code"),
327327 binding(["p"], "change your password"),
328328 ],
329329 render(area, s) {
@@ -337,7 +337,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
337337 ? "Sign in to see where you are signed in."
338338 : f.user.kind === "agent"
339339 ? "Where you are signed in. d signs a browser out or revokes a token."
340 : "Where you are signed in. d signs a browser out or revokes a token, m makes an access token, p changes your password.";
340 : "Where you are signed in. d signs a browser out or revokes a token, m makes an access code, p changes your password.";
341341 s.text(head!.x, head!.y, truncate(text, head!.w), style(c.mutedFg, c.bg), head!.x + head!.w);
342342 drawList(s, body!, c, state, rows(all));
343343 },
src/tui/screens/sshkeys.ts +1 −1
@@ -30,7 +30,7 @@ export function sshKeysScreen(f: Session, ctx: Ctx): Screen {
3030 }));
3131
3232 const screen: Screen = {
33 title: "SSH keys",
33 title: "Computer keys",
3434 keys: [binding(["j", "k"], "move cursor"), binding(["a"], "add"), binding(["e"], "rename"), binding(["d"], "remove")],
3535 render(area, s) {
3636 const c = ctx.theme.c;
src/web/client.ts +1 −1
@@ -1618,7 +1618,7 @@ const shellCore = /* js */ `
16181618 // ---- help overlay, generated from the shortcuts actually on the page ----
16191619 var STATIC_KEYS = [
16201620 ['j / k', 'Move between rows'], ['o or Enter', 'Open the focused row'],
1621 ['/', 'Focus search'], ['Cmd/Ctrl K', 'Command palette'], [':', 'Go to a page by name'], ['?', 'This help'],
1621 ['/', 'Focus search'], ['Cmd/Ctrl K', 'Jump to anywhere'], [':', 'Go to a page by name'], ['?', 'This help'],
16221622 ['Esc', 'Leave a box, close a menu or dialog'],
16231623 ];
16241624 // The rows a page earns by what it carries: a box to type in, a form
src/web/layout.ts +11 −42
@@ -347,9 +347,9 @@ function userMenu(
347347 ...forgeDoor(ctx),
348348 { separator: true, label: "" },
349349 { label: "Settings", href: "/settings", icon: "settings" },
outdated

Principle 8 / navigation: 'Access codes' opens a page still headed 'Access tokens'. Reword the destination page and settings sidebar too, or the label does not lead where it says.

350 { label: "Access tokens", href: "/settings/tokens", icon: "key" },
351 { label: "SSH keys", href: "/settings/keys", icon: "lock" },
352 { label: "Origins", href: "/settings/origins", icon: "globe" },
350 { label: "Access codes", href: "/settings/tokens", icon: "key" },
outdated

'Computer keys' opens a page headed 'SSH keys' (settings sidebar, site.ts:204, TUI screen title in src/tui/forge.ts:406). Reword those so both doors agree.

351 { label: "Computer keys", href: "/settings/keys", icon: "lock" },
outdated

'Connected accounts' opens 'Origins'. Reword the page heading and sidebar to match.

352 { label: "Connected accounts", href: "/settings/origins", icon: "globe" },
353353 ...(!user.isAdmin && administersAny(ctx.app.db, user)
354354 ? [{ label: "Boards", href: "/boards", icon: "activity" as const }]
355355 : []),
@@ -410,9 +410,9 @@ const STATIC_PALETTE: PaletteItem[] = [
410410 { t: "Migrate a repository", h: "/migrate", c: "Create" },
411411 { t: "New organization", h: "/organizations/new", c: "Create" },
412412 { t: "Account settings", h: "/settings", c: "Settings" },
413 { t: "Access tokens", h: "/settings/tokens", c: "Settings" },
414 { t: "SSH keys", h: "/settings/keys", c: "Settings" },
415 { t: "Origins", h: "/settings/origins", c: "Settings" },
413 { t: "Access codes", h: "/settings/tokens", c: "Settings" },
414 { t: "Computer keys", h: "/settings/keys", c: "Settings" },
415 { t: "Connected accounts", h: "/settings/origins", c: "Settings" },
416416 { t: "Your shells", h: "/settings/shells", c: "Settings" },
417417 ];
418418
@@ -576,33 +576,10 @@ function paletteIndex(ctx: Ctx, extra: PaletteItem[]): Html {
576576 * one that should close the dialog.
577577 */
578578 function overlays(): Html {
579 return html`<dialog
580 class="dialog palette"
581 id="palette"
582 role="dialog"
583 aria-modal="true"
584 aria-label="Command palette"
585 >
586 <input
587 class="input palette-input"
588 type="text"
589 role="combobox"
590 aria-expanded="true"
591 aria-haspopup="listbox"
592 aria-controls="palette-list"
593 aria-autocomplete="list"
594 placeholder="Jump to a repository, page or action…"
595 aria-label="Command palette"
596 autocomplete="off"
597 autocapitalize="off"
598 spellcheck="false"
599 />
600 <div
601 class="palette-list"
602 id="palette-list"
603 role="listbox"
604 aria-label="Results"
605 ></div>
579 // One line per element: this markup ships on every page, so its indentation is bytes too.
580 return html`<dialog class="dialog palette" id="palette" role="dialog" aria-modal="true" aria-label="Jump to anywhere">
581 <input class="input palette-input" type="text" role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-controls="palette-list" aria-autocomplete="list" placeholder="Jump to a repository, page or action…" aria-label="Jump to anywhere" autocomplete="off" autocapitalize="off" spellcheck="false" />
582 <div class="palette-list" id="palette-list" role="listbox" aria-label="Results"></div>
606583 <div class="palette-foot">
607584 ${kbd(["↑", "↓"])} navigate ${kbd(["↵"])} open ${kbd(["⌘", "↵"])} its app ${kbd(["esc"])} close
608585 </div>
@@ -736,15 +713,7 @@ function footer(ctx: Ctx, own?: Html): Html {
736713 ${ctx.auth.user !== undefined && ctx.auth.user.kind !== "org"
737714 ? html`<a class="btn" data-variant="link" data-size="sm" href="/#composer">Tell the crew</a>`
738715 : ""}
739 <button
740 class="btn js-only"
741 data-variant="link"
742 data-size="sm"
743 type="button"
744 data-palette
745 >
746 Command palette
747 </button>
716 <button class="btn js-only" data-variant="link" data-size="sm" type="button" data-palette>Jump to anywhere</button>
748717 </footer>`;
749718 }
750719
src/web/pages/account.ts +19 −19
@@ -429,7 +429,7 @@ function profileView(ctx: Ctx, values: ProfileForm): Html {
429429 ],
430430 })}
431431 <div class="form-actions">
432 ${btn("Save profile", { variant: "primary" })}
432 ${btn("Save", { variant: "primary" })}
433433 </div>
434434 ${refusal(values.errors?.form)}
435435 </form>`,
@@ -751,7 +751,7 @@ function tokensView(ctx: Ctx): Html {
751751 listSessions(ctx.app.db, ctx.auth.user!.id, cookieValue(ctx.request, "forge_session")),
752752 );
753753 const list = tokens.length === 0
754 ? html`<p class="muted">No access tokens yet. Mint one when a script, a CI job, or another machine needs to act as you.</p>`
754 ? html`<p class="muted">No access codes yet. Mint one when a script, a CI job, or another machine needs to act as you.</p>`
755755 : html`<ul class="list setting-list">
756756 ${tokens.map((t) =>
757757 row({
@@ -766,18 +766,18 @@ function tokensView(ctx: Ctx): Html {
766766 </ul>`;
767767 return html`${help([
768768 [
769 "Access tokens",
770 "Use a token as the password when pushing over HTTPS, or as a Bearer token for the API. Your account password works for git too — tokens are for scoped or automated access.",
769 "Access codes",
770 "Use an access code as the password when pushing over HTTPS, or to call the API (sent as a Bearer header). Your account password works for git too — access codes are for scoped or automated access.",
outdated

Principle 8: this help text still says 'token' under an 'Access codes' heading. Use one word.

771771 ],
772 ["Access token name", "Name it after where it lives — laptop, CI, deploy box."],
773 ["Scope", "A token can never mint another token, whatever its scope."],
772 ["Access code name", "Name it after where it lives — laptop, CI, deploy box."],
773 ["Scope", "An access code can never make another access code, whatever its scope."],
774774 ])}${connectCard(ctx, readFlash(ctx.request)?.code)}
775 ${block("Access tokens", list)}
775 ${block("Access codes", list)}
776776 ${block(
777 "New access token",
777 "New access code",
778778 html`<form method="post" action="/settings/tokens">
779779 ${uiField({
780 label: "Access token name",
780 label: "Access code name",
781781 name: "name",
782782 required: true,
783783 })}
@@ -797,14 +797,14 @@ function tokensView(ctx: Ctx): Html {
797797 <label class="check">
798798 <input type="checkbox" name="aiAdmin" required />
799799 <span
800 >I mean to give this token admin: you are the forge's admin,
800 >I mean to give this access code admin: you are the forge's admin,
801801 so whatever holds it — an AI included — acts as admin.</span
802802 >
803803 </label>
804804 </div>`
805805 : null}
806806 <div class="form-actions">
807 ${btn("Generate access token", { variant: "primary" })}
807 ${btn("Generate access code", { variant: "primary" })}
808808 </div>
809809 </form>`,
810810 )}
@@ -816,7 +816,7 @@ export function tokensPage(ctx: Ctx): Response {
816816 if (!ctx.auth.user) return signInFirst(ctx);
817817 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
818818 const body = tokensView(ctx);
819 return timePhase(ctx.request, "render", () => accountShell(ctx, "tokens", "Access tokens", body));
819 return timePhase(ctx.request, "render", () => accountShell(ctx, "tokens", "Access codes", body));
820820 }
821821
822822 // POST /settings/tokens
@@ -826,7 +826,7 @@ export async function tokenCreate(ctx: Ctx): Promise<Response> {
826826 // the platform edge's sign-in, or the password — never from another token
827827 // (no privilege bootstrapping). readForm below refuses a cross-site post.
828828 if (!isAccountItself(ctx.auth)) {
829 return forbiddenPage(ctx, "A token is made from your own signed-in account, never by another token.");
829 return forbiddenPage(ctx, "An access code is made from your own signed-in account, never by another access code.");
830830 }
831831 const form = await readForm(ctx, "admin");
832832 if (form instanceof Response) return form;
@@ -835,7 +835,7 @@ export async function tokenCreate(ctx: Ctx): Promise<Response> {
835835 if (ctx.auth.user.isAdmin && field(form, "aiAdmin") !== "on")
836836 return flashTo(ctx, "/settings/tokens", {
837837 tone: "danger",
838 message: "You are the forge's admin: tick that you mean to give this token admin, or make it from your everyday account.",
838 message: "You are the forge's admin: tick that you mean to give this access code admin, or make it from your everyday account.",
839839 });
840840 const scope = field(form, "scope");
841841 const chosen: TokenScope = scope === "ro" || scope === "admin" ? scope : "rw";
@@ -907,9 +907,9 @@ function keysView(
907907 >) — ed25519, ecdsa, or rsa (2048 bits or more).
908908 </p>
909909 ${block(
910 "SSH keys",
910 "Computer keys",
911911 keys.length === 0
912 ? html`<p class="muted">No SSH keys yet. Add one and git over SSH stops asking for a password.</p>`
912 ? html`<p class="muted">No computer keys yet. Add one and git over SSH stops asking for a password.</p>`
913913 : html`<ul class="list setting-list">
914914 ${keys.map((k) =>
915915 row({
@@ -940,7 +940,7 @@ function keysView(
940940 placeholder: "ssh-ed25519 AAAA…",
941941 })}
942942 <div class="form-actions">
943 ${btn("Add SSH key", { variant: "primary" })}
943 ${btn("Add computer key", { variant: "primary" })}
944944 </div>
945945 ${refusal(values.error)}
946946 </form>`,
@@ -952,7 +952,7 @@ export function keysPage(ctx: Ctx): Response {
952952 if (!ctx.auth.user) return signInFirst(ctx);
953953 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
954954 const body = keysView(ctx, { name: "", key: "" });
955 return timePhase(ctx.request, "render", () => accountShell(ctx, "keys", "SSH keys", body));
955 return timePhase(ctx.request, "render", () => accountShell(ctx, "keys", "Computer keys", body));
956956 }
957957
958958 // POST /settings/keys
@@ -971,7 +971,7 @@ export async function keyCreate(ctx: Ctx): Promise<Response> {
971971 return accountShell(
972972 ctx,
973973 "keys",
974 "SSH keys",
974 "Computer keys",
975975 keysView(ctx, {
976976 name,
977977 key,
src/web/pages/branches.ts +1 −1
@@ -342,7 +342,7 @@ export const CREATE_ERRORS: Record<string, string> = {
342342 origin_protected:
343343 "The operator protects this repository at its origin, so no branch is pushed to it from here. Propose the change from your fork.",
344344 origin_paused:
345 "This connection is not verified yet, so writes through it are paused. Nothing was created. Check it again under Settings → Origins.",
345 "This connection is not verified yet, so writes through it are paused. Nothing was created. Check it again under Settings → Connected accounts.",
346346 forbidden: "You need write access to create a branch.",
347347 unauthorized: "You need write access to create a branch.",
348348 not_found: "You need write access to create a branch.",
src/web/pages/browse.ts +1 −1
@@ -953,7 +953,7 @@ export function forkFailure(error: string): string {
953953 if (error === "origin_read_only")
954954 return "This account was connected read-only, so nothing is written through it.";
955955 if (error === "origin_paused")
956 return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Origins.";
956 return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Connected accounts.";
957957 if (error === "mirror_failed")
958958 return "The fork was made, but a local copy of it could not be.";
959959 if (error === "unauthorized") return "Sign in first.";
src/web/pages/edit.ts +1 −1
@@ -131,7 +131,7 @@ export function editErrorText(error: unknown): string {
131131 if (error === "origin_read_only")
132132 return "This account was connected read-only, so nothing is written through it.";
133133 if (error === "origin_paused")
134 return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Origins.";
134 return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Connected accounts.";
135135 return "That edit could not be saved.";
136136 }
137137 const e = error as { kind?: string; branch?: string; message?: string };
src/web/pages/frozen.ts +2 −2
@@ -70,7 +70,7 @@ export function frozenBlock(ctx: Ctx, repo: Repo): Html | null {
7070 // Only the viewer's own working origins on the same site — scheme, host
7171 // and port: reconnecting lends the mirror a credential, nobody can lend
7272 // one that is not theirs, and one that needs re-authorizing has nothing
73 // to lend until it is (which, done under Settings → Origins, reconnects
73 // to lend until it is (which, done under Settings → Connected accounts, reconnects
7474 // this mirror by itself).
7575 const origins =
7676 admin && user !== undefined
@@ -87,7 +87,7 @@ export function frozenBlock(ctx: Ctx, repo: Repo): Html | null {
8787 : origins.length === 0
8888 ? html`Connect an
8989 account${host === "" ? null : html` at ${hostLabel(d.htmlUrl)}`} under
90 <a href="${connect}">Settings → Origins</a>: connecting one
90 <a href="${connect}">Settings → Connected accounts</a>: connecting one
9191 reconnects this mirror by itself.`
9292 : html`<form
9393 method="post"
src/web/pages/migrate.ts +2 −2
@@ -291,7 +291,7 @@ function accounts(ctx: Ctx, origins: readonly OriginChoices[]): Html {
291291 : html`<p class="muted small">
292292 ${icon("github")} Connect GitHub needs this forge's GitHub
293293 application, set up once under
294 <a href="/settings/origins">Settings → Origins</a>; a token pasted
294 <a href="/settings/origins">Settings → Connected accounts</a>; a token pasted
295295 there works until then.
296296 </p>`;
297297 return html`<section class="mb" id="migrate-accounts-section">
@@ -335,7 +335,7 @@ function picker(
335335 if (listed.length === 0)
336336 return html`<p class="muted small mb">
337337 ${icon("globe")} Connect an account above — or another forge under
338 <a href="/settings/origins">Settings → Origins</a> — to pick from your
338 <a href="/settings/origins">Settings → Connected accounts</a> — to pick from your
339339 repositories there, private ones included.
340340 </p>`;
341341 return html`<div class="field" id="migrate-picker">
src/web/pages/origin-archived.ts +1 −1
@@ -117,7 +117,7 @@ async function render(
117117 const head = html`<div class="bar mb">
118118 <h2>Archived on ${host}</h2>
119119 <span class="spacer"></span>
120 ${btn("Back to origins", {
120 ${btn("Back to connected accounts", {
121121 href: "/settings/origins",
122122 size: "sm",
123123 variant: "ghost",
src/web/pages/origin-flow.ts +2 −2
@@ -85,10 +85,10 @@ export function originFlowPage(ctx: Ctx): Response {
8585 ${authorize === undefined
8686 ? html`<p>
8787 This connect cannot be continued from here — the authorization
88 link was lost. Start it again from Origins.
88 link was lost. Start it again from Connected accounts.
8989 </p>
9090 <div class="bar mt">
91 ${btn("Back to origins", {
91 ${btn("Back to connected accounts", {
9292 href: "/settings/origins",
9393 variant: "primary",
9494 })}
src/web/pages/origin-repos.ts +1 −1
@@ -293,7 +293,7 @@ function groupSection(
293293 icon: "alert",
294294 title: read.error === "unreachable" ? "The origin could not be reached" : "This group could not be read",
295295 note: "Nothing was changed. Try again, or reconnect the origin if it needs re-authorizing.",
296 action: btn("Back to origins", { href: "/settings/origins", variant: "primary" }),
296 action: btn("Back to connected accounts", { href: "/settings/origins", variant: "primary" }),
297297 })
298298 : found.items.length === 0
299299 ? empty({
src/web/pages/origins.ts +12 −12
@@ -144,14 +144,14 @@ const SECTION_ROWS: readonly (readonly [
144144 ["Account", "Profile", "/settings", "user", (c) => c === "profile", false],
145145 ["Account", "Password", "/settings/password", "lock", (c) => c === "password", false],
146146 ["Account", "Passkeys", "/settings/passkeys", "key", (c) => c === "passkeys", false],
147 ["Account", "SSH keys", "/settings/keys", "lock", (c) => c === "keys", false],
148 ["Account", "Access tokens", "/settings/tokens", "key", (c) => c === "tokens", false],
147 ["Account", "Computer keys", "/settings/keys", "lock", (c) => c === "keys", false],
148 ["Account", "Access codes", "/settings/tokens", "key", (c) => c === "tokens", false],
149149 ["Account", "Confirms", "/settings/confirms", "check", (c) => c === "confirms", false],
150150 ["Crew and AI", "Model", "/settings/model", "key", (c) => c === "model", false],
151151 ["Crew and AI", "Crew", "/settings/crew", "bot", (c) => c === "crew", true],
152152 ["Crew and AI", "Subscriptions", "/settings/subscriptions", "key", (c) => c === "subscriptions", true],
153153 ["Crew and AI", "Usage", "/settings/spend", "activity", (c) => USAGE.has(c), true],
154 ["Connections", "Origins", "/settings/origins", "globe", (c) => c === "origins", false],
154 ["Connections", "Connected accounts", "/settings/origins", "globe", (c) => c === "origins", false],
155155 ["Connections", "Cloud accounts", "/settings/cloud", "key", (c) => c === "cloud", true],
156156 ["Connections", "Secrets", "/settings/secrets", "lock", (c) => c === "secrets", true],
157157 ["Connections", "Devices", "/settings/devices", "globe", (c) => c === "devices", true],
@@ -689,7 +689,7 @@ function forgeSection(ctx: Ctx, values: ConnectValues): Html {
689689 hint: "Read is enough to bring an app; private repositories come with it.",
690690 })}
691691 ${readOnlyField()}
692 <p class="muted small">Make the token there: ${tokens} (Settings → Access tokens).</p>
692 <p class="muted small">Make the token there: ${tokens} (Settings → Access codes).</p>
693693 <div class="form-actions">
694694 ${btn("Connect that forge", { variant: "primary", icon: "globe" })}
695695 </div>
@@ -794,14 +794,14 @@ function originsView(
794794 </p>
795795 ${origins.length === 0
796796 ? html`<section class="settings-block">
797 <h2>Origins</h2>
797 <h2>Connected accounts</h2>
798798 <p class="small">
799 No origins yet: connect another forge, GitHub, a Forgejo
799 No connected accounts yet: connect another forge, GitHub, a Forgejo
800800 instance or a Gitea instance below.
801801 </p>
802802 </section>`
803803 : html`<section class="settings-block">
804 <h2>Origins</h2>
804 <h2>Connected accounts</h2>
805805 <ul class="list setting-list" data-origins>
806806 ${origins.map((o) =>
807807 originCard(
@@ -820,7 +820,7 @@ function originsView(
820820 )
821821 : null}
822822 <section class="settings-block" id="connect">
823 <h2>Connect an origin</h2>
823 <h2>Connect an account</h2>
824824 <ul class="list">
825825 ${connectPath(
826826 "connect-forge",
@@ -925,9 +925,9 @@ export function shell(
925925 return page(
926926 {
927927 ctx,
928 title: "Origins",
928 title: "Connected accounts",
929929 ...(scriptJs === undefined ? {} : { scriptJs }),
930 body: html`<div class="page-head"><h1>Origins</h1></div>
930 body: html`<div class="page-head"><h1>Connected accounts</h1></div>
931931 ${settingsLayout(
932932 accountSections("origins", {
933933 admin: ctx.auth.user?.isAdmin === true,
@@ -1218,8 +1218,8 @@ export function notFoundNote(): Html {
12181218 return empty({
12191219 icon: "search",
12201220 title: "No such connect",
1221 note: "It finished, expired, or was never yours. Start again from Origins.",
1222 action: btn("Back to origins", {
1221 note: "It finished, expired, or was never yours. Start again from Connected accounts.",
1222 action: btn("Back to connected accounts", {
outdated

Principle 8: the page is now 'Connected accounts' but this button still says 'Back to origins'. The same page keeps 'Connect an origin' and 'Trusted internal hosts'. Reword them so one thing has one name.

12231223 href: "/settings/origins",
12241224 variant: "primary",
12251225 }),
src/web/pages/site.ts +1 −1
@@ -201,7 +201,7 @@ export function terminalCard(ctx: Ctx): Html {
201201 ${terminalCommand(
202202 ssh,
203203 ctx.app.config.sshHostKey,
204 html`<a class="small" href="/settings/keys">SSH keys</a>`,
204 html`<a class="small" href="/settings/keys">Computer keys</a>`,
205205 )}
206206 </section>`;
207207 }
src/web/pages/thread.ts +1 −1
@@ -95,7 +95,7 @@ export function originError(error: string): string | undefined {
9595 origin_protected:
9696 "The operator protects this repository at its origin, so nothing is pushed to it from here. Propose the change from your fork.",
9797 origin_paused:
98 "This connection is not verified yet, so writes through it are paused. Nothing was changed. Check it again under Settings → Origins.",
98 "This connection is not verified yet, so writes through it are paused. Nothing was changed. Check it again under Settings → Connected accounts.",
9999 origin_unreachable: "The origin could not be reached. Nothing was changed.",
100100 };
101101 return words[error];
src/web/style/components.ts +1 −1
@@ -507,7 +507,7 @@ a.row:hover .row-title { color: var(--accent); }
507507 /* A settings row's acts (Accounts): always visible, wrapping, a full line under 640px. */
508508 .setting-list > li.has-after > .row-after { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
509509 @media (max-width: 640px) { .setting-list > li.has-after > .row-after { flex-basis: 100%; justify-content: flex-start; padding: 0 14px 10px; } }
510 /* An origin's error, notes and acts (Settings › Origins): a block of their own under the row, at every width. */
510 /* An origin's error, notes and acts (Settings › Connected accounts): a block of their own under the row, at every width. */
511511 .setting-list[data-origins] > li.has-after > .row-after, .setting-list[data-bets] > li.has-after > .row-after { flex-basis: 100%; display: block; min-width: 0; padding: 0 14px 12px; overflow-wrap: anywhere; }
512512 .list > li.has-after > .row-children { flex-basis: 100%; }
513513 /* A disk's alert reasons (Settings › Machines, docs/design/areas/machines.md): a line under its folded row, inset to the title. */
test/account-help.test.ts +1 −1
@@ -59,7 +59,7 @@ test("the password and token explanations live inside the help toggle, before th
5959 expect(pw.replace(helpOf(pw), "")).not.toContain("A passphrase beats a puzzle");
6060 expect(pw.indexOf("data-page-help")).toBeLessThan(pw.indexOf("Change password</h2>"));
6161 const tk = await get("/settings/tokens");
62 expect(helpOf(tk)).toContain("can never mint another token");
62 expect(helpOf(tk)).toContain("can never make another access code");
6363 expect(helpOf(tk)).toContain("laptop, CI, deploy box");
6464 expect(tk.replace(helpOf(tk), "")).not.toContain("laptop, CI, deploy box");
6565 });
test/account-menu-names.test.ts +16 −16
@@ -37,9 +37,9 @@ afterAll(async () => {
3737 });
3838
3939 const PAGES: { name: string; path: string; empty: string }[] = [
40 { name: "SSH keys", path: "/settings/keys", empty: "No SSH keys yet" },
41 { name: "Access tokens", path: "/settings/tokens", empty: "No access tokens yet" },
42 { name: "Origins", path: "/settings/origins", empty: "No origins yet" },
40 { name: "Computer keys", path: "/settings/keys", empty: "No computer keys yet" },
41 { name: "Access codes", path: "/settings/tokens", empty: "No access codes yet" },
42 { name: "Connected accounts", path: "/settings/origins", empty: "No connected accounts yet" },
4343 ];
4444
4545 for (const { name, path, empty } of PAGES) {
@@ -57,16 +57,16 @@ for (const { name, path, empty } of PAGES) {
5757 });
5858 }
5959
60 test("access tokens: help term, field label and button say access token", async () => {
60 test("access codes: help term, field label and button say access code", async () => {
6161 const body = await (await fetch(`${base}/settings/tokens`, { headers: { cookie } })).text();
62 expect(body).toContain("Access token name");
62 expect(body).toContain("Access code name");
6363 expect(body).not.toContain("Token name");
64 expect(body).toContain("Generate access token");
64 expect(body).toContain("Generate access code");
6565 });
6666
67 test("origins: the section heading reads Origins, not Connected origins", async () => {
67 test("origins: the section heading reads Connected accounts, not Origins", async () => {
6868 const body = await (await fetch(`${base}/settings/origins`, { headers: { cookie } })).text();
69 expect(body).toContain("<h2>Origins</h2>");
69 expect(body).toContain("<h2>Connected accounts</h2>");
7070 expect(body).not.toContain("Connected origins");
7171 });
7272
@@ -93,28 +93,28 @@ function boot() {
9393 };
9494 }
9595
96 test("terminal: Signed in makes an access token on screens titled access token", async () => {
96 test("terminal: Signed in makes an access code on screens titled access code", async () => {
9797 const h = boot();
9898 try {
9999 await h.palette("Signed in");
100 expect(h.text()).toContain("m makes an access token");
100 expect(h.text()).toContain("m makes an access code");
101101 await h.keys("m");
102 expect(h.text()).toContain(" Make an access token ");
102 expect(h.text()).toContain(" Make an access code ");
103103 await h.keys("enter"); // the name: "token"
104 expect(h.text()).toContain(" Make an access token ");
104 expect(h.text()).toContain(" Make an access code ");
105105 await h.keys("enter"); // the scope: ro
106 expect(h.text()).toMatch(/^ada › Signed in › New access token$/m);
106 expect(h.text()).toMatch(/^ada › Signed in › New access code$/m);
107107 expect(h.text()).not.toMatch(/ (New|Make a) token /);
108108 } finally {
109109 h.stop();
110110 }
111111 });
112112
113 test("terminal: the origins screen is titled Origins, as the page is", async () => {
113 test("terminal: the origins screen is titled Connected accounts, as the page is", async () => {
114114 const h = boot();
115115 try {
116 await h.palette("Origins");
117 expect(h.text()).toMatch(/^ada › Origins$/m);
116 await h.palette("Connected accounts");
117 expect(h.text()).toMatch(/^ada › Connected accounts$/m);
118118 expect(h.text()).not.toContain("Connected origins");
119119 } finally {
120120 h.stop();
test/account-rows.test.ts +7 −7
@@ -63,18 +63,18 @@ test("no account page is a card or holds a table", async () => {
6363 test("tokens are rows under a Tokens heading, each with Revoke", async () => {
6464 const m = await main("/settings/tokens");
6565 expect(m).toContain('<section class="settings-block">');
66 const list = m.slice(m.indexOf("<h2>Access tokens</h2>"));
67 expect(list).toMatch(/^<h2>Access tokens<\/h2>\s*<ul class="list setting-list">/);
66 const list = m.slice(m.indexOf("<h2>Access codes</h2>"));
67 expect(list).toMatch(/^<h2>Access codes<\/h2>\s*<ul class="list setting-list">/);
6868 const item = list.slice(0, list.indexOf("</ul>"));
6969 expect(item).toContain("<strong>laptop</strong>");
7070 expect(item).toMatch(/settings%2Ftokens%2F[^"]+%2Fdelete/);
7171 expect(m).toContain('action="/settings/tokens"');
7272 });
7373
74 test("keys are rows under an SSH keys heading, each with its Remove link", async () => {
74 test("keys are rows under a Computer keys heading, each with its Remove link", async () => {
outdated

Grammar slip from the mechanical rename: 'an Computer keys heading' should read 'a Computer keys heading'.

7575 const m = await main("/settings/keys");
76 const list = m.slice(m.indexOf("<h2>SSH keys</h2>"));
77 expect(list).toMatch(/^<h2>SSH keys<\/h2>\s*<ul class="list setting-list">/);
76 const list = m.slice(m.indexOf("<h2>Computer keys</h2>"));
77 expect(list).toMatch(/^<h2>Computer keys<\/h2>\s*<ul class="list setting-list">/);
7878 const item = list.slice(0, list.indexOf("</ul>"));
7979 expect(item).toContain("<strong>work</strong>");
8080 expect(item).toMatch(/settings%2Fkeys%2F[^"]+%2Fdelete/);
@@ -90,10 +90,10 @@ test("empty lists are one muted sentence, not a panel", async () => {
9090 const c = (other.headers.get("set-cookie") ?? "").split(";")[0]!;
9191 const get = async (path: string) => (await (await fetch(`${base}${path}`, { headers: { cookie: c } })).text());
9292 const tokens = await get("/settings/tokens");
93 expect(tokens).toContain('<p class="muted">No access tokens yet.');
93 expect(tokens).toContain('<p class="muted">No access codes yet.');
9494 expect(tokens).not.toContain("empty-title");
9595 const keys = await get("/settings/keys");
96 expect(keys).toContain('<p class="muted">No SSH keys yet.');
96 expect(keys).toContain('<p class="muted">No computer keys yet.');
9797 expect(keys).not.toContain("empty-title");
9898 const card = String(sessionsCard([], "/settings"));
9999 expect(card).toContain('<p class="muted">Not signed in on any browser.</p>');
test/everyday.test.ts +2 −2
@@ -223,7 +223,7 @@ describe("the everyday account", () => {
223223 test("an admin's token from the page is minted only when they tick that they mean it", async () => {
224224 const root = await signIn("ada");
225225 const form = await page(root, "/settings/tokens");
226 expect(form).toContain("I mean to give this token admin");
226 expect(form).toContain("I mean to give this access code admin");
227227 const count = () =>
228228 (
229229 app.db
@@ -241,7 +241,7 @@ describe("the everyday account", () => {
241241 expect(count()).toBe(before + 1);
242242 // Someone who is not an admin is not asked.
243243 expect(await page(await signIn("kim"), "/settings/tokens")).not.toContain(
244 "I mean to give this token admin",
244 "I mean to give this access code admin",
245245 );
246246 });
247247
test/first-login-words.test.ts +7 −7
@@ -24,13 +24,13 @@ describe("the first-login page of a spawned forge, in plain words", () => {
2424
2525 afterAll(() => forge.stop());
2626
27 /** The page itself: what sits in `<main>`; the menus around it are their own issue. */
27 /** The page itself, to check the first-login content is what was read. */
2828 const mainOf = (html: string) => /<main\b[\s\S]*?<\/main>/.exec(html)?.[0] ?? "";
2929
3030 test("the owner's first page has no jargon", async () => {
31 const main = mainOf(await (await fetch(url("/"), { headers: { cookie: owner } })).text());
32 expect(main).toContain("Your next steps");
33 expect(jargonIn(main)).toEqual([]);
31 const html = await (await fetch(url("/"), { headers: { cookie: owner } })).text();
32 expect(mainOf(html)).toContain("Your next steps");
33 expect(jargonIn(html)).toEqual([]);
3434 });
3535
3636 test("the everyday account's first page has no jargon", async () => {
@@ -41,9 +41,9 @@ describe("the first-login page of a spawned forge, in plain words", () => {
4141 });
4242 expect(res.status).toBe(303);
4343 const daily = (res.headers.get("set-cookie") ?? "").split(";")[0]!;
44 const main = mainOf(await (await fetch(url("/"), { headers: { cookie: daily } })).text());
45 expect(main).toContain("Your next steps");
46 expect(jargonIn(main)).toEqual([]);
44 const html = await (await fetch(url("/"), { headers: { cookie: daily } })).text();
45 expect(mainOf(html)).toContain("Your next steps");
46 expect(jargonIn(html)).toEqual([]);
4747 });
4848
4949 test("the word list sees the words it is for", () => {
test/frozen-line.test.ts +2 −2
@@ -62,9 +62,9 @@ for (const path of ["/alice/frozen", "/alice/frozen/branches"]) {
6262 });
6363 }
6464
65 test("an admin without an origin is pointed at Settings → Origins", async () => {
65 test("an admin without an origin is pointed at Settings → Connected accounts", async () => {
6666 const m = await get("/alice/frozen", admin);
67 expect(m).toContain("Settings → Origins");
67 expect(m).toContain("Settings → Connected accounts");
6868 expect(m).not.toContain("/settings/origin/reconnect");
6969 });
7070
test/keys.test.ts +1 −1
@@ -284,7 +284,7 @@ describe.skipIf(NO_KEYGEN)("ssh keys", () => {
284284 const empty = await (
285285 await fetch(`${base}/settings/keys`, { headers: { cookie } })
286286 ).text();
287 expect(empty).toContain("No SSH keys yet");
287 expect(empty).toContain("No computer keys yet");
288288
289289 const added = await fetch(`${base}/settings/keys`, {
290290 method: "POST",
test/mcp-trusted-hosts.test.ts +1 −1
@@ -1,5 +1,5 @@
11 /**
2 * The `origins` tool's `hosts` action is Settings → Origins' internal-host list over
2 * The `origins` tool's `hosts` action is Settings → Connected accounts' internal-host list over
33 * MCP: the same rules, the same refusals, a forge admin only.
44 */
55 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
test/oauth-connect-browser.test.ts +2 −2
@@ -3,7 +3,7 @@
33 * form's redirect wherever it points; a browser first checks the posting
44 * page's `form-action` policy against it, and refuses a redirect off this
55 * forge without a word on the page. So this drives headless Chromium
6 * (`test/helpers/chrome.ts`): click Authorize on Settings → Origins, then
6 * (`test/helpers/chrome.ts`): click Authorize on Settings → Connected accounts, then
77 * Continue on the flow's page, and see the browser actually ask the origin
88 * for its consent screen — the URL stored on the flow — with no
99 * Content-Security-Policy refusal on the way.
@@ -68,7 +68,7 @@ describe.skipIf(chromium === undefined)("connecting an origin in a real browser"
6868 await rm(dir, { recursive: true, force: true });
6969 });
7070
71 test("Settings → Origins: Authorize, then Continue, reaches the consent screen", async () => {
71 test("Settings → Connected accounts: Authorize, then Continue, reaches the consent screen", async () => {
7272 const made = await createUser(app.db, {
7373 username: "bea",
7474 email: "[email protected]",
test/onboarding.test.ts +1 −1
@@ -123,7 +123,7 @@ describe("onboarding: a person with just a login", () => {
123123 const pageHtml = await (
124124 await fetch(`${base}/settings/tokens`, { headers: { cookie } })
125125 ).text();
126 expect(pageHtml).toContain("Access tokens");
126 expect(pageHtml).toContain("Access codes");
127127
128128 // Post-redirect-get: minting redirects, and the secret rides a one-shot
129129 // flash cookie so a refresh shows the list instead of minting again.
test/origin-flow-page.test.ts +1 −1
@@ -53,7 +53,7 @@ describe("the connect flow page", () => {
5353 ...(form === undefined ? {} : { body: new URLSearchParams(form) }),
5454 });
5555
56 /** Start a code flow from Settings → Origins: its page, and its id. */
56 /** Start a code flow from Settings → Connected accounts: its page, and its id. */
5757 const start = async (): Promise<{ page: string; id: string }> => {
5858 const started = await browse("/settings/origins/oauth", { host: fake.url });
5959 expect(started.status).toBe(303);
test/origin-guard.test.ts +2 −2
@@ -677,7 +677,7 @@ describe("the guard against an operator's Forgejo", () => {
677677 const synced = await syncOriginNow(local.app, local.alice, id);
678678 expect(synced.ok && synced.value.error).toBe("");
679679
680 // Settings → Origins says so, with the way out.
680 // Settings → Connected accounts says so, with the way out.
681681 const page = await (await browse(local, "/settings/origins")).text();
682682 expect(page).toContain("Couldn't verify this connection; writes paused");
683683 expect(page).toContain(`action="/settings/origins/${id}/check"`);
@@ -717,7 +717,7 @@ describe("the guard against an operator's Forgejo", () => {
717717 reset();
718718 });
719719
720 test("Check again on Settings → Origins runs the guard and says what it found", async () => {
720 test("Check again on Settings → Connected accounts runs the guard and says what it found", async () => {
721721 reset();
722722 const local = await boot();
723723 // Connected before the guard: never checked, so paused once guarded.
test/origins-page-shape.test.ts +5 −5
@@ -50,14 +50,14 @@ test("the origins page has settings blocks with headings and no card", async ()
5050 expect(res.status).toBe(200);
5151 const body = await res.text();
5252 expect(body).not.toMatch(/class="(?:[^"]*\s)?card(?:\s[^"]*)?"/);
53 for (const h of ["Origins", "Trusted internal hosts", "Connect an origin"]) {
53 for (const h of ["Connected accounts", "Trusted internal hosts", "Connect an account"]) {
5454 expect(body).toMatch(new RegExp(`<section class="settings-block"[^>]*>\\s*<h2>${h}</h2>`));
5555 }
5656 expect(body).toContain(
57 "No origins yet: connect another forge, GitHub, a Forgejo\n instance or a Gitea instance below.",
57 "No connected accounts yet: connect another forge, GitHub, a Forgejo\n instance or a Gitea instance below.",
5858 );
5959 // One sentence: no full stop before its end.
60 const empty = body.split("<h2>Origins</h2>")[1]!.split("</p>")[0]!;
60 const empty = body.split("<h2>Connected accounts</h2>")[1]!.split("</p>")[0]!;
6161 expect(empty.replace(/<[^>]+>/g, "").trim().split(". ").length).toBe(1);
6262 expect(body).toContain('id="connect"');
6363 });
@@ -77,7 +77,7 @@ test("connected origins are rows of one list, with no wrapper box", async () =>
7777 }
7878 const body = await (await fetch(`${servers.url}/settings/origins`, { headers: { cookie } })).text();
7979 expect(body).not.toContain("origin-item");
80 const block = body.split("<h2>Origins</h2>")[1]!.split("</section>")[0]!;
80 const block = body.split("<h2>Connected accounts</h2>")[1]!.split("</section>")[0]!;
8181 expect(block.match(/<ul class="list setting-list" data-origins>/g)?.length).toBe(1);
8282 expect(block.match(/<li[ >]/g)!.length).toBeGreaterThanOrEqual(2);
8383 expect(block).toContain("Sync now");
@@ -100,7 +100,7 @@ test("an origin's error and notes render inside its own row", async () => {
100100 .query("UPDATE origin SET last_error = ?1 WHERE id = ?2")
101101 .run("upstream said 502", o.value.id);
102102 const body = await (await fetch(`${servers.url}/settings/origins`, { headers: { cookie } })).text();
103 const block = body.split("<h2>Origins</h2>")[1]!.split("</section>")[0]!;
103 const block = body.split("<h2>Connected accounts</h2>")[1]!.split("</section>")[0]!;
104104 const li = block
105105 .split(/<li[ >]/)
106106 .slice(1)
test/origins-reconnect.test.ts +1 −1
@@ -397,7 +397,7 @@ describe("a frozen mirror is said, and reconnected in one click", () => {
397397 expect(page).toContain(`>${day}</time>`);
398398 expect(page).toContain("it has been frozen since");
399399 // Alice has no origin on that host now: the way back is connecting one.
400 expect(page).toContain("Settings → Origins");
400 expect(page).toContain("Settings → Connected accounts");
401401 expect(page).toContain("connecting one reconnects this mirror by itself");
402402 }
403403 // A reader who cannot administer it reads the sentence, not the form.
test/origins-timing.test.ts +1 −1
@@ -47,7 +47,7 @@ test("GET /settings/origins names its phases", async () => {
4747 const res = await fetch(`${servers.url}/settings/origins`, { headers: { cookie } });
4848 const body = await res.text();
4949 expect(res.status).toBe(200);
50 expect(body).toContain("<h1>Origins</h1>");
50 expect(body).toContain("<h1>Connected accounts</h1>");
5151 const parts = (res.headers.get("server-timing") ?? "").split(", ");
5252 for (const name of ["app", "session", "origins", "mirrors", "apps", "frozen", "render"])
5353 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
test/palette-repos.test.ts +1 −1
@@ -117,7 +117,7 @@ test("a visitor's palette holds places to go and Sign in; a signed-in one holds
117117 expect(out.map((i) => i.t)).toEqual(["Home", "Apps", "Explore", "Forge packs", "Front page", "Search", "Sign in"]);
118118 expect(out.find((i) => i.t === "Sign in")!.h).toBe("/signin");
119119 const inn = (await rows({ cookie }, "/explore")).map((i) => i.t);
120 for (const t of ["New app", "New repository", "Migrate a repository", "New organization", "What you asked for", "Account settings", "Access tokens", "SSH keys", "Origins", "Map"]) expect(inn).toContain(t);
120 for (const t of ["New app", "New repository", "Migrate a repository", "New organization", "What you asked for", "Account settings", "Access codes", "Computer keys", "Connected accounts", "Map"]) expect(inn).toContain(t);
121121 expect(inn).not.toContain("Sign in");
122122 });
123123
test/palette-roster.test.ts +9 −9
@@ -89,7 +89,7 @@ test("a site-admin person is offered the whole roster", () => {
8989 ["confirms", "Confirms", "what the forge's agent asked to do; y does it once"],
9090 ["trusted", "Trusted sources", "who folds in without review; a trusts, x removes, s stops all, v reverts"],
9191 ["conversations", "Conversations", "your talks with the forge's agent; n starts one, r renames, x archives"],
92 ["ssh-keys", "SSH keys", "list, add and remove the keys that sign in as you"],
92 ["ssh-keys", "Computer keys", "list, add and remove the keys that sign in as you"],
9393 ["passkeys", "Passkeys", "the passkeys that sign in as you"],
9494 ["account", "Account settings", undefined],
9595 ["signins", "Signed in", "your browsers and tokens; d signs a browser out"],
@@ -100,7 +100,7 @@ test("a site-admin person is offered the whole roster", () => {
100100 ["home-pins", "Pinned panels", "the panels pinned to home: reorder and unpin"],
101101 ["now", "Now", "what check, review, land and live count, with the rows"],
102102 ["forge-view", "Forge view", "the forge's counts, and Foundry when it is installed"],
103 ["origin-apps", "Origin apps", "your OAuth apps for connecting origins; a adds, d removes"],
103 ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"],
104104 ["system-memory", "System memory", "RSS beside the JS heap, hourly for 24 h"],
105105 ["flow", "Flow", "the pipeline's bottleneck and throughput"],
106106 ["system", "System", "the machine now: CPU, memory, disks, processes"],
@@ -156,7 +156,7 @@ test("a site-admin person is offered the whole roster", () => {
156156 ["cloud", "Cloud accounts", "provider tokens, what they are used for, today's calls"],
157157 ["pinned", "Pinned forges", "deploy, update and roll back a forge on your cloud"],
158158 ["bets", "Bets", "back, stop and finish a repository\'s bets"],
159 ["origins", "Origins", "the hosts your mirrors come from"],
159 ["origins", "Connected accounts", "the hosts your mirrors come from"],
160160 ["domains", "Failure domains", "failure domains and their last drill"],
161161 ["feed", "Feed", "what happened, ranked; r replies on a post about an issue or pull, l likes, h who liked"],
162162 ["feature-review", "Menu entries to review", "absorbed apps whose declared name or page changed: accept them"],
@@ -208,12 +208,12 @@ test("a site-admin agent is offered the roster without the person-only entries",
208208 ["asked", "Asked", "your thoughts and what became of them"],
209209 ["thought-notes", "Notes", "write an idea and watch it become shipped work"],
210210 ["decider", "Decider", "what it closed, deferred and asked you about today"],
211 ["ssh-keys", "SSH keys", "list, add and remove the keys that sign in as you"],
211 ["ssh-keys", "Computer keys", "list, add and remove the keys that sign in as you"],
212212 ["account", "Account settings", undefined],
213213 ["signins", "Signed in", "your browsers and tokens; d signs a browser out"],
214214 ["sentinel", "Sentinel", "the forge's open faults"],
215215 ["resilience", "Resilience", "where snapshots go and the mirror; each protection green, red or untested"],
216 ["origin-apps", "Origin apps", "your OAuth apps for connecting origins; a adds, d removes"],
216 ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"],
217217 ["api", "API", "the forge's endpoints; run a GET"],
218218 ["changelog", "Changelog", "the last 30 releases and their pulls"],
219219 ["releases", "Releases", "the live release and the kept ones"],
@@ -248,7 +248,7 @@ test("a site-admin agent is offered the roster without the person-only entries",
248248 ]);
249249 });
250250
251 test("no agent is offered Origins", () => {
251 test("no agent is offered Connected accounts", () => {
252252 expect(roster({ ...admin, kind: "agent" }).map(([id]) => id)).not.toContain("origins");
253253 });
254254
@@ -294,11 +294,11 @@ test("a member person is offered the signed-in roster without the admin entries"
294294 ["notify", "Phone notifications", "where the crew reaches your phone; s sets, x turns off"],
295295 ["confirms", "Confirms", "what the forge's agent asked to do; y does it once"],
296296 ["conversations", "Conversations", "your talks with the forge's agent; n starts one, r renames, x archives"],
297 ["ssh-keys", "SSH keys", "list, add and remove the keys that sign in as you"],
297 ["ssh-keys", "Computer keys", "list, add and remove the keys that sign in as you"],
298298 ["passkeys", "Passkeys", "the passkeys that sign in as you"],
299299 ["account", "Account settings", undefined],
300300 ["signins", "Signed in", "your browsers and tokens; d signs a browser out"],
301 ["origin-apps", "Origin apps", "your OAuth apps for connecting origins; a adds, d removes"],
301 ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"],
302302 ["api", "API", "the forge's endpoints; run a GET"],
303303 ["changelog", "Changelog", "the last 30 releases and their pulls"],
304304 ["stalled", "Stalled steps", "pulls whose check is red or was never asked for; enter re-runs"],
@@ -313,7 +313,7 @@ test("a member person is offered the signed-in roster without the admin entries"
313313 ["archived", "Archived repositories", "put away, not gone: open one or wake it"],
314314 ["bulk", "Bulk actions", "your last bulk actions; u undoes one inside its day"],
315315 ["bets", "Bets", "back, stop and finish a repository\'s bets"],
316 ["origins", "Origins", "the hosts your mirrors come from"],
316 ["origins", "Connected accounts", "the hosts your mirrors come from"],
317317 ["feed", "Feed", "what happened, ranked; r replies on a post about an issue or pull, l likes, h who liked"],
318318 ["feature-feed", "Feature feeds", "the feed items about an absorbed app, as Settings › Features shows them"],
319319 ["front", "Front page", "the apps a first visitor sees, ranked; an admin pins or hides"],
test/ssh-site.test.ts +1 −1
@@ -95,7 +95,7 @@ describe("the terminal on the website", () => {
9595 r.text(),
9696 );
9797 expect(page).not.toContain("terminal-line");
98 expect(page).toContain('<a class="small" href="/settings/keys">SSH keys</a>');
98 expect(page).toContain('<a class="small" href="/settings/keys">Computer keys</a>');
9999 expect(page).toContain("host key SHA256:");
100100 });
101101
test/tui-originapps.test.ts +2 −2
@@ -1,5 +1,5 @@
11 /**
2 * The terminal's Origin apps screen (src/tui/screens/originapps.ts, #7291):
2 * The terminal's Connection apps screen (src/tui/screens/originapps.ts, #7291):
33 * add, list without the secret, a bad host refused in the API's words, and
44 * remove after a confirm. No frame and no log line holds the secret.
55 */
@@ -18,7 +18,7 @@ import { settleApp } from "./helpers/tui.ts";
1818
1919 const SECRET = "s3cr3t_Qw9zXv";
2020 const RIGHT = "\x1b[C";
21 const open = [":", "Origin apps", "\r"];
21 const open = [":", "Connection apps", "\r"];
2222
2323 let dir: string;
2424 let app: App;
test/tui-origins-forge.test.ts +2 −2
@@ -73,7 +73,7 @@ test("c connects another forge by address and token, and the list says what did
7373 };
7474 try {
7575 await key("k");
76 await type("Origins");
76 await type("Connected accounts");
7777 await key("enter");
7878 expect(app.screen.toText()).toContain("Press c to connect one.");
7979 // A forge has no device sign-in: the terminal says so and points at the page.
@@ -123,7 +123,7 @@ test("a token the forge refuses connects nothing and says so", async () => {
123123 };
124124 try {
125125 await press("k", true);
126 await type("Origins");
126 await type("Connected accounts");
127127 await press("enter");
128128 await press("c");
129129 await type(friendServers.url);
test/tui-origins-guard.test.ts +1 −1
@@ -68,7 +68,7 @@ describe("a paused origin in the terminal", () => {
6868 };
6969 try {
7070 await key("k", true);
71 for (const ch of "Origins") await key(ch);
71 for (const ch of "Connected accounts") await key(ch);
7272 await key("enter");
7373 expect(app.screen.toText()).toContain("writes paused");
7474 expect(app.screen.toText()).toContain("Press v to check again");
test/tui-origins.test.ts +5 −5
@@ -1,4 +1,4 @@
1 /** Origins in the terminal: listed, disconnected after a confirm, never offered to a guest. */
1 /** Connected accounts in the terminal: listed, disconnected after a confirm, never offered to a guest. */
22
33 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
44 import { mkdtemp, rm } from "node:fs/promises";
@@ -47,7 +47,7 @@ describe("connected origins in the terminal", () => {
4747
4848 const open = async (h: ReturnType<typeof boot>) => {
4949 await h.ctrl("k");
50 await h.type("Origins");
50 await h.type("Connected accounts");
5151 return h;
5252 };
5353
@@ -116,7 +116,7 @@ describe("connected origins in the terminal", () => {
116116 const h = boot({ sshUsername: "t", remoteAddress: "127.0.0.1", user: getUserByName(forge.db, "carol")! } as TuiIdentity);
117117 try {
118118 await h.ctrl("k");
119 await h.type("Origins");
119 await h.type("Connected accounts");
120120 await h.press("enter");
121121 expect(listOrigins(forge.db, made.value.id)[0]!.lastError).toBe("");
122122 await h.press("s");
@@ -167,7 +167,7 @@ describe("connected origins in the terminal", () => {
167167 const h = boot({ ...alice(), user: { ...getUserByName(forge.db, "alice")!, kind: "agent" } } as TuiIdentity);
168168 try {
169169 await h.ctrl("k");
170 await h.type("Origins");
170 await h.type("Connected accounts");
171171 expect(h.text()).not.toContain("the hosts your mirrors come from");
172172 } finally {
173173 h.stop();
@@ -200,7 +200,7 @@ describe("connected origins in the terminal", () => {
200200 const h = boot({ sshUsername: "git", remoteAddress: "127.0.0.1", fingerprint: "SHA256:unknown" });
201201 try {
202202 await h.ctrl("k");
203 await h.type("Origins");
203 await h.type("Connected accounts");
204204 expect(h.text()).not.toContain("the hosts your mirrors come from");
205205 } finally {
206206 h.stop();
test/tui-sshkeys.test.ts +3 −3
@@ -1,5 +1,5 @@
11 /**
2 * SSH keys in the terminal (#7242): your own keys listed, one added, a bad
2 * Computer keys in the terminal (#7242): your own keys listed, one added, a bad
33 * line refused in MCP's words, one removed after a confirm.
44 */
55 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
@@ -27,7 +27,7 @@ const line = (fill: number, comment = "") => {
2727 return `ssh-ed25519 ${blob.toString("base64")} ${comment}`.trim();
2828 };
2929
30 describe("SSH keys in the terminal", () => {
30 describe("Computer keys in the terminal", () => {
3131 let dataDir: string;
3232 let app: App;
3333
@@ -62,7 +62,7 @@ describe("SSH keys in the terminal", () => {
6262 open: async function () {
6363 key("k", true);
6464 await settleApp(tui);
65 await this.type("SSH keys");
65 await this.type("Computer keys");
6666 await this.press("enter");
6767 },
6868 stop: () => tui.stop(),
test/web-origins.test.ts +2 −2
@@ -232,7 +232,7 @@ describe("origins on the web", () => {
232232
233233 test("the origins page starts empty and offers the three ways in", async () => {
234234 const html = await (await asUser(alice, "/settings/origins")).text();
235 expect(html).toContain("No origins yet");
235 expect(html).toContain("No connected accounts yet");
236236 expect(html).toContain("Forgejo / Gitea, one click");
237237 // Another forge leads (#9388); the rest are folded.
238238 expect(html).toContain("Another forge");
@@ -363,7 +363,7 @@ describe("origins on the web", () => {
363363
364364 test("another user cannot see or act on that origin", async () => {
365365 const html = await (await asUser(bob, "/settings/origins")).text();
366 expect(html).toContain("No origins yet");
366 expect(html).toContain("No connected accounts yet");
367367 const id = listOrigins(app.db, getUserByName(app.db, "alice")!.id)[0]!.id;
368368 const stolen = await asUser(bob, `/settings/origins/${id}/repos`);
369369 expect(stolen.status).toBe(404);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs