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

The Subscriptions page's refusal words live in one module both doors read, and the page is unchanged #5471

Merged opened this

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

Merged

mason/forge:mason/5466-the-subscriptions-page-s-refusal-words-l → main 3 commits fork 2 threads · 2 unresolved

  • Merged. mason/5466-the-subscriptions-page-s-refusal-words-l was merged into main as 058f3b0.

Review

Sign in to review this change.
4 files changed +88 −17
src/actions/model_grant.ts +8 −5
@@ -15,15 +15,18 @@ import { getUserByName, type User } from "../core/users.ts";
1515 import { err, ok, type Result } from "../kernel/result.ts";
1616 import { requireUser } from "./access.ts";
1717 import type { SubscriptionError } from "../core/subscriptions.ts";
18 import { SUBSCRIPTION_WORDS } from "../core/subscriptions_words.ts";
1819
19 /** What a refused add says, on every surface that adds one. */
20 /**
21 * What a refused add says, on every surface that adds one. The token and
22 * API base sentences are the Subscriptions page's own; the name sentences
23 * differ on purpose, since a person has one subscription of their own.
24 */
2025 export const ADD_MESSAGES = {
2126 invalid_name: "That name will not do.",
2227 name_taken: "You already added your own subscription.",
23 invalid_token:
24 "That is not a model token: one line, no spaces. One from `claude setup-token` begins sk-ant-oat; anything else is taken as an API key.",
25 invalid_api_base:
26 "The API base URL must be https, with no credentials, query, or hash (a Claude subscription token takes none).",
28 invalid_token: SUBSCRIPTION_WORDS.invalid_token,
29 invalid_api_base: SUBSCRIPTION_WORDS.invalid_api_base,
2730 not_allowed: "Only a person adds their own subscription.",
2831 };
2932
src/core/subscriptions_words.ts added +29
@@ -0,0 +1,29 @@
1 /**
2 * Subscriptions' refusal words — the sentences an admin reads when adding,
3 * removing or tuning an account is refused. One place, so every door says
outdated

The doc says the settings page and the MCP tools share these, but no MCP tool imports it yet; src/actions/model_grant.ts ADD_MESSAGES still has its own copies. Wire that door to this module.

4 * the same thing: the Subscriptions page reads them all, and a person's own
5 * add (`actions/model_grant.ts`, on /settings/model and in the terminal)

No MCP tool reads this yet; drop the sentence so the comment names only real readers.

6 * reads the token and API base sentences. An MCP tool for these reads here.
7 */
8
9 import { HEADROOM_MAX } from "./standing.ts";
10 import { NAME_MAX, type SubscriptionError } from "./subscriptions.ts";
11
12 export const SUBSCRIPTION_WORDS = {
13 invalid_name: `A name is a word or two, up to ${NAME_MAX} characters.`,
14 name_taken: (name: string): string =>
15 `There is already a subscription called ${name.trim()}.`,
16 invalid_token:
17 "That is not a model token: one line, no spaces. One from `claude setup-token` begins sk-ant-oat; anything else is taken as an API key (sk-ant-api…, sk-ant-usr…, sk-or-v1-…, or a gateway's own with its base URL).",
18 invalid_api_base:
19 "The API base URL must be https, with no credentials, query, or hash. For OpenRouter use https://openrouter.ai/api (a Claude subscription token cannot use a gateway).",
20 headroom_range: `Headroom is a whole percent from 0 to ${HEADROOM_MAX}.`,
21 not_found: "No such subscription.",
22 } as const;
23
24 /** The sentence for an add refused with `error`; `name` is what was typed. */
25 export function addRefusal(error: SubscriptionError, name: string): string {
26 return error === "name_taken"
27 ? SUBSCRIPTION_WORDS.name_taken(name)
28 : SUBSCRIPTION_WORDS[error];
29 }
src/web/pages/subscriptions.ts +8 −12
@@ -24,6 +24,10 @@ import {
2424 wakeSubscription,
2525 type Subscription,
2626 } from "../../core/subscriptions.ts";
27 import {
28 addRefusal,
29 SUBSCRIPTION_WORDS,
30 } from "../../core/subscriptions_words.ts";
2731 import {
2832 HEADROOM_MAX,
2933 headroomPct,
@@ -417,17 +421,9 @@ export async function subscriptionAdd(ctx: Ctx): Promise<Response> {
417421 createdBy: ctx.auth.user!.id,
418422 });
419423 if (!added.ok) {
420 const messages = {
421 invalid_name: `A name is a word or two, up to ${NAME_MAX} characters.`,
422 name_taken: `There is already a subscription called ${name.trim()}.`,
423 invalid_token:
424 "That is not a model token: one line, no spaces. One from `claude setup-token` begins sk-ant-oat; anything else is taken as an API key (sk-ant-api…, sk-ant-usr…, sk-or-v1-…, or a gateway's own with its base URL).",
425 invalid_api_base:
426 "The API base URL must be https, with no credentials, query, or hash. For OpenRouter use https://openrouter.ai/api (a Claude subscription token cannot use a gateway).",
427 };
428424 return flashTo(ctx, POOL, {
429425 tone: "danger",
430 message: messages[added.error],
426 message: addRefusal(added.error, name),
431427 });
432428 }
433429 return flashTo(ctx, POOL, {
@@ -450,7 +446,7 @@ export async function subscriptionDelete(ctx: Ctx): Promise<Response> {
450446 message:
451447 "Removed, with its grants. Runs it paid for keep it in their history.",
452448 }
453 : { tone: "danger", message: "No such subscription." },
449 : { tone: "danger", message: SUBSCRIPTION_WORDS.not_found },
454450 );
455451 }
456452
@@ -502,7 +498,7 @@ export async function subscriptionHeadroom(ctx: Ctx): Promise<Response> {
502498 if (pct === undefined)
503499 return flashTo(ctx, POOL, {
504500 tone: "danger",
505 message: `Headroom is a whole percent from 0 to ${HEADROOM_MAX}.`,
501 message: SUBSCRIPTION_WORDS.headroom_range,
506502 });
507503 const saved = setHeadroom(ctx.app.db, ctx.params.id ?? "", pct);
508504 return flashTo(
@@ -512,7 +508,7 @@ export async function subscriptionHeadroom(ctx: Ctx): Promise<Response> {
512508 ? {
513509 message: `The crew now leaves ${pct}% of each window for people.`,
514510 }
515 : { tone: "danger", message: "No such subscription." },
511 : { tone: "danger", message: SUBSCRIPTION_WORDS.not_found },
516512 );
517513 }
518514
test/subscriptions-words.test.ts added +43
@@ -0,0 +1,43 @@
1 import { describe, expect, test } from "bun:test";
2 import { ADD_MESSAGES } from "../src/actions/model_grant.ts";
3 import { HEADROOM_MAX } from "../src/core/standing.ts";
4 import { NAME_MAX } from "../src/core/subscriptions.ts";
5 import {
6 addRefusal,
7 SUBSCRIPTION_WORDS,
8 } from "../src/core/subscriptions_words.ts";
9
10 describe("subscriptions' refusal words", () => {
11 test("every key is a non-empty sentence ending in a full stop", () => {
12 for (const words of Object.values(SUBSCRIPTION_WORDS)) {
13 const sentence = typeof words === "function" ? words("Team") : words;
14 expect(sentence.trim().length).toBeGreaterThan(1);
15 expect(sentence.endsWith(".")).toBe(true);
16 }
17 });
18
19 test("the limits are the code's own", () => {
20 expect(SUBSCRIPTION_WORDS.invalid_name).toContain(
21 `up to ${NAME_MAX} characters`,
22 );
23 expect(SUBSCRIPTION_WORDS.headroom_range).toContain(
24 `0 to ${HEADROOM_MAX}.`,
25 );
26 });
27
28 test("an add refusal names the taken name, trimmed", () => {
29 expect(addRefusal("name_taken", " Team ")).toBe(
30 "There is already a subscription called Team.",
31 );
32 expect(addRefusal("invalid_token", "Team")).toBe(
33 SUBSCRIPTION_WORDS.invalid_token,
34 );
35 });
36
37 test("a person's own add says the page's token and API base words", () => {
38 expect(ADD_MESSAGES.invalid_token).toBe(SUBSCRIPTION_WORDS.invalid_token);
39 expect(ADD_MESSAGES.invalid_api_base).toBe(
40 SUBSCRIPTION_WORDS.invalid_api_base,
41 );
42 });
43 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs