From 87ba36ad0b821f3246120941798db5d82f4e4439 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 09:07:52 +0000 Subject: [PATCH 1/3] feat: list the repository for a Write API token prismic repo list reads GET /user/repositories when PRISMIC_TOKEN is set. A saved login still requests the legacy user service. Co-authored-by: tomasz.borowicz --- src/commands/repo-list.ts | 11 ++++- src/lib/prismic/clients/user.ts | 26 ++++++++++++ test/repo-list.test.ts | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/commands/repo-list.ts b/src/commands/repo-list.ts index ac4b464e..395832a7 100644 --- a/src/commands/repo-list.ts +++ b/src/commands/repo-list.ts @@ -1,7 +1,9 @@ import { getCredentials } from "../auth"; +import { env } from "../env"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getRepositories } from "../lib/prismic/clients/legacy-user"; +import { getRepositories as getLegacyRepositories } from "../lib/prismic/clients/legacy-user"; +import { getRepositories } from "../lib/prismic/clients/user"; import { formatTable } from "../lib/string"; const config = { @@ -17,7 +19,12 @@ export default createCommand(config, async ({ values }) => { const { token, host } = await getCredentials(); - const repos = await getRepositories({ token, host }); + // A saved login stays on user-service, which returns locale-scoped roles. + // PRISMIC_TOKEN reads GET /user/repositories. A Write API token gets the one + // repository that token can write to. + const repos = env.PRISMIC_TOKEN + ? await getRepositories({ token, host }) + : await getLegacyRepositories({ token, host }); if (json) { console.info( diff --git a/src/lib/prismic/clients/user.ts b/src/lib/prismic/clients/user.ts index 308f5c9d..af4c2252 100644 --- a/src/lib/prismic/clients/user.ts +++ b/src/lib/prismic/clients/user.ts @@ -9,6 +9,19 @@ const ProfileSchema = z.object({ }); export type Profile = z.infer; +// A role is a string (e.g. "Owner") for a repository with one role, or a +// record of locale to role (e.g. { "de-de": "Writer" }) when roles are +// locale-scoped. A Write API token sends a string role, or omits it. +const RepositorySchema = z.object({ + domain: z.string(), + name: z.optional(z.string()), + role: z.optional(z.union([z.string(), z.record(z.string(), z.string())])), +}); + +const RepositoryListSchema = z.object({ + repositories: z.array(RepositorySchema), +}); + export async function getProfile(config: { token: string | undefined; host: string; @@ -20,3 +33,16 @@ export async function getProfile(config: { unknownErrorMessage: "Failed to load your profile", }); } + +export async function getRepositories(config: { + token: string | undefined; + host: string; +}): Promise[]> { + const url = new URL("user/repositories", `https://api.internal.${config.host}/`); + const response = await request(url, { + headers: { Authorization: `Bearer ${config.token}` }, + schema: RepositoryListSchema, + unknownErrorMessage: "Failed to load your repositories", + }); + return response.repositories; +} diff --git a/test/repo-list.test.ts b/test/repo-list.test.ts index 1b18ad5b..9ce05d5a 100644 --- a/test/repo-list.test.ts +++ b/test/repo-list.test.ts @@ -1,3 +1,6 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + import { it } from "./it"; it("supports --help", async ({ expect, prismic }) => { @@ -18,3 +21,72 @@ it("lists repositories as JSON", async ({ expect, prismic, repo }) => { const parsed = JSON.parse(stdout); expect(parsed).toEqual(expect.arrayContaining([expect.objectContaining({ domain: repo })])); }); + +it("lists the one repository a Write API token can write to", async ({ + expect, + prismic, + repo, + host, + home, + writeToken, +}) => { + const fetches = await recordFetches(home); + const { stdout, stderr, exitCode } = await prismic("repo", ["list", "--json"], { + nodeOptions: { env: { PRISMIC_TOKEN: writeToken, ...fetches.env } }, + }); + expect(exitCode, stderr).toBe(0); + const parsed: unknown = JSON.parse(stdout); + expect(parsed).toEqual([ + { + domain: repo, + name: expect.any(String), + role: expect.any(String), + url: `https://${repo}.${host}/`, + }, + ]); + const urls = await fetches.urls(); + expect(urls).toContain(`https://api.internal.${host}/user/repositories`); + expect(urls).not.toContain(`https://user-service.${host}/repositories`); +}); + +it("requests the legacy user service when signed in", async ({ expect, prismic, host, home }) => { + const fetches = await recordFetches(home); + const { stderr, exitCode } = await prismic("repo", ["list"], { + nodeOptions: { env: fetches.env }, + }); + expect(exitCode, stderr).toBe(0); + const urls = await fetches.urls(); + expect(urls).toContain(`https://user-service.${host}/repositories`); + expect(urls).not.toContain(`https://api.internal.${host}/user/repositories`); +}); + +async function recordFetches(home: URL): Promise<{ + env: { NODE_OPTIONS: string; PRISMIC_TEST_FETCH_LOG: string }; + urls: () => Promise; +}> { + const log = new URL("fetch-log.txt", home); + const recorder = new URL("record-fetch.mjs", home); + await writeFile(recorder, FETCH_RECORDER); + return { + env: { + NODE_OPTIONS: `--import ${recorder.href}`, + PRISMIC_TEST_FETCH_LOG: fileURLToPath(log), + }, + urls: async () => { + const text = await readFile(log, "utf8").catch(() => ""); + return text.split("\n").filter((line) => line.length > 0); + }, + }; +} + +const FETCH_RECORDER = ` +import { appendFileSync } from "node:fs"; + +const logPath = process.env.PRISMIC_TEST_FETCH_LOG; +const originalFetch = globalThis.fetch; +globalThis.fetch = async (input, init) => { + const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url; + appendFileSync(logPath, url + "\\n"); + return originalFetch(input, init); +}; +`; From a561ab48828112a78016c2f6a24f6079ed9fb52d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 09:59:48 +0000 Subject: [PATCH 2/3] refactor: list every repository from the user API A saved login uses GET /user/repositories, the same call as a Write API token. Co-authored-by: tomasz.borowicz --- src/commands/repo-list.ts | 9 +-------- src/lib/prismic/clients/legacy-user.ts | 27 -------------------------- test/repo-list.test.ts | 6 +++--- 3 files changed, 4 insertions(+), 38 deletions(-) delete mode 100644 src/lib/prismic/clients/legacy-user.ts diff --git a/src/commands/repo-list.ts b/src/commands/repo-list.ts index 395832a7..801d623e 100644 --- a/src/commands/repo-list.ts +++ b/src/commands/repo-list.ts @@ -1,8 +1,6 @@ import { getCredentials } from "../auth"; -import { env } from "../env"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getRepositories as getLegacyRepositories } from "../lib/prismic/clients/legacy-user"; import { getRepositories } from "../lib/prismic/clients/user"; import { formatTable } from "../lib/string"; @@ -19,12 +17,7 @@ export default createCommand(config, async ({ values }) => { const { token, host } = await getCredentials(); - // A saved login stays on user-service, which returns locale-scoped roles. - // PRISMIC_TOKEN reads GET /user/repositories. A Write API token gets the one - // repository that token can write to. - const repos = env.PRISMIC_TOKEN - ? await getRepositories({ token, host }) - : await getLegacyRepositories({ token, host }); + const repos = await getRepositories({ token, host }); if (json) { console.info( diff --git a/src/lib/prismic/clients/legacy-user.ts b/src/lib/prismic/clients/legacy-user.ts deleted file mode 100644 index a3743602..00000000 --- a/src/lib/prismic/clients/legacy-user.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as z from "zod/mini"; - -import { request } from "../../request"; - -const RepositoriesSchema = z.array( - z.object({ - domain: z.string(), - name: z.optional(z.string()), - // A role is a string (e.g. "Owner") for repositories with a single - // role, or a record of locale to role (e.g. { "de-de": "Writer" }) - // for repositories with locale-scoped roles. - role: z.optional(z.union([z.string(), z.record(z.string(), z.string())])), - }), -); -type Repositories = z.infer; - -export async function getRepositories(config: { - token: string | undefined; - host: string; -}): Promise { - const url = new URL("repositories", `https://user-service.${config.host}/`); - return request(url, { - credentials: { "prismic-auth": config.token }, - schema: RepositoriesSchema, - unknownErrorMessage: "Failed to load your repositories", - }); -} diff --git a/test/repo-list.test.ts b/test/repo-list.test.ts index 9ce05d5a..74cc88ac 100644 --- a/test/repo-list.test.ts +++ b/test/repo-list.test.ts @@ -49,15 +49,15 @@ it("lists the one repository a Write API token can write to", async ({ expect(urls).not.toContain(`https://user-service.${host}/repositories`); }); -it("requests the legacy user service when signed in", async ({ expect, prismic, host, home }) => { +it("requests the user API when signed in", async ({ expect, prismic, host, home }) => { const fetches = await recordFetches(home); const { stderr, exitCode } = await prismic("repo", ["list"], { nodeOptions: { env: fetches.env }, }); expect(exitCode, stderr).toBe(0); const urls = await fetches.urls(); - expect(urls).toContain(`https://user-service.${host}/repositories`); - expect(urls).not.toContain(`https://api.internal.${host}/user/repositories`); + expect(urls).toContain(`https://api.internal.${host}/user/repositories`); + expect(urls).not.toContain(`https://user-service.${host}/repositories`); }); async function recordFetches(home: URL): Promise<{ From d3deea00155554255ad58d60bbcde110cca37114 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 15 Oct 2026 09:57:38 +0000 Subject: [PATCH 3/3] test: assert the printed repository list Drop the fetch recorder and the checks that name the request URL. Co-authored-by: tomasz.borowicz --- test/repo-list.test.ts | 52 +----------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/test/repo-list.test.ts b/test/repo-list.test.ts index 74cc88ac..a3357723 100644 --- a/test/repo-list.test.ts +++ b/test/repo-list.test.ts @@ -1,6 +1,3 @@ -import { readFile, writeFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; - import { it } from "./it"; it("supports --help", async ({ expect, prismic }) => { @@ -27,12 +24,10 @@ it("lists the one repository a Write API token can write to", async ({ prismic, repo, host, - home, writeToken, }) => { - const fetches = await recordFetches(home); const { stdout, stderr, exitCode } = await prismic("repo", ["list", "--json"], { - nodeOptions: { env: { PRISMIC_TOKEN: writeToken, ...fetches.env } }, + nodeOptions: { env: { PRISMIC_TOKEN: writeToken } }, }); expect(exitCode, stderr).toBe(0); const parsed: unknown = JSON.parse(stdout); @@ -44,49 +39,4 @@ it("lists the one repository a Write API token can write to", async ({ url: `https://${repo}.${host}/`, }, ]); - const urls = await fetches.urls(); - expect(urls).toContain(`https://api.internal.${host}/user/repositories`); - expect(urls).not.toContain(`https://user-service.${host}/repositories`); -}); - -it("requests the user API when signed in", async ({ expect, prismic, host, home }) => { - const fetches = await recordFetches(home); - const { stderr, exitCode } = await prismic("repo", ["list"], { - nodeOptions: { env: fetches.env }, - }); - expect(exitCode, stderr).toBe(0); - const urls = await fetches.urls(); - expect(urls).toContain(`https://api.internal.${host}/user/repositories`); - expect(urls).not.toContain(`https://user-service.${host}/repositories`); }); - -async function recordFetches(home: URL): Promise<{ - env: { NODE_OPTIONS: string; PRISMIC_TEST_FETCH_LOG: string }; - urls: () => Promise; -}> { - const log = new URL("fetch-log.txt", home); - const recorder = new URL("record-fetch.mjs", home); - await writeFile(recorder, FETCH_RECORDER); - return { - env: { - NODE_OPTIONS: `--import ${recorder.href}`, - PRISMIC_TEST_FETCH_LOG: fileURLToPath(log), - }, - urls: async () => { - const text = await readFile(log, "utf8").catch(() => ""); - return text.split("\n").filter((line) => line.length > 0); - }, - }; -} - -const FETCH_RECORDER = ` -import { appendFileSync } from "node:fs"; - -const logPath = process.env.PRISMIC_TEST_FETCH_LOG; -const originalFetch = globalThis.fetch; -globalThis.fetch = async (input, init) => { - const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url; - appendFileSync(logPath, url + "\\n"); - return originalFetch(input, init); -}; -`;