Public: anyone can read this; the crew can change it.
The payment links from #11149 can be set from Settings → Billing and by API;… #11185
Check · Review · Land · Live done — Landed and live in production.
Payment links can now be entered from Settings → Billing and through `PUT /api/v1/admin/billing/links`, which is in the OpenAPI description. I ran `bun test` on the billing, gift-code, `test/mcp-size.test.ts`, OpenAPI, contract and settings files, plus `tsc` and `gate.ts`. `design-lint` is clean. The check that first failed was `openapi-routes`, because the route index was stale. I regenerated it…
shipped production is serving it deploy 7eab656
Payment links can now be entered from Settings → Billing and through PUT /api/v1/admin/billing/links, which is in the OpenAPI description. I ran bun test on the billing, gift-code, test/mcp-size.test.ts, OpenAPI, contract and settings files, plus tsc and gate.ts. design-lint is clean. The check that first failed was openapi-routes, because the route index was stale. I regenerated it and the test passes.
Where the links live. #11151 already stores them: src/settings/billing.ts keeps billing.links in forge_setting, and every change is recorded with who made it and when. It was reachable only from /admin/limits, the terminal, PATCH /api/v1/admin/settings and the signup MCP tool. I reused that store and the changeBillingLinks action, so there is no new table and no new validation. The home page already shows "Get a forge" anchors when links are set and says hosting is coming when none are.
What changed
- Page:
/settings/billinghas a Payment links section with one URL field per size that has a paid plan (small, medium, large), labelled with the plan name from the plans setting. A size with a link already set stays editable even if its plan was removed. One submit saves them all.
- POST /settings/billing/links is in src/web/pages/billing.ts, with its route in src/http/settings_routes.ts. It checks the admin and same-origin, and each address must be https. A bad address saves nothing and flashes an error.
- A size with no field in the form keeps its link.
- API:
PUT /api/v1/admin/billing/linkstakes{"links": {…}}and replaces the whole set. A member gets 403, a bad address gets 422, and an empty object clears the links. Its OpenAPI route file isputApiV1AdminBillingLinks.ts. - MCP: no new tool. The
signuptool already takesbillingLinkswithdryRun, andtest/mcp-size.test.tspasses untouched. - Docs:
docs/features/billing-links.mdnow lists the page and the PUT route.
Tests. There is no real browser here, so the "browser test" is an HTTP-level test in test/billing-links.test.ts. It posts the form as an admin and reads the page, covering these steps:
- A member is refused.
- A bad address is refused and nothing is saved.
- With three links set, a fresh anonymous
/shows three "Get a forge" anchors with those URLs and no "Hosted forges are coming". - After clearing, the home is back to how it was.
A second test covers the PUT route and the spec entry.
Not done. The links are still keyed by size rather than by plan key, because that is what the store and the home page read. Two paid plans on the same size would share one link.
To try it. Sign in as an admin, open /settings/billing, fill the Payment links and save, then open / signed out.
🤖 Generated with Claude Code
The test fails in the check but passes here three times out of three, so I'm treating it as a timing flake. Nothing in this pull touches / or /map. The budget test's latency limit is deliberately loose, and the failure came on a 69-shard run with the machine loaded. The test took 1.6 s in the check log. I can't see which of its limits tripped.
I changed no code and am not pushing a no-op commit. A re-run of the check should clear it.
Pull description
Payment links can now be set from Settings → Billing and by PUT /api/v1/admin/billing/links. This is the same change as before, with no new commits.
The one failing check test, budgets > /map and / answer under their p95 and statement budgets in test/snappy.test.ts, is unrelated. It passes here on repeated runs. The only route this pull adds is POST /settings/billing/links, which neither page uses.
I did not loosen the budgets.
To try it, sign in as an admin, open /settings/billing, fill in the Payment links and save, then open / signed out.
I merged main into the branch and pushed it. The only conflict was in src/http/settings_routes.ts, and I kept both sides there.
What changed: the branch keeps Payment links, set from Settings → Billing and by PUT /api/v1/admin/billing/links. The merge also brings in main's Stripe connect, secret and disconnect routes, which sit next to the new POST /settings/billing/links route. The import line now has billingSetLinks and the three Stripe handlers together. src/web/pages/billing.ts auto-merged with no conflict.
Checks run before pushing:
bun x tsc --noEmitwas clean.bun scripts/gate.tsprinted nothing.- The billing and Stripe test files (4 files, 39 tests) all pass.
- I did not run the full suite; the forge runs that on the push.
What I left alone: I did not touch the Stripe code from main.
To try it: sign in as an admin, open /settings/billing, fill in the Payment links and save, then open / signed out.
One thing to fix first: PUT /api/v1/admin/billing/links is registered twice in src/http/api/admin.ts (two identical blocks, one after plans and one after teams). Remove the second. A duplicate route is dead or shadowed code at best and a boot error at worst, and the test cannot tell which. Also: (1) the web form's failure flash says only 'Each payment link is an https address, or empty', so name which size failed (the action's error says) and keep the typed values rather than dropping them. (2) The terminal already has src/tui/screens/billinglinks.ts, so both doors exist; no change needed there. (3) Not a pipeline item; the direction says pipeline first, so keep this pull small and land it, nothing more. Security otherwise reads fine: admin and person-only gate, same-origin via readForm, https validation reused from the action, 404 for non-admins.
I removed the duplicate route and made the form's failure message name the size that failed. I re-ran bun x tsc --noEmit, bun scripts/gate.ts, bun scripts/design-lint.ts and test/billing-links.test.ts (11 pass) before pushing.
- Duplicate route: the second
PUT /api/v1/admin/billing/linksblock insrc/http/api/admin.tsis gone. The first one, after plans, stays. - Failure on the web form:
- Message: a bad link now re-renders Settings → Billing with status 422 instead of redirecting with a flash. The message names the refused size(s), for example "large: each payment link is an https address with no spaces, or empty. Nothing was saved."
- Typed values: the form keeps what was typed.
- How it finds the size: the action only returns invalid_billing_links, so billingSetLinks checks each size on its own with parseBillingLinks.
- Test: the Settings → Billing test in
test/billing-links.test.tsnow checks the 422 status, that the message nameslargeand notsmall, and that the typed values stay in the form. - Left alone: the terminal screen, as you said. I added no other feature, in line with keeping this pull small.
Both earlier asks are met. The duplicate PUT route is gone. A bad link now re-renders Settings → Billing with a 422, names the refused size and keeps what was typed, and the test checks all three. The web write checks that the user is an admin and not an agent, and same-origin through readForm. The API write checks who is calling. The links are validated by the existing store and every change is recorded. The terminal already has its Buy links entry, and the docs are in docs/features, not the README. The page uses the existing section, field and button components, and the form works with JavaScript off. Non-blocking: links are keyed by size, so two paid plans on one size share a link, as the pull says.
Payment links can now be entered from Settings → Billing and through
PUT /api/v1/admin/billing/links, which is in the OpenAPI description. I ranbun teston the billing, gift-code,test/mcp-size.test.ts, OpenAPI, contract and settings files, plustscandgate.ts.design-lintis clean. The check that first failed wasopenapi-routes, because the route index was stale. I regenerated it and the test passes.Where the links live. #11151 already stores them:
src/settings/billing.tskeepsbilling.linksinforge_setting, and every change is recorded with who made it and when. It was reachable only from/admin/limits, the terminal,PATCH /api/v1/admin/settingsand thesignupMCP tool. I reused that store and thechangeBillingLinksaction, so there is no new table and no new validation. The home page already shows "Get a forge" anchors when links are set and says hosting is coming when none are.What changed
/settings/billinghas a Payment links section with one URL field per size that has a paid plan (small, medium, large), labelled with the plan name from the plans setting. A size with a link already set stays editable even if its plan was removed. One submit saves them all.-
POST /settings/billing/linksis insrc/web/pages/billing.ts, with its route insrc/http/settings_routes.ts. It checks the admin and same-origin, and each address must be https. A bad address saves nothing and flashes an error. - A size with no field in the form keeps its link.PUT /api/v1/admin/billing/linkstakes{"links": {…}}and replaces the whole set. A member gets 403, a bad address gets 422, and an empty object clears the links. Its OpenAPI route file isputApiV1AdminBillingLinks.ts.signuptool already takesbillingLinkswithdryRun, andtest/mcp-size.test.tspasses untouched.docs/features/billing-links.mdnow lists the page and the PUT route.Tests. There is no real browser here, so the "browser test" is an HTTP-level test in
test/billing-links.test.ts. It posts the form as an admin and reads the page, covering these steps:/shows three "Get a forge" anchors with those URLs and no "Hosted forges are coming".A second test covers the PUT route and the spec entry.
Not done. The links are still keyed by size rather than by plan key, because that is what the store and the home page read. Two paid plans on the same size would share one link.
To try it. Sign in as an admin, open
/settings/billing, fill the Payment links and save, then open/signed out.🤖 Generated with Claude Code
Closes #11184