Public: anyone can read this; the crew can change it.
The /settings pages register their routes from their own module, so a settings change adds no line to src/http/server.ts #8393
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/8309-the-settings-pages-register-their-routes → main 4 commits fork 3 threads · 3 unresolved
6 files
changed +433
−255
AGENTS.md +2
| @@ -11,3 +11,5 @@ A new `forge` command is a new file in `src/cli/commands/` (exporting `command`) | ||
| 11 | 11 | A new terminal screen offered only to site admins exports its palette `entry` from its own module and adds one line to `ENTRIES` in `src/tui/screens/entries.ts`; `after` names the commands it follows. Entries offered to everyone, and entries gated by a helper (`mayCapacity`, `mayManageFlags`, `canSeeMemory`, `telemetryReader` and the like) rather than by `isAdmin`, stay in `forgeCommands` in `src/tui/forge.ts`. |
| 12 | 12 | |
| 13 | 13 | A new read of a table that grows (`event`, `issue`, `run`, `comment`) names the index it walks, in the migration that adds it, and a test shows it from the query plan (`test/hot-reads-indexed.test.ts` is the pattern): no `SCAN`, no `TEMP B-TREE`. Judge by plan or row count, never the clock. A read that stays slow with the best index is a summary table, kept as the rows change, before it is a worker. |
| 14 | ||
| 15 | A new `/settings…` page route goes in `src/http/settings_routes.ts` (`settingsRoutes`), not `src/http/server.ts`. |
src/http/server.ts +5 −251
| @@ -13,21 +13,15 @@ import { signupOptions, signupPasskey } from "../web/pages/signup-passkey.ts"; | ||
| 13 | 13 | import { toolbarComment, toolbarPage } from "../web/studio/toolbar.ts"; |
| 14 | 14 | import { TOOLBAR_NAME, toolbarResponse } from "../apps/toolbar.ts"; |
| 15 | 15 | import { unsealReleased } from "../actions/sealed.ts"; |
| 16 | import { parentCallback, parentLinkStart, parentStart } from "./rp.ts"; | |
| 16 | import { parentCallback, parentStart } from "./rp.ts"; | |
| 17 | 17 | import { prerenderComments } from "../web/prerender.ts"; |
| 18 | import { accessPage } from "../web/pages/access.ts"; | |
| 19 | import { capacityPage, capacitySet } from "../web/pages/capacity.ts"; | |
| 20 | 18 | import { systemPage, systemStream } from "../web/pages/system.ts"; |
| 21 | import { teamSet, teamsPage, teamsStream } from "../web/pages/teams.ts"; | |
| 22 | import { telemetryPage, telemetrySet } from "../web/pages/telemetry.ts"; | |
| 23 | 19 | import { contributePage } from "../web/pages/contribute.ts"; |
| 24 | 20 | import { pipelinePage } from "../web/pages/pipeline.ts"; |
| 25 | import { flagsPage, flagsSet } from "../web/pages/flags.ts"; | |
| 26 | import { usagePage } from "../web/pages/usage.ts"; | |
| 27 | import { spendPage } from "../web/pages/spend-tab.ts"; | |
| 28 | 21 | import { resiliencePage } from "../web/pages/resilience.ts"; |
| 29 | 22 | import { limitsPage, limitsSet, quotasSet, signupSet, inviteMint, inviteRevoke } from "../web/pages/admin-limits.ts"; |
| 30 | 23 | import { measureSubmit } from "./measure.ts"; |
| 24 | import { settingsRoutes } from "./settings_routes.ts"; | |
| 31 | 25 | import { boardsPage } from "../web/pages/boards.ts"; |
| 32 | 26 | import { boardNew, boardPin, boardPost, pinPost } from "../web/pages/boards-edit.ts"; |
| 33 | 27 | import { boardsHoldPost } from "../web/pages/boards-holds.ts"; |
| @@ -39,14 +33,6 @@ import { | ||
| 39 | 33 | noteCaller, |
| 40 | 34 | sweepStanding, |
| 41 | 35 | } from "./caller.ts"; |
| 42 | import { | |
| 43 | accountAct, | |
| 44 | retireAct, | |
| 45 | retirePage, | |
| 46 | accountsPage, | |
| 47 | personSignInsPage, | |
| 48 | signInAct, | |
| 49 | } from "../web/pages/accounts.ts"; | |
| 50 | 36 | import { defect } from "../kernel/error.ts"; |
| 51 | 37 | import { |
| 52 | 38 | armClose, |
| @@ -135,13 +121,8 @@ import { | ||
| 135 | 121 | verifyPage, |
| 136 | 122 | themeSubmit, |
| 137 | 123 | } from "../web/pages/site.ts"; |
| 138 | import { confirmsList, confirmYes } from "../web/pages/confirms.ts"; | |
| 139 | 124 | import { talkConfirm, talkSend, talkStream, talkView } from "../web/pages/talk.ts"; |
| 140 | 125 | import { |
| 141 | passkeyAdd, | |
| 142 | passkeyDelete, | |
| 143 | passkeyOptions, | |
| 144 | passkeysPage, | |
| 145 | 126 | recoverFinish, |
| 146 | 127 | recoverOptions, |
| 147 | 128 | recoverPage, |
| @@ -156,44 +137,9 @@ import { followingSubmit } from "../web/pages/following.ts"; | ||
| 156 | 137 | import { reconnectSubmit } from "../web/pages/frozen.ts"; |
| 157 | 138 | import { activityAtom } from "../web/feeds.ts"; |
| 158 | 139 | import { auditCsv, auditPage, auditProbeSubmit } from "../web/pages/audit.ts"; |
| 159 | import { secretDelete, secretSet, secretsPage } from "../web/pages/secrets.ts"; | |
| 160 | import { | |
| 161 | backupsClear, | |
| 162 | backupsDestination, | |
| 163 | backupsDrill, | |
| 164 | backupsLive, | |
| 165 | backupsMirror, | |
| 166 | backupsMirrorClear, | |
| 167 | backupsMirrorNow, | |
| 168 | backupsNow, | |
| 169 | backupsPage, | |
| 170 | backupsTest, | |
| 171 | domainsMoved, | |
| 172 | } from "../web/pages/backups.ts"; | |
| 173 | 140 | import { apiPage, appApiPage } from "../web/pages/api.ts"; |
| 174 | 141 | import { changelogPage } from "../web/pages/changelog.ts"; |
| 175 | 142 | import { releaseHoldSubmit, liftSubmit, releasesPage, rollbackSubmit } from "../web/pages/releases.ts"; |
| 176 | import { | |
| 177 | cloudAdd, | |
| 178 | cloudAudit, | |
| 179 | cloudAuditPage, | |
| 180 | cloudRename, | |
| 181 | cloudRotate, | |
| 182 | cloudPage, | |
| 183 | cloudRemove, | |
| 184 | cloudVerify, | |
| 185 | cloudWrite, | |
| 186 | } from "../web/pages/cloud.ts"; | |
| 187 | import { | |
| 188 | subscriptionAdd, | |
| 189 | subscriptionDelete, | |
| 190 | subscriptionsPage, | |
| 191 | subscriptionBudget, | |
| 192 | subscriptionHeadroom, | |
| 193 | subscriptionWake, | |
| 194 | } from "../web/pages/subscriptions.ts"; | |
| 195 | import { peoplePage, roleAct } from "../web/pages/people.ts"; | |
| 196 | import { ceilingAct, grantAct, modelAdd, modelPage } from "../web/pages/model.ts"; | |
| 197 | 143 | import { |
| 198 | 144 | changePassword, |
| 199 | 145 | iconPng, |
| @@ -272,7 +218,6 @@ import { | ||
| 272 | 218 | composerSubmit, |
| 273 | 219 | tellSubmit, |
| 274 | 220 | } from "../web/studio/actions.ts"; |
| 275 | import { everydayCreate, everydayKeep, everydayOptions, everydaySwitch } from "../web/pages/everyday.ts"; | |
| 276 | 221 | import { claimFragmentPage, claimOptions, claimSubmit } from "../web/pages/claim.ts"; |
| 277 | 222 | import { firstAdminOptions, firstAdminPage, firstAdminSubmit } from "../web/pages/first-admin.ts"; |
| 278 | 223 | import { familyPage } from "../web/pages/family.ts"; |
| @@ -358,15 +303,6 @@ import { | ||
| 358 | 303 | fleetGraphLive, |
| 359 | 304 | fleetGraphPage, |
| 360 | 305 | } from "../web/pages/fleet-graph.ts"; |
| 361 | import { | |
| 362 | agentCreate, | |
| 363 | agentDelete, | |
| 364 | modelReviewDecline, | |
| 365 | modelReviewTake, | |
| 366 | agentPage, | |
| 367 | agentUpdate, | |
| 368 | crewPage, | |
| 369 | } from "../web/pages/crew.ts"; | |
| 370 | 306 | import { fileHoldUpdate } from "../web/pages/file_holds.ts"; |
| 371 | 307 | import { newPullPage, newPullSubmit } from "../web/pages/pulls.ts"; |
| 372 | 308 | import { |
| @@ -442,25 +378,6 @@ import { | ||
| 442 | 378 | teamMemberRemove, |
| 443 | 379 | teamPage, |
| 444 | 380 | } from "../web/pages/org.ts"; |
| 445 | import { | |
| 446 | accountPage, | |
| 447 | notifyUpdate, | |
| 448 | edgeLink, | |
| 449 | edgeUnlink, | |
| 450 | parentUnlink, | |
| 451 | accountUpdate, | |
| 452 | keyCreate, | |
| 453 | keyDelete, | |
| 454 | keysPage, | |
| 455 | passwordPage, | |
| 456 | resendVerifyLink, | |
| 457 | passwordUpdate, | |
| 458 | tokenCreate, | |
| 459 | sessionDelete, | |
| 460 | tokenDelete, | |
| 461 | tokensPage, | |
| 462 | } from "../web/pages/account.ts"; | |
| 463 | import { completeOAuthCallback } from "../actions/origins.ts"; | |
| 464 | 381 | import { |
| 465 | 382 | browseBlob, |
| 466 | 383 | browseBranchesPage, |
| @@ -479,21 +396,7 @@ import { | ||
| 479 | 396 | browsePulls, |
| 480 | 397 | browseRaw, |
| 481 | 398 | browseTree, |
| 482 | mirrorJobPage, | |
| 483 | 399 | } from "../web/pages/browse.ts"; |
| 484 | import { | |
| 485 | originAppCreate, | |
| 486 | originAppDelete, | |
| 487 | originConnectToken, | |
| 488 | originDisconnect, | |
| 489 | originOAuthStart, | |
| 490 | originSyncSubmit, | |
| 491 | originsPage, | |
| 492 | originReplaceToken, | |
| 493 | originTrustedHosts, | |
| 494 | } from "../web/pages/origins.ts"; | |
| 495 | import { originFlowPage, originFlowPoll } from "../web/pages/origin-flow.ts"; | |
| 496 | import { originMirrorSubmit, originReposPage } from "../web/pages/origin-repos.ts"; | |
| 497 | 400 | import { pushThrough } from "../origins/push-through.ts"; |
| 498 | 401 | import { notFoundPage } from "../web/chrome.ts"; |
| 499 | 402 | import { targetForShortCode } from "../origins/oauth.ts"; |
| @@ -862,8 +765,9 @@ export function buildRouter(): Router<Handler> { | ||
| 862 | 765 | router.add("GET", "/contribute", contributePage); |
| 863 | 766 | // The one path a change follows, in four words (#7703); literal, above /:owner. |
| 864 | 767 | router.add("GET", "/pipeline", pipelinePage); |
| 865 | router.add("GET", "/settings/telemetry", telemetryPage); | |
| 866 | router.add("POST", "/settings/telemetry", telemetrySet); | |
| 768 | // Every /settings… page, in its own module (settings_routes.ts); literal, above /:owner. | |
| 769 | // The usage page is handed the pattern list, so settings_routes.ts never imports server.ts. | |
| 770 | settingsRoutes(router, knownPatterns); | |
| 867 | 771 | // Each THREATS.md protection against the last gate; literal, above /:owner. |
| 868 | 772 | router.add("GET", "/resilience", resiliencePage); |
| 869 | 773 | // A selection's spend and runs over time; literal, above /:owner. |
| @@ -897,35 +801,10 @@ export function buildRouter(): Router<Handler> { | ||
| 897 | 801 | }); |
| 898 | 802 | }); |
| 899 | 803 | router.add("GET", "/runs/:id", runRedirect); |
| 900 | router.add("GET", "/settings", accountPage); | |
| 901 | router.add("POST", "/settings", accountUpdate); | |
| 902 | router.add("POST", "/settings/notify", notifyUpdate); | |
| 903 | router.add("POST", "/settings/edge/link", edgeLink); | |
| 904 | router.add("POST", "/settings/edge/unlink", edgeUnlink); | |
| 905 | router.add("POST", "/settings/parent/link", parentLinkStart); | |
| 906 | router.add("POST", "/settings/parent/unlink", parentUnlink); | |
| 907 | router.add("POST", "/settings/email/verify", resendVerifyLink); | |
| 908 | router.add("GET", "/settings/password", passwordPage); | |
| 909 | router.add("POST", "/settings/password", passwordUpdate); | |
| 910 | // The everyday account: an admin's daily account, apart from admin (#596). | |
| 911 | router.add("POST", "/settings/everyday", everydayCreate); | |
| 912 | router.add("POST", "/settings/everyday/options", everydayOptions); | |
| 913 | router.add("POST", "/settings/everyday/switch", everydaySwitch); | |
| 914 | router.add("POST", "/settings/everyday/keep", everydayKeep); | |
| 915 | router.add("GET", "/settings/tokens", tokensPage); | |
| 916 | router.add("POST", "/settings/tokens", tokenCreate); | |
| 917 | router.add("POST", "/settings/tokens/:id/delete", tokenDelete); | |
| 918 | router.add("POST", "/settings/sessions/:id/delete", sessionDelete); | |
| 919 | 804 | router.add("GET", "/talk", talkView); |
| 920 | 805 | router.add("POST", "/talk", talkSend); |
| 921 | 806 | router.add("GET", "/talk/stream", talkStream); |
| 922 | 807 | router.add("POST", "/talk/confirm/:id", talkConfirm); |
| 923 | router.add("GET", "/settings/confirms", confirmsList); | |
| 924 | router.add("POST", "/settings/confirms/:id", confirmYes); | |
| 925 | router.add("GET", "/settings/passkeys", passkeysPage); | |
| 926 | router.add("POST", "/settings/passkeys/options", passkeyOptions); | |
| 927 | router.add("POST", "/settings/passkeys", passkeyAdd); | |
| 928 | router.add("POST", "/settings/passkeys/:id/delete", passkeyDelete); | |
| 929 | 808 | router.add("POST", "/signin/passkey/options", signinOptions); |
| 930 | 809 | router.add("POST", "/signin/passkey", signinPasskey); |
| 931 | 810 | router.add("GET", "/recover", recoverPage); |
| @@ -934,33 +813,6 @@ export function buildRouter(): Router<Handler> { | ||
| 934 | 813 | router.add("POST", "/recover/passkey", recoverFinish); |
| 935 | 814 | router.add("POST", "/signup/passkey/options", signupOptions); |
| 936 | 815 | router.add("POST", "/signup/passkey", signupPasskey); |
| 937 | // The crew: a forge admin's section, hidden from everyone else. | |
| 938 | router.add("GET", "/settings/subscriptions", subscriptionsPage); | |
| 939 | router.add("POST", "/settings/subscriptions", subscriptionAdd); | |
| 940 | router.add("POST", "/settings/subscriptions/:id/delete", subscriptionDelete); | |
| 941 | router.add("POST", "/settings/subscriptions/:id/wake", subscriptionWake); | |
| 942 | router.add( | |
| 943 | "POST", | |
| 944 | "/settings/subscriptions/:id/headroom", | |
| 945 | subscriptionHeadroom, | |
| 946 | ); | |
| 947 | router.add("POST", "/settings/subscriptions/:id/budget", subscriptionBudget); | |
| 948 | router.add("GET", "/settings/secrets", secretsPage); | |
| 949 | router.add("POST", "/settings/secrets", secretSet); | |
| 950 | router.add("POST", "/settings/secrets/delete", secretDelete); | |
| 951 | router.add("GET", "/settings/cloud", cloudPage); | |
| 952 | router.add("POST", "/settings/cloud", cloudAdd); | |
| 953 | router.add("POST", "/settings/cloud/verify", cloudVerify); | |
| 954 | router.add("POST", "/settings/cloud/write", cloudWrite); | |
| 955 | router.add("POST", "/settings/cloud/audit", cloudAudit); | |
| 956 | router.add("GET", "/settings/cloud/audit", cloudAuditPage); | |
| 957 | router.add("POST", "/settings/cloud/rotate", cloudRotate); | |
| 958 | router.add("POST", "/settings/cloud/rename", cloudRename); | |
| 959 | router.add("POST", "/settings/cloud/remove", cloudRemove); | |
| 960 | router.add("GET", "/settings/access", accessPage); | |
| 961 | router.add("GET", "/settings/resilience", domainsMoved); | |
| 962 | router.add("GET", "/settings/capacity", capacityPage); | |
| 963 | router.add("POST", "/settings/capacity", capacitySet); | |
| 964 | 816 | router.add("GET", "/boards", boardsList); |
| 965 | 817 | router.add("GET", "/boards/data", boardsPage); |
| 966 | 818 | router.add("POST", "/boards/new", boardNew); |
| @@ -970,86 +822,12 @@ export function buildRouter(): Router<Handler> { | ||
| 970 | 822 | router.add("GET", "/boards/:slug", boardShow); |
| 971 | 823 | router.add("GET", "/boards/:slug/live", boardLive); |
| 972 | 824 | router.add("POST", "/boards/:slug/:op", boardPost); |
| 973 | router.add("GET", "/settings/flags", flagsPage); | |
| 974 | router.add("POST", "/settings/flags", flagsSet); | |
| 975 | router.add("GET", "/settings/usage", (ctx) => usagePage(ctx, knownPatterns)); | |
| 976 | router.add("GET", "/settings/spend", spendPage); | |
| 977 | 825 | router.add("GET", "/admin/limits", limitsPage); |
| 978 | 826 | router.add("POST", "/admin/limits", limitsSet); |
| 979 | 827 | router.add("POST", "/admin/quotas", quotasSet); |
| 980 | 828 | router.add("POST", "/admin/signup", signupSet); |
| 981 | 829 | router.add("POST", "/admin/invites", inviteMint); |
| 982 | 830 | router.add("POST", "/admin/invites/:id/revoke", inviteRevoke); |
| 983 | router.add("GET", "/settings/backups", backupsPage); | |
| 984 | router.add("GET", "/settings/backups/live", backupsLive); | |
| 985 | router.add("POST", "/settings/backups/now", backupsNow); | |
| 986 | router.add("POST", "/settings/backups/drill", backupsDrill); | |
| 987 | router.add("POST", "/settings/backups/destination", backupsDestination); | |
| 988 | router.add("POST", "/settings/backups/test", backupsTest); | |
| 989 | router.add("POST", "/settings/backups/clear", backupsClear); | |
| 990 | router.add("POST", "/settings/backups/mirror", backupsMirror); | |
| 991 | router.add("POST", "/settings/backups/mirror/clear", backupsMirrorClear); | |
| 992 | router.add("POST", "/settings/backups/mirror/now", backupsMirrorNow); | |
| 993 | router.add("GET", "/settings/accounts", accountsPage); | |
| 994 | router.add("GET", "/settings/people", peoplePage); | |
| 995 | router.add("POST", "/settings/people/:username/role", roleAct); | |
| 996 | router.add("GET", "/settings/model", modelPage); | |
| 997 | router.add("POST", "/settings/model", modelAdd); | |
| 998 | router.add("POST", "/settings/accounts/:username/model", grantAct); | |
| 999 | router.add("POST", "/settings/accounts/:username/ceiling", ceilingAct); | |
| 1000 | router.add("GET", "/settings/accounts/:username", personSignInsPage); | |
| 1001 | router.add( | |
| 1002 | "POST", | |
| 1003 | "/settings/accounts/:username/tokens/:id/delete", | |
| 1004 | signInAct("token"), | |
| 1005 | ); | |
| 1006 | router.add( | |
| 1007 | "POST", | |
| 1008 | "/settings/accounts/:username/sessions/:id/delete", | |
| 1009 | signInAct("session"), | |
| 1010 | ); | |
| 1011 | router.add( | |
| 1012 | "POST", | |
| 1013 | "/settings/accounts/:username/suspend", | |
| 1014 | accountAct("suspend"), | |
| 1015 | ); | |
| 1016 | router.add( | |
| 1017 | "POST", | |
| 1018 | "/settings/accounts/:username/unsuspend", | |
| 1019 | accountAct("unsuspend"), | |
| 1020 | ); | |
| 1021 | router.add("POST", "/settings/accounts/:username/remove", accountAct("remove")); | |
| 1022 | router.add("GET", "/settings/accounts/:username/retire", retirePage); | |
| 1023 | router.add("POST", "/settings/accounts/:username/retire", retireAct); | |
| 1024 | router.add("POST", "/settings/accounts/:username/verify", accountAct("verify")); | |
| 1025 | router.add("GET", "/settings/crew", crewPage); | |
| 1026 | router.add("POST", "/settings/crew", agentCreate); | |
| 1027 | router.add("GET", "/settings/teams", teamsPage); | |
| 1028 | router.add("GET", "/settings/teams/stream", teamsStream); | |
| 1029 | router.add("POST", "/settings/teams/:slug", teamSet); | |
| 1030 | router.add("GET", "/settings/crew/:username", agentPage); | |
| 1031 | router.add("POST", "/settings/crew/:username", agentUpdate); | |
| 1032 | router.add("POST", "/settings/crew/:username/delete", agentDelete); | |
| 1033 | router.add("POST", "/settings/crew/model-review/:id/take", modelReviewTake); | |
| 1034 | router.add("POST", "/settings/crew/model-review/:id/decline", modelReviewDecline); | |
| 1035 | // Where an OAuth authorization comes back to. Deliberately a plain route | |
| 1036 | // and not an API one: it is a browser redirect, possibly from a phone, and | |
| 1037 | // the only thing it carries is a `state` this forge issued minutes ago. | |
| 1038 | router.add("GET", "/settings/origins/callback", async (ctx) => { | |
| 1039 | const done = await completeOAuthCallback(ctx.app, ctx.auth.user, { | |
| 1040 | state: ctx.url.searchParams.get("state") ?? "", | |
| 1041 | code: ctx.url.searchParams.get("code") ?? "", | |
| 1042 | }); | |
| 1043 | const query = !done.ok | |
| 1044 | ? "error=state" | |
| 1045 | : done.value.status === "done" | |
| 1046 | ? "connected=1" | |
| 1047 | : `error=${encodeURIComponent(done.value.detail.slice(0, 120))}`; | |
| 1048 | return new Response(null, { | |
| 1049 | status: 303, | |
| 1050 | headers: { location: `/settings/origins?${query}` }, | |
| 1051 | }); | |
| 1052 | }); | |
| 1053 | 831 | |
| 1054 | 832 | // The short link for a connect in flight, and the one route here that is |
| 1055 | 833 | // deliberately anonymous: the device typing it is a phone that has never |
| @@ -1076,26 +854,6 @@ export function buildRouter(): Router<Handler> { | ||
| 1076 | 854 | }); |
| 1077 | 855 | }); |
| 1078 | 856 | |
| 1079 | // Origins. Literal thirds first, so `flows` and `apps` can never be | |
| 1080 | // captured as an origin id by the parameterized routes below them. | |
| 1081 | router.add("GET", "/settings/origins", originsPage); | |
| 1082 | router.add("POST", "/settings/origins/token", originConnectToken); | |
| 1083 | router.add("POST", "/settings/origins/oauth", originOAuthStart); | |
| 1084 | router.add("POST", "/settings/origins/apps", originAppCreate); | |
| 1085 | router.add("POST", "/settings/origins/trusted-hosts", originTrustedHosts); | |
| 1086 | router.add("POST", "/settings/origins/apps/:id/delete", originAppDelete); | |
| 1087 | router.add("GET", "/settings/origins/flows/:id", originFlowPage); | |
| 1088 | router.add("POST", "/settings/origins/flows/:id/poll", originFlowPoll); | |
| 1089 | router.add("GET", "/settings/origins/:id/repos", originReposPage); | |
| 1090 | router.add("POST", "/settings/origins/:id/mirror", originMirrorSubmit); | |
| 1091 | // A mirror is a job with stages, so it has a page of its own to watch it | |
| 1092 | // on. The literal "mirror" third segment is registered above the :id/:job | |
| 1093 | // shape for the same reason `flows` and `apps` are. | |
| 1094 | router.add("GET", "/settings/origins/:id/mirror/:job", mirrorJobPage); | |
| 1095 | router.add("POST", "/settings/origins/:id/sync", originSyncSubmit); | |
| 1096 | router.add("POST", "/settings/origins/:id/delete", originDisconnect); | |
| 1097 | router.add("POST", "/settings/origins/:id/token", originReplaceToken); | |
| 1098 | ||
| 1099 | 857 | // Browsing a repository on an origin, live and read-only. Registered here, |
| 1100 | 858 | // above every parameterized repo route, so `/origins` can never be captured |
| 1101 | 859 | // as an owner — the same reservation `/new` and `/go` already make. Each |
| @@ -1142,10 +900,6 @@ export function buildRouter(): Router<Handler> { | ||
| 1142 | 900 | browseContributeSubmit, |
| 1143 | 901 | ); |
| 1144 | 902 | |
| 1145 | router.add("GET", "/settings/keys", keysPage); | |
| 1146 | router.add("POST", "/settings/keys", keyCreate); | |
| 1147 | router.add("POST", "/settings/keys/:id/delete", keyDelete); | |
| 1148 | ||
| 1149 | 903 | // git smart HTTP |
| 1150 | 904 | router.add("GET", "/:owner/:name.git/info/refs", (ctx) => { |
| 1151 | 905 | const service = ctx.url.searchParams.get("service"); |
src/http/settings_routes.ts added +265
| @@ -0,0 +1,265 @@ | ||
| 1 | /** | |
| 2 | * Every `/settings…` page route: the account, its tokens, sessions and | |
| 3 | * passkeys, and a forge admin's sections (subscriptions, secrets, cloud, | |
| 4 | * backups, accounts, crew, origins, keys). Moved out of `server.ts` (#8309), | |
| 5 | * unchanged and in their original order; `buildRouter` calls it once, where | |
| 6 | * the first of them stood, above every `/:owner`-shaped route, and hands | |
| 7 | * in the pattern list the usage page reads, so this module never imports it. | |
| 8 | */ | |
| 9 | ||
| 10 | import type { Handler } from "./context.ts"; | |
| 11 | import type { Router } from "./router.ts"; | |
| 12 | import { parentLinkStart } from "./rp.ts"; | |
| 13 | import { accessPage } from "../web/pages/access.ts"; | |
| 14 | import { capacityPage, capacitySet } from "../web/pages/capacity.ts"; | |
| 15 | import { teamSet, teamsPage, teamsStream } from "../web/pages/teams.ts"; | |
| 16 | import { telemetryPage, telemetrySet } from "../web/pages/telemetry.ts"; | |
| 17 | import { flagsPage, flagsSet } from "../web/pages/flags.ts"; | |
| 18 | import { usagePage } from "../web/pages/usage.ts"; | |
| 19 | import { spendPage } from "../web/pages/spend-tab.ts"; | |
| 20 | import { | |
| 21 | accountAct, | |
| 22 | retireAct, | |
| 23 | retirePage, | |
| 24 | accountsPage, | |
| 25 | personSignInsPage, | |
| 26 | signInAct, | |
| 27 | } from "../web/pages/accounts.ts"; | |
| 28 | import { confirmsList, confirmYes } from "../web/pages/confirms.ts"; | |
| 29 | import { passkeyAdd, passkeyDelete, passkeyOptions, passkeysPage } from "../web/pages/passkeys.ts"; | |
| 30 | import { secretDelete, secretSet, secretsPage } from "../web/pages/secrets.ts"; | |
| 31 | import { | |
| 32 | backupsClear, | |
| 33 | backupsDestination, | |
| 34 | backupsDrill, | |
| 35 | backupsLive, | |
| 36 | backupsMirror, | |
| 37 | backupsMirrorClear, | |
| 38 | backupsMirrorNow, | |
| 39 | backupsNow, | |
| 40 | backupsPage, | |
| 41 | backupsTest, | |
| 42 | domainsMoved, | |
| 43 | } from "../web/pages/backups.ts"; | |
| 44 | import { | |
| 45 | cloudAdd, | |
| 46 | cloudAudit, | |
| 47 | cloudAuditPage, | |
| 48 | cloudRename, | |
| 49 | cloudRotate, | |
| 50 | cloudPage, | |
| 51 | cloudRemove, | |
| 52 | cloudVerify, | |
| 53 | cloudWrite, | |
| 54 | } from "../web/pages/cloud.ts"; | |
| 55 | import { | |
| 56 | subscriptionAdd, | |
| 57 | subscriptionDelete, | |
| 58 | subscriptionsPage, | |
| 59 | subscriptionBudget, | |
| 60 | subscriptionHeadroom, | |
| 61 | subscriptionWake, | |
| 62 | } from "../web/pages/subscriptions.ts"; | |
| 63 | import { peoplePage, roleAct } from "../web/pages/people.ts"; | |
| 64 | import { ceilingAct, grantAct, modelAdd, modelPage } from "../web/pages/model.ts"; | |
| 65 | import { | |
| 66 | everydayCreate, | |
| 67 | everydayKeep, | |
| 68 | everydayOptions, | |
| 69 | everydaySwitch, | |
| 70 | } from "../web/pages/everyday.ts"; | |
| 71 | import { | |
| 72 | agentCreate, | |
| 73 | agentDelete, | |
| 74 | modelReviewDecline, | |
| 75 | modelReviewTake, | |
| 76 | agentPage, | |
| 77 | agentUpdate, | |
| 78 | crewPage, | |
| 79 | } from "../web/pages/crew.ts"; | |
| 80 | import { | |
| 81 | accountPage, | |
| 82 | notifyUpdate, | |
| 83 | edgeLink, | |
| 84 | edgeUnlink, | |
| 85 | parentUnlink, | |
| 86 | accountUpdate, | |
| 87 | keyCreate, | |
| 88 | keyDelete, | |
| 89 | keysPage, | |
| 90 | passwordPage, | |
| 91 | resendVerifyLink, | |
| 92 | passwordUpdate, | |
| 93 | tokenCreate, | |
| 94 | sessionDelete, | |
| 95 | tokenDelete, | |
| 96 | tokensPage, | |
| 97 | } from "../web/pages/account.ts"; | |
| 98 | import { completeOAuthCallback } from "../actions/origins.ts"; | |
| 99 | import { mirrorJobPage } from "../web/pages/browse.ts"; | |
| 100 | import { | |
| 101 | originAppCreate, | |
| 102 | originAppDelete, | |
| 103 | originConnectToken, | |
| 104 | originDisconnect, | |
| 105 | originOAuthStart, | |
| 106 | originSyncSubmit, | |
| 107 | originsPage, | |
| 108 | originReplaceToken, | |
| 109 | originTrustedHosts, | |
| 110 | } from "../web/pages/origins.ts"; | |
| 111 | import { originFlowPage, originFlowPoll } from "../web/pages/origin-flow.ts"; | |
| 112 | import { originMirrorSubmit, originReposPage } from "../web/pages/origin-repos.ts"; | |
| 113 | ||
| 114 | export function settingsRoutes(router: Router<Handler>, knownPatterns: () => readonly string[]): void { | |
| 115 | router.add("GET", "/settings/telemetry", telemetryPage); | |
| 116 | router.add("POST", "/settings/telemetry", telemetrySet); | |
| 117 | router.add("GET", "/settings", accountPage); | |
| 118 | router.add("POST", "/settings", accountUpdate); | |
| 119 | router.add("POST", "/settings/notify", notifyUpdate); | |
| 120 | router.add("POST", "/settings/edge/link", edgeLink); | |
| 121 | router.add("POST", "/settings/edge/unlink", edgeUnlink); | |
| 122 | router.add("POST", "/settings/parent/link", parentLinkStart); | |
| 123 | router.add("POST", "/settings/parent/unlink", parentUnlink); | |
| 124 | router.add("POST", "/settings/email/verify", resendVerifyLink); | |
| 125 | router.add("GET", "/settings/password", passwordPage); | |
| 126 | router.add("POST", "/settings/password", passwordUpdate); | |
| 127 | // The everyday account: an admin's daily account, apart from admin (#596). | |
| 128 | router.add("POST", "/settings/everyday", everydayCreate); | |
| 129 | router.add("POST", "/settings/everyday/options", everydayOptions); | |
| 130 | router.add("POST", "/settings/everyday/switch", everydaySwitch); | |
| 131 | router.add("POST", "/settings/everyday/keep", everydayKeep); | |
| 132 | router.add("GET", "/settings/tokens", tokensPage); | |
| 133 | router.add("POST", "/settings/tokens", tokenCreate); | |
| 134 | router.add("POST", "/settings/tokens/:id/delete", tokenDelete); | |
| 135 | router.add("POST", "/settings/sessions/:id/delete", sessionDelete); | |
| 136 | router.add("GET", "/settings/confirms", confirmsList); | |
| 137 | router.add("POST", "/settings/confirms/:id", confirmYes); | |
| 138 | router.add("GET", "/settings/passkeys", passkeysPage); | |
| 139 | router.add("POST", "/settings/passkeys/options", passkeyOptions); | |
| 140 | router.add("POST", "/settings/passkeys", passkeyAdd); | |
| 141 | router.add("POST", "/settings/passkeys/:id/delete", passkeyDelete); | |
| 142 | // The crew: a forge admin's section, hidden from everyone else. | |
| 143 | router.add("GET", "/settings/subscriptions", subscriptionsPage); | |
| 144 | router.add("POST", "/settings/subscriptions", subscriptionAdd); | |
| 145 | router.add("POST", "/settings/subscriptions/:id/delete", subscriptionDelete); | |
| 146 | router.add("POST", "/settings/subscriptions/:id/wake", subscriptionWake); | |
| 147 | router.add( | |
| 148 | "POST", | |
| 149 | "/settings/subscriptions/:id/headroom", | |
| 150 | subscriptionHeadroom, | |
| 151 | ); | |
| 152 | router.add("POST", "/settings/subscriptions/:id/budget", subscriptionBudget); | |
| 153 | router.add("GET", "/settings/secrets", secretsPage); | |
| 154 | router.add("POST", "/settings/secrets", secretSet); | |
| 155 | router.add("POST", "/settings/secrets/delete", secretDelete); | |
| 156 | router.add("GET", "/settings/cloud", cloudPage); | |
| 157 | router.add("POST", "/settings/cloud", cloudAdd); | |
| 158 | router.add("POST", "/settings/cloud/verify", cloudVerify); | |
| 159 | router.add("POST", "/settings/cloud/write", cloudWrite); | |
| 160 | router.add("POST", "/settings/cloud/audit", cloudAudit); | |
| 161 | router.add("GET", "/settings/cloud/audit", cloudAuditPage); | |
| 162 | router.add("POST", "/settings/cloud/rotate", cloudRotate); | |
| 163 | router.add("POST", "/settings/cloud/rename", cloudRename); | |
| 164 | router.add("POST", "/settings/cloud/remove", cloudRemove); | |
| 165 | router.add("GET", "/settings/access", accessPage); | |
| 166 | router.add("GET", "/settings/resilience", domainsMoved); | |
| 167 | router.add("GET", "/settings/capacity", capacityPage); | |
| 168 | router.add("POST", "/settings/capacity", capacitySet); | |
| 169 | router.add("GET", "/settings/flags", flagsPage); | |
| 170 | router.add("POST", "/settings/flags", flagsSet); | |
| 171 | router.add("GET", "/settings/usage", (ctx) => usagePage(ctx, knownPatterns)); | |
| 172 | router.add("GET", "/settings/spend", spendPage); | |
| 173 | router.add("GET", "/settings/backups", backupsPage); | |
| 174 | router.add("GET", "/settings/backups/live", backupsLive); | |
| 175 | router.add("POST", "/settings/backups/now", backupsNow); | |
| 176 | router.add("POST", "/settings/backups/drill", backupsDrill); | |
| 177 | router.add("POST", "/settings/backups/destination", backupsDestination); | |
| 178 | router.add("POST", "/settings/backups/test", backupsTest); | |
| 179 | router.add("POST", "/settings/backups/clear", backupsClear); | |
| 180 | router.add("POST", "/settings/backups/mirror", backupsMirror); | |
| 181 | router.add("POST", "/settings/backups/mirror/clear", backupsMirrorClear); | |
| 182 | router.add("POST", "/settings/backups/mirror/now", backupsMirrorNow); | |
| 183 | router.add("GET", "/settings/accounts", accountsPage); | |
| 184 | router.add("GET", "/settings/people", peoplePage); | |
| 185 | router.add("POST", "/settings/people/:username/role", roleAct); | |
| 186 | router.add("GET", "/settings/model", modelPage); | |
| 187 | router.add("POST", "/settings/model", modelAdd); | |
| 188 | router.add("POST", "/settings/accounts/:username/model", grantAct); | |
| 189 | router.add("POST", "/settings/accounts/:username/ceiling", ceilingAct); | |
| 190 | router.add("GET", "/settings/accounts/:username", personSignInsPage); | |
| 191 | router.add( | |
| 192 | "POST", | |
| 193 | "/settings/accounts/:username/tokens/:id/delete", | |
| 194 | signInAct("token"), | |
| 195 | ); | |
| 196 | router.add( | |
| 197 | "POST", | |
| 198 | "/settings/accounts/:username/sessions/:id/delete", | |
| 199 | signInAct("session"), | |
| 200 | ); | |
| 201 | router.add( | |
| 202 | "POST", | |
| 203 | "/settings/accounts/:username/suspend", | |
| 204 | accountAct("suspend"), | |
| 205 | ); | |
| 206 | router.add( | |
| 207 | "POST", | |
| 208 | "/settings/accounts/:username/unsuspend", | |
| 209 | accountAct("unsuspend"), | |
| 210 | ); | |
| 211 | router.add("POST", "/settings/accounts/:username/remove", accountAct("remove")); | |
| 212 | router.add("GET", "/settings/accounts/:username/retire", retirePage); | |
| 213 | router.add("POST", "/settings/accounts/:username/retire", retireAct); | |
| 214 | router.add("POST", "/settings/accounts/:username/verify", accountAct("verify")); | |
| 215 | router.add("GET", "/settings/crew", crewPage); | |
| 216 | router.add("POST", "/settings/crew", agentCreate); | |
| 217 | router.add("GET", "/settings/teams", teamsPage); | |
| 218 | router.add("GET", "/settings/teams/stream", teamsStream); | |
| 219 | router.add("POST", "/settings/teams/:slug", teamSet); | |
| 220 | router.add("GET", "/settings/crew/:username", agentPage); | |
| 221 | router.add("POST", "/settings/crew/:username", agentUpdate); | |
| 222 | router.add("POST", "/settings/crew/:username/delete", agentDelete); | |
| 223 | router.add("POST", "/settings/crew/model-review/:id/take", modelReviewTake); | |
| 224 | router.add("POST", "/settings/crew/model-review/:id/decline", modelReviewDecline); | |
| 225 | // Where an OAuth authorization comes back to. Deliberately a plain route | |
| 226 | // and not an API one: it is a browser redirect, possibly from a phone, and | |
| 227 | // the only thing it carries is a `state` this forge issued minutes ago. | |
| 228 | router.add("GET", "/settings/origins/callback", async (ctx) => { | |
| 229 | const done = await completeOAuthCallback(ctx.app, ctx.auth.user, { | |
| 230 | state: ctx.url.searchParams.get("state") ?? "", | |
| 231 | code: ctx.url.searchParams.get("code") ?? "", | |
| 232 | }); | |
| 233 | const query = !done.ok | |
| 234 | ? "error=state" | |
| 235 | : done.value.status === "done" | |
| 236 | ? "connected=1" | |
| 237 | : `error=${encodeURIComponent(done.value.detail.slice(0, 120))}`; | |
| 238 | return new Response(null, { | |
| 239 | status: 303, | |
| 240 | headers: { location: `/settings/origins?${query}` }, | |
| 241 | }); | |
| 242 | }); | |
| 243 | // Origins. Literal thirds first, so `flows` and `apps` can never be | |
| 244 | // captured as an origin id by the parameterized routes below them. | |
| 245 | router.add("GET", "/settings/origins", originsPage); | |
| 246 | router.add("POST", "/settings/origins/token", originConnectToken); | |
| 247 | router.add("POST", "/settings/origins/oauth", originOAuthStart); | |
| 248 | router.add("POST", "/settings/origins/apps", originAppCreate); | |
| 249 | router.add("POST", "/settings/origins/trusted-hosts", originTrustedHosts); | |
| 250 | router.add("POST", "/settings/origins/apps/:id/delete", originAppDelete); | |
| 251 | router.add("GET", "/settings/origins/flows/:id", originFlowPage); | |
| 252 | router.add("POST", "/settings/origins/flows/:id/poll", originFlowPoll); | |
| 253 | router.add("GET", "/settings/origins/:id/repos", originReposPage); | |
| 254 | router.add("POST", "/settings/origins/:id/mirror", originMirrorSubmit); | |
| 255 | // A mirror is a job with stages, so it has a page of its own to watch it | |
| 256 | // on. The literal "mirror" third segment is registered above the :id/:job | |
| 257 | // shape for the same reason `flows` and `apps` are. | |
| 258 | router.add("GET", "/settings/origins/:id/mirror/:job", mirrorJobPage); | |
| 259 | router.add("POST", "/settings/origins/:id/sync", originSyncSubmit); | |
| 260 | router.add("POST", "/settings/origins/:id/delete", originDisconnect); | |
| 261 | router.add("POST", "/settings/origins/:id/token", originReplaceToken); | |
| 262 | router.add("GET", "/settings/keys", keysPage); | |
| 263 | router.add("POST", "/settings/keys", keyCreate); | |
| 264 | router.add("POST", "/settings/keys/:id/delete", keyDelete); | |
| 265 | } |
test/design-audit.test.ts +6 −2
| @@ -1,12 +1,16 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * docs/design/audit.md has one entry for every top-level page, and names no |
| 3 | 3 | * route the router does not have (#2709). The router is read as text: the |
| 4 | * static GET paths in src/http/server.ts, less the ones that are not pages. | |
| 4 | * static GET paths in src/http/server.ts and settings_routes.ts, less the | |
| 5 | * ones that are not pages. | |
| 5 | 6 | */ |
| 6 | 7 | import { expect, test } from "bun:test"; |
| 7 | 8 | import { readFileSync } from "node:fs"; |
| 8 | 9 | |
| 9 | const server = readFileSync("src/http/server.ts", "utf8"); | |
| 10 | // The settings routes register from their own module (#8309), so both are read. | |
| 11 | const server = ["src/http/server.ts", "src/http/settings_routes.ts"] | |
| 12 | .map((file) => readFileSync(file, "utf8")) | |
| 13 | .join("\n"); | |
| 10 | 14 | const doc = readFileSync("docs/design/audit.md", "utf8"); |
| 11 | 15 | |
| 12 | 16 | /** Routes that answer a file, a fragment, a redirect hop or a callback. */ |
test/server-timing-sweep.test.ts +5 −2
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * Every GET page names its Server-Timing phases (#3387). The routes come from |
| 3 | * `src/http/server.ts` itself, so a new page is swept the day it is added. | |
| 3 | * `src/http/server.ts` and `settings_routes.ts` themselves, so a new page is | |
| 4 | * swept the day it is added. | |
| 4 | 5 | * `UNNAMED` is the debt: each page that still answers with `app` alone, and |
| 5 | 6 | * the issue that will fix it. The list may not outlive its pages: a page that |
| 6 | 7 | * starts naming `session` and `render` must come off it. |
| @@ -52,7 +53,9 @@ const REDIRECTS = new Set([ | ||
| 52 | 53 | |
| 53 | 54 | /** Every GET pattern the router source registers, one-line or wrapped. */ |
| 54 | 55 | function registeredGets(): Set<string> { |
| 55 | const source = readFileSync(join(import.meta.dir, "../src/http/server.ts"), "utf8"); | |
| 56 | const source = ["server.ts", "settings_routes.ts"] | |
| 57 | .map((file) => readFileSync(join(import.meta.dir, "../src/http", file), "utf8")) | |
| 58 | .join("\n"); | |
| 56 | 59 | const found = new Set<string>(); |
| 57 | 60 | for (const m of source.matchAll(/router\.add\(\s*"GET",\s*"([^"]+)"/g)) found.add(m[1]!); |
| 58 | 61 | return found; |
test/settings-routes.test.ts added +150
| @@ -0,0 +1,150 @@ | ||
| 1 | /** | |
| 2 | * The `/settings…` routes live in `src/http/settings_routes.ts` (#8309). | |
| 3 | * The studio's router holds exactly what `settingsRoutes` registers, as one | |
| 4 | * block in the same order; every settings path still reaches its own route | |
| 5 | * (nothing registered above the block captures it); and the routes recorded | |
| 6 | * when they moved out of `server.ts` that are still there keep that order. | |
| 7 | */ | |
| 8 | import { expect, test } from "bun:test"; | |
| 9 | import type { Handler } from "../src/http/context.ts"; | |
| 10 | import { Router } from "../src/http/router.ts"; | |
| 11 | import { buildRouter } from "../src/http/server.ts"; | |
| 12 | import { settingsRoutes } from "../src/http/settings_routes.ts"; | |
| 13 | ||
| 14 | const isSettings = (pattern: string) => pattern === "/settings" || pattern.startsWith("/settings/"); | |
| 15 | const key = (r: { method: string; pattern: string }) => `${r.method} ${r.pattern}`; | |
| 16 | ||
| 17 | /** The table as it stood in server.ts when it moved, in registration order. */ | |
| 18 | const RECORDED = ` | |
| 19 | GET /settings/telemetry | |
| 20 | POST /settings/telemetry | |
| 21 | GET /settings | |
| 22 | POST /settings | |
| 23 | POST /settings/notify | |
| 24 | POST /settings/edge/link | |
| 25 | POST /settings/edge/unlink | |
| 26 | POST /settings/parent/link | |
| 27 | POST /settings/parent/unlink | |
| 28 | POST /settings/email/verify | |
| 29 | GET /settings/password | |
| 30 | POST /settings/password | |
| 31 | POST /settings/everyday | |
| 32 | POST /settings/everyday/options | |
| 33 | POST /settings/everyday/switch | |
| 34 | POST /settings/everyday/keep | |
| 35 | GET /settings/tokens | |
| 36 | POST /settings/tokens | |
| 37 | POST /settings/tokens/:id/delete | |
| 38 | POST /settings/sessions/:id/delete | |
| 39 | GET /settings/confirms | |
| 40 | POST /settings/confirms/:id | |
| 41 | GET /settings/passkeys | |
| 42 | POST /settings/passkeys/options | |
| 43 | POST /settings/passkeys | |
| 44 | POST /settings/passkeys/:id/delete | |
| 45 | GET /settings/subscriptions | |
| 46 | POST /settings/subscriptions | |
| 47 | POST /settings/subscriptions/:id/delete | |
| 48 | POST /settings/subscriptions/:id/wake | |
| 49 | POST /settings/subscriptions/:id/headroom | |
| 50 | POST /settings/subscriptions/:id/budget | |
| 51 | GET /settings/secrets | |
| 52 | POST /settings/secrets | |
| 53 | POST /settings/secrets/delete | |
| 54 | GET /settings/cloud | |
| 55 | POST /settings/cloud | |
| 56 | POST /settings/cloud/verify | |
| 57 | POST /settings/cloud/write | |
| 58 | POST /settings/cloud/audit | |
| 59 | GET /settings/cloud/audit | |
| 60 | POST /settings/cloud/rotate | |
| 61 | POST /settings/cloud/rename | |
| 62 | POST /settings/cloud/remove | |
| 63 | GET /settings/access | |
| 64 | GET /settings/resilience | |
| 65 | GET /settings/capacity | |
| 66 | POST /settings/capacity | |
| 67 | GET /settings/flags | |
| 68 | POST /settings/flags | |
| 69 | GET /settings/usage | |
| 70 | GET /settings/spend | |
| 71 | GET /settings/backups | |
| 72 | GET /settings/backups/live | |
| 73 | POST /settings/backups/now | |
| 74 | POST /settings/backups/drill | |
| 75 | POST /settings/backups/destination | |
| 76 | POST /settings/backups/test | |
| 77 | POST /settings/backups/clear | |
| 78 | POST /settings/backups/mirror | |
| 79 | POST /settings/backups/mirror/clear | |
| 80 | POST /settings/backups/mirror/now | |
| 81 | GET /settings/accounts | |
| 82 | GET /settings/people | |
| 83 | POST /settings/people/:username/role | |
| 84 | GET /settings/model | |
| 85 | POST /settings/model | |
| 86 | POST /settings/accounts/:username/model | |
| 87 | POST /settings/accounts/:username/ceiling | |
| 88 | GET /settings/accounts/:username | |
| 89 | POST /settings/accounts/:username/tokens/:id/delete | |
| 90 | POST /settings/accounts/:username/sessions/:id/delete | |
| 91 | POST /settings/accounts/:username/suspend | |
| 92 | POST /settings/accounts/:username/unsuspend | |
| 93 | POST /settings/accounts/:username/remove | |
| 94 | GET /settings/accounts/:username/retire | |
| 95 | POST /settings/accounts/:username/retire | |
| 96 | POST /settings/accounts/:username/verify | |
| 97 | GET /settings/crew | |
| 98 | POST /settings/crew | |
| 99 | GET /settings/teams | |
| 100 | GET /settings/teams/stream | |
| 101 | POST /settings/teams/:slug | |
| 102 | GET /settings/crew/:username | |
| 103 | POST /settings/crew/:username | |
| 104 | POST /settings/crew/:username/delete | |
| 105 | POST /settings/crew/model-review/:id/take | |
| 106 | POST /settings/crew/model-review/:id/decline | |
| 107 | GET /settings/origins/callback | |
| 108 | GET /settings/origins | |
| 109 | POST /settings/origins/token | |
| 110 | POST /settings/origins/oauth | |
| 111 | POST /settings/origins/apps | |
| 112 | POST /settings/origins/trusted-hosts | |
| 113 | POST /settings/origins/apps/:id/delete | |
| 114 | GET /settings/origins/flows/:id | |
| 115 | POST /settings/origins/flows/:id/poll | |
| 116 | GET /settings/origins/:id/repos | |
| 117 | POST /settings/origins/:id/mirror | |
| 118 | GET /settings/origins/:id/mirror/:job | |
| 119 | POST /settings/origins/:id/sync | |
| 120 | POST /settings/origins/:id/delete | |
| 121 | POST /settings/origins/:id/token | |
| 122 | GET /settings/keys | |
| 123 | POST /settings/keys | |
| 124 | POST /settings/keys/:id/delete | |
| 125 | `.trim().split("\n"); | |
| 126 | ||
| 127 | test("the studio's settings routes are exactly settingsRoutes', as one block in order", () => { | |
| 128 | const own = new Router<Handler>(); | |
| 129 | settingsRoutes(own, () => []); | |
| 130 | const mine = own.routes().map(key); | |
| 131 | expect(mine.every((r) => isSettings(r.split(" ")[1]!))).toBe(true); | |
| 132 | const all = buildRouter().routes().map(key); | |
| 133 | const first = all.indexOf(mine[0]!); | |
| 134 | expect(all.slice(first, first + mine.length)).toEqual(mine); | |
| 135 | expect(all.filter((r) => isSettings(r.split(" ")[1]!))).toEqual(mine); | |
| 136 | }); | |
| 137 | ||
| 138 | test("every settings path reaches its own route through the whole table", () => { | |
| 139 | const router = buildRouter(); | |
| 140 | for (const { method, pattern } of router.routes().filter((r) => isSettings(r.pattern))) { | |
| 141 | const path = pattern.replace(/:\w+/g, "x"); | |
| 142 | expect(`${method} ${router.match(method, path)?.pattern}`).toBe(`${method} ${pattern}`); | |
| 143 | } | |
| 144 | }); | |
| 145 | ||
| 146 | test("the routes recorded when the block moved keep their order, where they remain", () => { | |
| 147 | const settings = buildRouter().routes().map(key).filter((r) => isSettings(r.split(" ")[1]!)); | |
| 148 | const remaining = new Set(settings); | |
| 149 | expect(settings.filter((r) => RECORDED.includes(r))).toEqual(RECORDED.filter((r) => remaining.has(r))); | |
| 150 | }); |
new above
offline
This creates a server.ts <-> settings_routes.ts import cycle. Move knownPatterns to its own module and import it from there in both files.