From 033311016387f9ad63389859bc1e8d2b7a6f3d96 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:03:59 +0000 Subject: [PATCH 1/8] refactor: merge Prismic clients into one file Move all Prismic service requests from src/lib/prismic/clients/ into src/lib/prismic/client.ts, grouped by service. The services now share one config type and two auth helpers: one sends the token as a Bearer header and one sends it as the prismic-auth cookie. Core and Wroom use the same repository domain, so they now share one request helper. The behavior does not change. AGENTS.md now describes the new file. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- AGENTS.md | 8 +- src/adapters/index.ts | 7 +- src/auth.ts | 2 +- src/commands/dev.ts | 3 +- src/commands/docs-list.ts | 2 +- src/commands/docs-view.ts | 2 +- src/commands/init.ts | 12 +- src/commands/locale-add.ts | 2 +- src/commands/locale-list.ts | 2 +- src/commands/locale-remove.ts | 2 +- src/commands/locale-set-master.ts | 2 +- src/commands/preview-add.ts | 2 +- src/commands/preview-list.ts | 2 +- src/commands/preview-remove.ts | 2 +- src/commands/preview-set-simulator.ts | 2 +- src/commands/push.ts | 3 +- src/commands/repo-create.ts | 3 +- src/commands/repo-list.ts | 2 +- src/commands/repo-set-api-access.ts | 2 +- src/commands/repo-set-name.ts | 2 +- src/commands/repo-view.ts | 3 +- src/commands/slice-add-variation.ts | 2 +- src/commands/slice-edit-variation.ts | 2 +- src/commands/status.ts | 2 +- src/commands/token-create.ts | 2 +- src/commands/token-delete.ts | 2 +- src/commands/token-list.ts | 2 +- src/commands/webhook-create.ts | 2 +- src/commands/webhook-disable.ts | 2 +- src/commands/webhook-enable.ts | 2 +- src/commands/webhook-list.ts | 2 +- src/commands/webhook-remove.ts | 2 +- src/commands/webhook-set-triggers.ts | 2 +- src/commands/webhook-view.ts | 2 +- src/commands/whoami.ts | 2 +- src/index.ts | 3 +- src/lib/prismic/client.ts | 892 ++++++++++++++++++++++++ src/lib/prismic/clients/auth.ts | 29 - src/lib/prismic/clients/core.ts | 163 ----- src/lib/prismic/clients/custom-types.ts | 157 ----- src/lib/prismic/clients/docs.ts | 60 -- src/lib/prismic/clients/locale.ts | 74 -- src/lib/prismic/clients/repository.ts | 102 --- src/lib/prismic/clients/user.ts | 48 -- src/lib/prismic/clients/wroom.ts | 326 --------- src/lib/prismic/environments.ts | 3 +- src/lib/prismic/models.ts | 2 +- src/lib/prismic/onboarding.ts | 2 +- src/tracking.ts | 2 +- 49 files changed, 939 insertions(+), 1019 deletions(-) create mode 100644 src/lib/prismic/client.ts delete mode 100644 src/lib/prismic/clients/auth.ts delete mode 100644 src/lib/prismic/clients/core.ts delete mode 100644 src/lib/prismic/clients/custom-types.ts delete mode 100644 src/lib/prismic/clients/docs.ts delete mode 100644 src/lib/prismic/clients/locale.ts delete mode 100644 src/lib/prismic/clients/repository.ts delete mode 100644 src/lib/prismic/clients/user.ts delete mode 100644 src/lib/prismic/clients/wroom.ts diff --git a/AGENTS.md b/AGENTS.md index 181203ea..63cdab01 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,11 +37,11 @@ or app state; callers pass in what it needs. Prismic-domain logic shared by multiple commands. Still application-unaware. -### `src/lib/prismic/clients/` +### `src/lib/prismic/client.ts` -One file per backend service. Network calls only: build the request, validate -the response, return typed data. No orchestration, no business decisions, no -console output. +Every request to a Prismic backend service, in one file grouped by service. +Network calls only: build the request, validate the response, return typed +data. No orchestration, no business decisions, no console output. ## `src/adapters/` diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 31b938ee..a0033319 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -19,12 +19,7 @@ import { } from "../lib/file"; import { stringify } from "../lib/json"; import { getInstalledMajor, readPackageJson } from "../lib/packageJson"; -import { - addPreview, - getPreviews, - getSimulatorUrl, - setSimulatorUrl, -} from "../lib/prismic/clients/core"; +import { addPreview, getPreviews, getSimulatorUrl, setSimulatorUrl } from "../lib/prismic/client"; import { canonicalizeCustomType, canonicalizeSlice, diff --git a/src/auth.ts b/src/auth.ts index ae97dd48..c14b6201 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -10,7 +10,7 @@ import { CREDENTIALS_PATH } from "./config"; import { DEFAULT_PRISMIC_HOST, env } from "./env"; import { exists, readJsonFile, writeFileRecursive } from "./lib/file"; import { stringify } from "./lib/json"; -import { refreshToken as baseRefreshToken } from "./lib/prismic/clients/auth"; +import { refreshToken as baseRefreshToken } from "./lib/prismic/client"; import { appendTrailingSlash } from "./lib/url"; import { forgetTrackedUser } from "./tracking"; diff --git a/src/commands/dev.ts b/src/commands/dev.ts index f23a2a3b..c5a05fb8 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -13,8 +13,7 @@ import { openBrowser } from "../lib/browser"; import { createCommand, type CommandConfig, CommandError, exclusiveOptions } from "../lib/command"; import { readJsonFile, watchFiles, writeFileRecursive } from "../lib/file"; import { stringify } from "../lib/json"; -import { createRelease, deleteRelease } from "../lib/prismic/clients/core"; -import type { CustomTypesConfig } from "../lib/prismic/clients/custom-types"; +import { createRelease, type CustomTypesConfig, deleteRelease } from "../lib/prismic/client"; import { canonicalizeCustomType, canonicalizeSlice, diff --git a/src/commands/docs-list.ts b/src/commands/docs-list.ts index 2190c5c5..3f8dc9b8 100644 --- a/src/commands/docs-list.ts +++ b/src/commands/docs-list.ts @@ -1,7 +1,7 @@ import { env } from "../env"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getDocsIndex, getDocsPageIndex } from "../lib/prismic/clients/docs"; +import { getDocsIndex, getDocsPageIndex } from "../lib/prismic/client"; import { formatTable } from "../lib/string"; const config = { diff --git a/src/commands/docs-view.ts b/src/commands/docs-view.ts index cadb4db5..acc7f8c2 100644 --- a/src/commands/docs-view.ts +++ b/src/commands/docs-view.ts @@ -3,7 +3,7 @@ import GithubSlugger from "github-slugger"; import { env } from "../env"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getDocsPageContent } from "../lib/prismic/clients/docs"; +import { getDocsPageContent } from "../lib/prismic/client"; import { getAnalyticsHeaders } from "../tracking"; const config = { diff --git a/src/commands/init.ts b/src/commands/init.ts index 9dd377c4..ba702796 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -16,16 +16,14 @@ import { } from "../lib/packageJson"; import { addPreview, - getPreviews, - removePreview, - setSimulatorUrl, -} from "../lib/prismic/clients/core"; -import { enableTypeBuilder, + getPreviews, + getProfile, getRepository, + removePreview, type Repository, -} from "../lib/prismic/clients/repository"; -import { getProfile } from "../lib/prismic/clients/user"; + setSimulatorUrl, +} from "../lib/prismic/client"; import { diffModels, getLegacySlices, getRemoteModels } from "../lib/prismic/models"; import { completeOnboardingSteps } from "../lib/prismic/onboarding"; import { ForbiddenRequestError, UnauthorizedRequestError } from "../lib/request"; diff --git a/src/commands/locale-add.ts b/src/commands/locale-add.ts index cc3ff4f8..5fb3c6d6 100644 --- a/src/commands/locale-add.ts +++ b/src/commands/locale-add.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; -import { upsertLocale } from "../lib/prismic/clients/locale"; +import { upsertLocale } from "../lib/prismic/client"; const config = { name: "prismic locale add", diff --git a/src/commands/locale-list.ts b/src/commands/locale-list.ts index 14bf8cba..6e02b584 100644 --- a/src/commands/locale-list.ts +++ b/src/commands/locale-list.ts @@ -2,7 +2,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getLocales } from "../lib/prismic/clients/locale"; +import { getLocales } from "../lib/prismic/client"; import { formatTable } from "../lib/string"; const config = { diff --git a/src/commands/locale-remove.ts b/src/commands/locale-remove.ts index 66da2df5..133a59dd 100644 --- a/src/commands/locale-remove.ts +++ b/src/commands/locale-remove.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; -import { removeLocale } from "../lib/prismic/clients/locale"; +import { removeLocale } from "../lib/prismic/client"; const config = { name: "prismic locale remove", diff --git a/src/commands/locale-set-master.ts b/src/commands/locale-set-master.ts index 029c1990..ea18d21d 100644 --- a/src/commands/locale-set-master.ts +++ b/src/commands/locale-set-master.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { getLocales, upsertLocale } from "../lib/prismic/clients/locale"; +import { getLocales, upsertLocale } from "../lib/prismic/client"; const config = { name: "prismic locale set-master", diff --git a/src/commands/preview-add.ts b/src/commands/preview-add.ts index 1cea62d3..534fa8c0 100644 --- a/src/commands/preview-add.ts +++ b/src/commands/preview-add.ts @@ -2,7 +2,7 @@ import { getActiveRepositoryName, getAdapter, NoSupportedFrameworkError } from " import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { MissingPackageJson } from "../lib/packageJson"; -import { addPreview } from "../lib/prismic/clients/core"; +import { addPreview } from "../lib/prismic/client"; const config = { name: "prismic preview add", diff --git a/src/commands/preview-list.ts b/src/commands/preview-list.ts index fba51177..eb6be79a 100644 --- a/src/commands/preview-list.ts +++ b/src/commands/preview-list.ts @@ -2,7 +2,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getPreviews, getSimulatorUrl } from "../lib/prismic/clients/core"; +import { getPreviews, getSimulatorUrl } from "../lib/prismic/client"; import { formatTable } from "../lib/string"; const config = { diff --git a/src/commands/preview-remove.ts b/src/commands/preview-remove.ts index 93ef5b9c..d36120cf 100644 --- a/src/commands/preview-remove.ts +++ b/src/commands/preview-remove.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { getPreviews, removePreview } from "../lib/prismic/clients/core"; +import { getPreviews, removePreview } from "../lib/prismic/client"; const config = { name: "prismic preview remove", diff --git a/src/commands/preview-set-simulator.ts b/src/commands/preview-set-simulator.ts index b5f1843d..d02a12b8 100644 --- a/src/commands/preview-set-simulator.ts +++ b/src/commands/preview-set-simulator.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { setSimulatorUrl } from "../lib/prismic/clients/core"; +import { setSimulatorUrl } from "../lib/prismic/client"; const config = { name: "prismic preview set-simulator", diff --git a/src/commands/push.ts b/src/commands/push.ts index eee9fda1..bccc05c6 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -6,8 +6,7 @@ import { getAdapter } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { getDirtyPaths, getGitRoot } from "../lib/git"; -import { getDocumentTotalByCustomTypes } from "../lib/prismic/clients/core"; -import { deleteScreenshots } from "../lib/prismic/clients/custom-types"; +import { deleteScreenshots, getDocumentTotalByCustomTypes } from "../lib/prismic/client"; import { diffModels, getRemoteModels, writeRemoteModels } from "../lib/prismic/models"; import { completeOnboardingSteps, type OnboardingStep } from "../lib/prismic/onboarding"; import { ForbiddenRequestError } from "../lib/request"; diff --git a/src/commands/repo-create.ts b/src/commands/repo-create.ts index 42cdd2a0..aacdc5cf 100644 --- a/src/commands/repo-create.ts +++ b/src/commands/repo-create.ts @@ -2,8 +2,7 @@ import { FRAMEWORKS, getAdapter } from "../adapters"; import { getCredentials } from "../auth"; import { detectAgent } from "../lib/ai"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { upsertLocale } from "../lib/prismic/clients/locale"; -import { checkIsDomainAvailable, createRepository } from "../lib/prismic/clients/wroom"; +import { checkIsDomainAvailable, createRepository, upsertLocale } from "../lib/prismic/client"; import { completeOnboardingSteps } from "../lib/prismic/onboarding"; const MAX_DOMAIN_TRIES = 5; diff --git a/src/commands/repo-list.ts b/src/commands/repo-list.ts index 801d623e..8eaeee19 100644 --- a/src/commands/repo-list.ts +++ b/src/commands/repo-list.ts @@ -1,7 +1,7 @@ import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getRepositories } from "../lib/prismic/clients/user"; +import { getRepositories } from "../lib/prismic/client"; import { formatTable } from "../lib/string"; const config = { diff --git a/src/commands/repo-set-api-access.ts b/src/commands/repo-set-api-access.ts index eb9caab7..ca44f233 100644 --- a/src/commands/repo-set-api-access.ts +++ b/src/commands/repo-set-api-access.ts @@ -1,6 +1,6 @@ import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { type RepositoryAccessLevel, setRepositoryAccess } from "../lib/prismic/clients/wroom"; +import { type RepositoryAccessLevel, setRepositoryAccess } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const VALID_LEVELS: RepositoryAccessLevel[] = ["private", "public", "open"]; diff --git a/src/commands/repo-set-name.ts b/src/commands/repo-set-name.ts index 3cb27376..4463f0b9 100644 --- a/src/commands/repo-set-name.ts +++ b/src/commands/repo-set-name.ts @@ -1,6 +1,6 @@ import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; -import { setRepositoryName } from "../lib/prismic/clients/wroom"; +import { setRepositoryName } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { diff --git a/src/commands/repo-view.ts b/src/commands/repo-view.ts index 5f6be791..65bd43b7 100644 --- a/src/commands/repo-view.ts +++ b/src/commands/repo-view.ts @@ -3,8 +3,7 @@ import { openBrowser } from "../lib/browser"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { isInvalidAuthContextError } from "../lib/prismic/auth"; -import { getRepository } from "../lib/prismic/clients/repository"; -import { getRepositoryAccess } from "../lib/prismic/clients/wroom"; +import { getRepository, getRepositoryAccess } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index ce5ad48a..57633236 100644 --- a/src/commands/slice-add-variation.ts +++ b/src/commands/slice-add-variation.ts @@ -6,7 +6,7 @@ import { getAdapter } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { readURLFile } from "../lib/file"; -import { uploadScreenshot } from "../lib/prismic/clients/custom-types"; +import { uploadScreenshot } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { diff --git a/src/commands/slice-edit-variation.ts b/src/commands/slice-edit-variation.ts index dca61c1b..5d01dc01 100644 --- a/src/commands/slice-edit-variation.ts +++ b/src/commands/slice-edit-variation.ts @@ -4,7 +4,7 @@ import { getAdapter } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { readURLFile } from "../lib/file"; -import { uploadScreenshot } from "../lib/prismic/clients/custom-types"; +import { uploadScreenshot } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { diff --git a/src/commands/status.ts b/src/commands/status.ts index 2050352b..5638eedc 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -3,7 +3,7 @@ import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; import { hasChanges } from "../lib/diff"; import { getDirtyPaths, getGitRoot } from "../lib/git"; -import { validateToken } from "../lib/prismic/clients/auth"; +import { validateToken } from "../lib/prismic/client"; import { diffModels, getLegacySlices, diff --git a/src/commands/token-create.ts b/src/commands/token-create.ts index 8fdb013f..df82d6f9 100644 --- a/src/commands/token-create.ts +++ b/src/commands/token-create.ts @@ -7,7 +7,7 @@ import { createOAuthApp, createWriteToken, getOAuthApps, -} from "../lib/prismic/clients/wroom"; +} from "../lib/prismic/client"; const CLI_APP_NAME = "Prismic CLI"; diff --git a/src/commands/token-delete.ts b/src/commands/token-delete.ts index d0afd931..df5d8be3 100644 --- a/src/commands/token-delete.ts +++ b/src/commands/token-delete.ts @@ -6,7 +6,7 @@ import { deleteWriteToken, getOAuthApps, getWriteTokens, -} from "../lib/prismic/clients/wroom"; +} from "../lib/prismic/client"; const config = { name: "prismic token delete", diff --git a/src/commands/token-list.ts b/src/commands/token-list.ts index 58208b5f..9c1e4f0b 100644 --- a/src/commands/token-list.ts +++ b/src/commands/token-list.ts @@ -2,7 +2,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getOAuthApps, getWriteTokens } from "../lib/prismic/clients/wroom"; +import { getOAuthApps, getWriteTokens } from "../lib/prismic/client"; import { formatTable } from "../lib/string"; const config = { diff --git a/src/commands/webhook-create.ts b/src/commands/webhook-create.ts index 6f1d2824..50c4ebe1 100644 --- a/src/commands/webhook-create.ts +++ b/src/commands/webhook-create.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { createWebhook, WEBHOOK_TRIGGERS } from "../lib/prismic/clients/wroom"; +import { createWebhook, WEBHOOK_TRIGGERS } from "../lib/prismic/client"; const config = { name: "prismic webhook create", diff --git a/src/commands/webhook-disable.ts b/src/commands/webhook-disable.ts index dafd85ce..26dd7294 100644 --- a/src/commands/webhook-disable.ts +++ b/src/commands/webhook-disable.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { getWebhooks, updateWebhook } from "../lib/prismic/clients/wroom"; +import { getWebhooks, updateWebhook } from "../lib/prismic/client"; const config = { name: "prismic webhook disable", diff --git a/src/commands/webhook-enable.ts b/src/commands/webhook-enable.ts index 7ad3c433..82e3fbb5 100644 --- a/src/commands/webhook-enable.ts +++ b/src/commands/webhook-enable.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { getWebhooks, updateWebhook } from "../lib/prismic/clients/wroom"; +import { getWebhooks, updateWebhook } from "../lib/prismic/client"; const config = { name: "prismic webhook enable", diff --git a/src/commands/webhook-list.ts b/src/commands/webhook-list.ts index 8db16fd1..fc086f7b 100644 --- a/src/commands/webhook-list.ts +++ b/src/commands/webhook-list.ts @@ -2,7 +2,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getWebhooks } from "../lib/prismic/clients/wroom"; +import { getWebhooks } from "../lib/prismic/client"; import { formatTable } from "../lib/string"; const config = { diff --git a/src/commands/webhook-remove.ts b/src/commands/webhook-remove.ts index 06de4d77..fb66359a 100644 --- a/src/commands/webhook-remove.ts +++ b/src/commands/webhook-remove.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { deleteWebhook, getWebhooks } from "../lib/prismic/clients/wroom"; +import { deleteWebhook, getWebhooks } from "../lib/prismic/client"; const config = { name: "prismic webhook remove", diff --git a/src/commands/webhook-set-triggers.ts b/src/commands/webhook-set-triggers.ts index a9f3ab3f..e5855f99 100644 --- a/src/commands/webhook-set-triggers.ts +++ b/src/commands/webhook-set-triggers.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { getWebhooks, updateWebhook, WEBHOOK_TRIGGERS } from "../lib/prismic/clients/wroom"; +import { getWebhooks, updateWebhook, WEBHOOK_TRIGGERS } from "../lib/prismic/client"; const config = { name: "prismic webhook set-triggers", diff --git a/src/commands/webhook-view.ts b/src/commands/webhook-view.ts index b61431cf..2d39e51c 100644 --- a/src/commands/webhook-view.ts +++ b/src/commands/webhook-view.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { getWebhooks, WEBHOOK_TRIGGERS } from "../lib/prismic/clients/wroom"; +import { getWebhooks, WEBHOOK_TRIGGERS } from "../lib/prismic/client"; const config = { name: "prismic webhook view", diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 66b2048c..f5f6ce1b 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -1,6 +1,6 @@ import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; -import { validateToken } from "../lib/prismic/clients/auth"; +import { validateToken } from "../lib/prismic/client"; const config = { name: "prismic whoami", diff --git a/src/index.ts b/src/index.ts index 5c3722f4..1ed07e9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,8 +19,7 @@ import { detectAgent } from "./lib/ai"; import { CommandError } from "./lib/command"; import { decodePayload } from "./lib/jwt"; import { MissingPackageJson } from "./lib/packageJson"; -import { UnsupportedFileTypeError } from "./lib/prismic/clients/custom-types"; -import { getProfile } from "./lib/prismic/clients/user"; +import { getProfile, UnsupportedFileTypeError } from "./lib/prismic/client"; import { InvalidEnvironmentError } from "./lib/prismic/environments"; import { FieldExistsError, diff --git a/src/lib/prismic/client.ts b/src/lib/prismic/client.ts new file mode 100644 index 00000000..54f74411 --- /dev/null +++ b/src/lib/prismic/client.ts @@ -0,0 +1,892 @@ +import { createHash } from "node:crypto"; + +import type { DynamicCustomTypeModel, SharedSliceModel } from "@prismicio/types-internal"; +import * as z from "zod/mini"; + +import { request, type RequestOptions } from "../request"; +import { appendTrailingSlash } from "../url"; + +type Config = { + token: string | undefined; + host: string; +}; + +type RepositoryConfig = Config & { repo: string }; + +// Auth service + +export async function refreshToken(token: string, config: { host: string }): Promise { + const url = new URL("refreshtoken", getAuthServiceUrl(config.host)); + url.searchParams.set("token", token); + return request(url, { schema: z.string() }); +} + +const SessionSchema = z.union([ + z.object({ type: z.literal("USER"), email: z.string(), shortId: z.string() }), + z.object({ type: z.literal("Machine2Machine"), domain: z.string(), appName: z.string() }), +]); + +export async function validateToken( + token: string | undefined, + config: { host: string }, +): Promise> { + const url = new URL("validate", getAuthServiceUrl(config.host)); + if (token) url.searchParams.set("token", token); + return request(url, { schema: SessionSchema }); +} + +function getAuthServiceUrl(host: string): URL { + return new URL(`https://auth.${host}/`); +} + +// User service + +const ProfileSchema = z.object({ + email: z.string(), + shortId: z.string(), + intercomHash: z.string(), +}); +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 UserRepositorySchema = z.object({ + domain: z.string(), + name: z.optional(z.string()), + role: z.optional(z.union([z.string(), z.record(z.string(), z.string())])), +}); + +const UserRepositoryListSchema = z.object({ + repositories: z.array(UserRepositorySchema), +}); + +export async function getProfile(config: Config): Promise { + const url = new URL("user/profile", getUserServiceUrl(config.host)); + return bearerRequest(url, config, { + schema: ProfileSchema, + unknownErrorMessage: "Failed to load your profile", + }); +} + +export async function getRepositories( + config: Config, +): Promise[]> { + const url = new URL("user/repositories", getUserServiceUrl(config.host)); + const response = await bearerRequest(url, config, { + schema: UserRepositoryListSchema, + unknownErrorMessage: "Failed to load your repositories", + }); + return response.repositories; +} + +function getUserServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/`); +} + +// Repository service + +const RepositoryStarterSchema = z.object({ + id: z.string(), + revision: z.string(), + framework: z.string(), + deploymentUrl: z.url(), +}); + +const RepositorySchema = z.object({ + name: z.optional(z.string()), + starter: z.nullish(RepositoryStarterSchema), + framework: z.optional(z.string()), + quotas: z.optional( + z.object({ + sliceMachineEnabled: z.boolean(), + }), + ), +}); + +export type Repository = z.infer; + +export function getRepository(config: RepositoryConfig): Promise { + const url = getRepositoryServiceUrl(config.host); + return repositoryServiceRequest(url, config, { schema: RepositorySchema }); +} + +export async function enableTypeBuilder( + framework: string, + config: RepositoryConfig, +): Promise { + const url = new URL("type-builder", getRepositoryServiceUrl(config.host)); + await repositoryServiceRequest(url, config, { method: "PATCH", json: { framework } }); +} + +const OnboardingStateSchema = z.object({ + completedSteps: z.array(z.string()), +}); +type OnboardingState = z.infer; + +export async function getOnboardingState(config: RepositoryConfig): Promise { + const url = new URL("onboarding", getRepositoryServiceUrl(config.host)); + return onboardingServiceRequest(url, config, { + schema: OnboardingStateSchema, + }); +} + +export async function toggleOnboardingStep( + stepId: string, + config: RepositoryConfig, +): Promise { + const url = new URL( + `onboarding/${encodeURIComponent(stepId)}/toggle`, + getRepositoryServiceUrl(config.host), + ); + return onboardingServiceRequest(url, config, { + method: "PATCH", + schema: OnboardingStateSchema, + }); +} + +function repositoryServiceRequest( + url: URL, + config: RepositoryConfig, + options: PrismicRequestOptions = {}, +): Promise { + const scopedUrl = new URL(url); + scopedUrl.searchParams.set("repository", config.repo); + return bearerRequest(scopedUrl, config, { + headers: { repository: config.repo }, + notFoundMessage: `Repository not found: ${config.repo}`, + ...options, + }); +} + +function onboardingServiceRequest( + url: URL, + config: RepositoryConfig, + options: PrismicRequestOptions = {}, +): Promise { + const scopedUrl = new URL(url); + scopedUrl.searchParams.set("repository", config.repo); + return cookieRequest(scopedUrl, config, options); +} + +function getRepositoryServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/repository/`); +} + +// Custom types service + +export type CustomTypesConfig = RepositoryConfig & { releaseId?: string }; + +export function getCustomTypes(config: CustomTypesConfig): Promise { + const url = new URL("customtypes", getCustomTypesServiceUrl(config.host)); + return customTypesServiceRequest(url, config); +} + +export function getSlices(config: CustomTypesConfig): Promise { + const url = new URL("slices", getCustomTypesServiceUrl(config.host)); + return customTypesServiceRequest(url, config); +} + +export type BulkChange = { + type: `${"CUSTOM_TYPE" | "SLICE"}_${"INSERT" | "UPDATE" | "DELETE"}`; + id: string; + payload: DynamicCustomTypeModel | SharedSliceModel | { id: string }; +}; + +export async function bulkUpdate(changes: BulkChange[], config: CustomTypesConfig): Promise { + const url = new URL("bulk-update", getCustomTypesServiceUrl(config.host)); + await customTypesServiceRequest(url, config, { + method: "POST", + json: { changes }, + unknownErrorMessage: "Failed to update models", + }); +} + +function customTypesServiceRequest( + url: URL, + config: CustomTypesConfig, + options: PrismicRequestOptions = {}, +): Promise { + if (config.releaseId) url.searchParams.set("release", config.releaseId); + return bearerRequest(url, config, { + headers: { repository: config.repo }, + notFoundMessage: `Repository not found: ${config.repo}`, + ...options, + }); +} + +function getCustomTypesServiceUrl(host: string): URL { + return new URL(`https://customtypes.${host}/`); +} + +// Screenshot service + +const ScreenshotPresignedUrlResponseSchema = z.object({ + values: z.object({ + url: z.string(), + fields: z.record(z.string(), z.string()), + }), + imgixEndpoint: z.string(), +}); + +const SUPPORTED_IMAGE_MIME_TYPES: Record = { + "image/png": ".png", + "image/jpeg": ".jpg", + "image/gif": ".gif", + "image/webp": ".webp", +}; + +export async function deleteScreenshots(sliceId: string, config: RepositoryConfig): Promise { + const url = new URL("delete", getScreenshotServiceUrl(config.host)); + await screenshotServiceRequest(url, config, { + method: "POST", + json: { sliceId }, + }); +} + +export async function uploadScreenshot( + blob: Blob, + config: RepositoryConfig & { sliceId: string; variationId: string }, +): Promise { + const { sliceId, variationId, repo, host } = config; + + const type = blob.type; + if (!(type in SUPPORTED_IMAGE_MIME_TYPES)) { + throw new UnsupportedFileTypeError(type); + } + + const presignedUrl = new URL("presigned-url", getScreenshotServiceUrl(host)); + const presigned = await screenshotServiceRequest(presignedUrl, config, { + schema: ScreenshotPresignedUrlResponseSchema, + }); + + const extension = SUPPORTED_IMAGE_MIME_TYPES[type]; + const digest = createHash("sha1") + .update(new Uint8Array(await blob.arrayBuffer())) + .digest("hex"); + const key = `${repo}/shared-slices/${sliceId}/${variationId}/${digest}${extension}`; + + const formData = new FormData(); + for (const [field, value] of Object.entries(presigned.values.fields)) { + formData.append(field, value); + } + formData.set("key", key); + formData.set("Content-Type", type); + formData.set("file", blob); + + await request(presigned.values.url, { method: "POST", body: formData }); + + const url = new URL(key, appendTrailingSlash(presigned.imgixEndpoint)); + url.searchParams.set("auto", "compress,format"); + + return url; +} + +export class UnsupportedFileTypeError extends Error { + name = "UnsupportedFileTypeError"; + + constructor(mimeType: string) { + const supportedTypes = Object.keys(SUPPORTED_IMAGE_MIME_TYPES); + super( + `Unsupported file type: ${mimeType || "unknown"}. Supported: ${supportedTypes.join(", ")}`, + ); + } +} + +function screenshotServiceRequest( + url: URL, + config: RepositoryConfig, + options: PrismicRequestOptions = {}, +): Promise { + const scopedUrl = new URL(url); + scopedUrl.searchParams.set("repository", config.repo); + return bearerRequest(scopedUrl, config, { + headers: { repository: config.repo }, + ...options, + }); +} + +function getScreenshotServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/screenshot/`); +} + +// Locale service + +const LocaleSchema = z.object({ + id: z.string(), + label: z.string(), + customName: z.nullable(z.string()), + isMaster: z.boolean(), +}); + +export type Locale = z.infer; + +export async function getLocales(config: RepositoryConfig): Promise { + const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); + const response = await localeServiceRequest(url, config, { + schema: z.object({ results: z.array(LocaleSchema) }), + unknownErrorMessage: "Failed to load locales", + }); + return response.results; +} + +export async function upsertLocale( + locale: { id: string; isMaster?: boolean; customName?: string }, + config: RepositoryConfig, +): Promise { + const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); + return localeServiceRequest(url, config, { + method: "POST", + json: { + id: locale.id, + isMaster: locale.isMaster ?? false, + ...(locale.customName ? { customName: locale.customName } : {}), + }, + schema: LocaleSchema, + unknownErrorMessage: "Failed to save locale", + }); +} + +export async function removeLocale(code: string, config: RepositoryConfig): Promise { + const url = new URL( + `repository/locales/${encodeURIComponent(code)}`, + getLocaleServiceUrl(config.host), + ); + await localeServiceRequest(url, config, { + method: "DELETE", + notFoundMessage: `Locale not found: ${code}`, + unknownErrorMessage: "Failed to remove locale", + }); +} + +function localeServiceRequest( + url: URL, + config: RepositoryConfig, + options: PrismicRequestOptions = {}, +): Promise { + const scopedUrl = new URL(url); + scopedUrl.searchParams.set("repository", config.repo); + return bearerRequest(scopedUrl, config, { + notFoundMessage: `Repository not found: ${config.repo}`, + ...options, + }); +} + +function getLocaleServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/locale/`); +} + +// Repository domain (Core and Wroom) + +const PreviewSchema = z.object({ + id: z.string(), + label: z.string(), + url: z.string(), +}); + +const GetPreviewsResponseSchema = z.object({ + results: z.array(PreviewSchema), +}); + +export type Preview = z.infer; + +export async function getPreviews(config: RepositoryConfig): Promise { + const url = new URL("core/repository/preview_configs", getRepositoryDomainUrl(config)); + const response = await repositoryDomainRequest(url, config, { + schema: GetPreviewsResponseSchema, + unknownErrorMessage: "Failed to load previews", + }); + return response.results; +} + +export async function addPreview( + previewConfig: { + name: string; + websiteURL: string; + resolverPath: string | undefined; + }, + config: RepositoryConfig, +): Promise { + const url = new URL("previews/new", getRepositoryDomainUrl(config)); + await repositoryDomainRequest(url, config, { + method: "POST", + json: { + name: previewConfig.name, + websiteURL: previewConfig.websiteURL, + resolverPath: previewConfig.resolverPath, + }, + unknownErrorMessage: "Failed to add preview", + }); +} + +export async function removePreview(id: string, config: RepositoryConfig): Promise { + const url = new URL(`previews/delete/${encodeURIComponent(id)}`, getRepositoryDomainUrl(config)); + await repositoryDomainRequest(url, config, { + method: "POST", + json: {}, + notFoundMessage: `Preview not found: ${id}`, + unknownErrorMessage: "Failed to remove preview", + }); +} + +const EnvironmentSchema = z.object({ + kind: z.enum(["prod", "stage", "dev"]), + name: z.string(), + domain: z.string(), + users: z.array(z.object({ id: z.string() })), +}); +export type Environment = z.infer; + +export async function getEnvironments(config: RepositoryConfig): Promise { + const url = new URL("core/environments", getRepositoryDomainUrl(config)); + const response = await repositoryDomainRequest(url, config, { + schema: z.object({ results: z.array(EnvironmentSchema) }), + }); + return response.results; +} + +const SimulatorUrlResponseSchema = z.object({ + simulator_url: z.optional(z.string()), +}); + +export async function getSimulatorUrl(config: RepositoryConfig): Promise { + const url = new URL("core/repository", getRepositoryDomainUrl(config)); + const response = await repositoryDomainRequest(url, config, { + schema: SimulatorUrlResponseSchema, + unknownErrorMessage: "Failed to load simulator URL", + }); + return response.simulator_url; +} + +export async function setSimulatorUrl( + simulatorUrl: string, + config: RepositoryConfig, +): Promise { + const url = new URL("core/repository", getRepositoryDomainUrl(config)); + await repositoryDomainRequest(url, config, { + method: "PATCH", + json: { simulator_url: simulatorUrl }, + unknownErrorMessage: "Failed to set simulator URL", + }); +} + +const DocumentSearchTotalSchema = z.object({ + total: z.number(), +}); + +export async function getDocumentTotalByCustomTypes( + customTypeId: string, + config: RepositoryConfig, +): Promise { + const url = new URL("core/documents/search", getRepositoryDomainUrl(config)); + const response = await repositoryDomainRequest(url, config, { + method: "POST", + json: { customTypes: [customTypeId], limit: 0 }, + schema: DocumentSearchTotalSchema, + }); + return response.total; +} + +export async function createRelease( + release: { label: string; hidden?: boolean }, + config: RepositoryConfig, +): Promise { + const url = new URL("core/releases", getRepositoryDomainUrl(config)); + const response = await repositoryDomainRequest(url, config, { + method: "POST", + json: release, + schema: z.looseObject({ id: z.string() }), + unknownErrorMessage: "Failed to create a release", + }); + return response.id; +} + +export async function deleteRelease(id: string, config: RepositoryConfig): Promise { + const url = new URL(`core/releases/${encodeURIComponent(id)}`, getRepositoryDomainUrl(config)); + await repositoryDomainRequest(url, config, { + method: "DELETE", + notFoundMessage: `Release not found: ${id}`, + unknownErrorMessage: "Failed to delete the release", + }); +} + +const WebhookTriggersSchema = z.object({ + documentsPublished: z.boolean(), + documentsUnpublished: z.boolean(), + releasesCreated: z.boolean(), + releasesUpdated: z.boolean(), + tagsCreated: z.boolean(), + tagsDeleted: z.boolean(), +}); + +export const WEBHOOK_TRIGGERS = Object.keys(WebhookTriggersSchema.shape); + +const WebhookSchema = z.object({ + config: z.extend(WebhookTriggersSchema, { + _id: z.string(), + url: z.string(), + active: z.boolean(), + name: z.nullable(z.string()), + secret: z.nullable(z.string()), + headers: z.record(z.string(), z.string()), + }), +}); +type Webhook = z.infer; + +export function getWebhooks(config: RepositoryConfig): Promise { + const url = new URL("app/settings/webhooks", getRepositoryDomainUrl(config)); + return repositoryDomainRequest(url, config, { + schema: z.array(WebhookSchema), + unknownErrorMessage: "Failed to load webhooks", + }); +} + +export async function createWebhook( + webhookConfig: Omit, + config: RepositoryConfig, +): Promise { + const body = toWebhookFormData({ ...webhookConfig, active: true, headers: {} }); + const url = new URL("app/settings/webhooks/create", getRepositoryDomainUrl(config)); + await repositoryDomainRequest(url, config, { + method: "POST", + body, + unknownErrorMessage: "Failed to create webhook", + }); +} + +export async function updateWebhook( + id: string, + webhookConfig: Omit, + config: RepositoryConfig, +): Promise { + const body = toWebhookFormData(webhookConfig); + const url = new URL( + `app/settings/webhooks/${encodeURIComponent(id)}`, + getRepositoryDomainUrl(config), + ); + await repositoryDomainRequest(url, config, { + method: "POST", + body, + notFoundMessage: `Webhook not found: ${id}`, + unknownErrorMessage: "Failed to update webhook", + }); +} + +export async function deleteWebhook(id: string, config: RepositoryConfig): Promise { + const url = new URL( + `app/settings/webhooks/${encodeURIComponent(id)}/delete`, + getRepositoryDomainUrl(config), + ); + await repositoryDomainRequest(url, config, { + method: "POST", + notFoundMessage: `Webhook not found: ${id}`, + unknownErrorMessage: "Failed to delete webhook", + }); +} + +function toWebhookFormData(webhookConfig: Omit): FormData { + const body = new FormData(); + body.set("url", webhookConfig.url); + body.set("name", webhookConfig.name ?? ""); + body.set("secret", webhookConfig.secret ?? ""); + body.set("headers", JSON.stringify(webhookConfig.headers ?? {})); + body.set("active", webhookConfig.active ? "on" : "off"); + body.set("documentsPublished", webhookConfig.documentsPublished.toString()); + body.set("documentsUnpublished", webhookConfig.documentsUnpublished.toString()); + body.set("releasesCreated", webhookConfig.releasesCreated.toString()); + body.set("releasesUpdated", webhookConfig.releasesUpdated.toString()); + body.set("tagsCreated", webhookConfig.tagsCreated.toString()); + body.set("tagsDeleted", webhookConfig.tagsDeleted.toString()); + return body; +} + +const AccessTokenSchema = z.object({ + id: z.string(), + scope: z.string(), + token: z.string(), + created_at: z.object({ $date: z.number() }), +}); +type AccessToken = z.infer; + +const OAuthAppSchema = z.object({ + id: z.string(), + name: z.string(), + wroom_auths: z.array(AccessTokenSchema), +}); +type OAuthApp = z.infer; + +const WriteTokenSchema = z.object({ + app_name: z.string(), + token: z.string(), + timestamp: z.number(), +}); +type WriteToken = z.infer; + +const WriteTokensInfoSchema = z.object({ + max_tokens: z.number(), + tokens: z.array(WriteTokenSchema), +}); +type WriteTokensInfo = z.infer; + +export function getOAuthApps(config: RepositoryConfig): Promise { + const url = new URL("settings/security/contentapi", getRepositoryDomainUrl(config)); + return repositoryDomainRequest(url, config, { + schema: z.array(OAuthAppSchema), + unknownErrorMessage: "Failed to load OAuth apps", + }); +} + +export async function createOAuthApp(name: string, config: RepositoryConfig): Promise { + const url = new URL("settings/security/oauthapp", getRepositoryDomainUrl(config)); + return repositoryDomainRequest(url, config, { + method: "POST", + json: { app_name: name }, + schema: OAuthAppSchema, + unknownErrorMessage: "Failed to create OAuth app", + }); +} + +export async function createOAuthAuthorization( + appId: string, + scope: string, + config: RepositoryConfig, +): Promise { + const url = new URL("settings/security/authorizations", getRepositoryDomainUrl(config)); + return repositoryDomainRequest(url, config, { + method: "POST", + json: { app: appId, scope }, + schema: AccessTokenSchema, + unknownErrorMessage: "Failed to create token", + }); +} + +export async function deleteOAuthAuthorization( + authId: string, + config: RepositoryConfig, +): Promise { + const url = new URL( + `settings/security/authorizations/${encodeURIComponent(authId)}`, + getRepositoryDomainUrl(config), + ); + await repositoryDomainRequest(url, config, { + method: "DELETE", + notFoundMessage: `Token not found: ${authId}`, + unknownErrorMessage: "Failed to delete token", + }); +} + +export function getWriteTokens(config: RepositoryConfig): Promise { + const url = new URL("settings/security/customtypesapi", getRepositoryDomainUrl(config)); + return repositoryDomainRequest(url, config, { + schema: WriteTokensInfoSchema, + unknownErrorMessage: "Failed to load write tokens", + }); +} + +export async function createWriteToken( + name: string, + config: RepositoryConfig, +): Promise { + const url = new URL("settings/security/token", getRepositoryDomainUrl(config)); + return repositoryDomainRequest(url, config, { + method: "POST", + json: { app_name: name }, + schema: WriteTokenSchema, + unknownErrorMessage: "Failed to create write token", + }); +} + +export async function deleteWriteToken( + tokenValue: string, + config: RepositoryConfig, +): Promise { + const url = new URL( + `settings/security/token/${encodeURIComponent(tokenValue)}`, + getRepositoryDomainUrl(config), + ); + await repositoryDomainRequest(url, config, { + method: "DELETE", + notFoundMessage: "Token not found", + unknownErrorMessage: "Failed to delete write token", + }); +} + +const SyncStateSchema = z.object({ + repository: z.object({ + api_access: z.string(), + }), +}); + +export async function getRepositoryAccess(config: RepositoryConfig): Promise { + const url = new URL("syncState", getRepositoryDomainUrl(config)); + const response = await repositoryDomainRequest(url, config, { + schema: SyncStateSchema, + unknownErrorMessage: "Failed to load repository access", + }); + return response.repository.api_access; +} + +export type RepositoryAccessLevel = "private" | "public" | "open"; + +export async function setRepositoryAccess( + level: RepositoryAccessLevel, + config: RepositoryConfig, +): Promise { + const url = new URL("settings/security/apiaccess", getRepositoryDomainUrl(config)); + await repositoryDomainRequest(url, config, { + method: "POST", + json: { api_access: level }, + unknownErrorMessage: "Failed to set repository access", + }); +} + +const SetNameResponseSchema = z.object({ + repository: z.object({ + name: z.string(), + }), +}); + +export async function setRepositoryName(name: string, config: RepositoryConfig): Promise { + const formData = new FormData(); + formData.set("displayname", name); + const url = new URL("app/settings/repository", getRepositoryDomainUrl(config)); + const response = await repositoryDomainRequest(url, config, { + method: "POST", + body: formData, + schema: SetNameResponseSchema, + unknownErrorMessage: "Failed to set repository name", + }); + return response.repository.name; +} + +function repositoryDomainRequest( + url: URL, + config: RepositoryConfig, + options: PrismicRequestOptions = {}, +): Promise { + return cookieRequest(url, config, { + notFoundMessage: `Repository not found: ${config.repo}`, + ...options, + }); +} + +function getRepositoryDomainUrl(config: RepositoryConfig): URL { + return new URL(`https://${config.repo}.${config.host}/`); +} + +// Dashboard (Wroom) + +export async function checkIsDomainAvailable( + config: Config & { domain: string }, +): Promise { + const url = new URL( + `app/dashboard/repositories/${encodeURIComponent(config.domain)}/exists`, + getDashboardUrl(config.host), + ); + return cookieRequest(url, config, { schema: z.boolean() }); +} + +export async function createRepository( + config: Config & { + domain: string; + name: string; + framework: string; + agent: string | undefined; + }, +): Promise { + const { domain, name, framework, agent, host } = config; + const url = new URL("app/dashboard/repositories", getDashboardUrl(host)); + url.searchParams.set("app", "cli"); + if (agent) url.searchParams.set("agent", agent); + await cookieRequest(url, config, { + method: "POST", + json: { domain, name, framework, plan: "personal" }, + unknownErrorMessage: "Failed to create repository", + }); +} + +function getDashboardUrl(host: string): URL { + return new URL(`https://${host}/`); +} + +// Documentation + +// Documentation is only published at prismic.io; the host option exists for +// overrides only. +const DEFAULT_DOCS_HOST = "prismic.io"; + +type DocsConfig = { host?: string; headers?: Record }; + +const DocsIndexEntrySchema = z.object({ + path: z.string(), + title: z.string(), + description: z.optional(z.string()), +}); +type DocsIndexEntry = z.infer; + +const DocsPageSchema = z.object({ + path: z.string(), + title: z.string(), + description: z.optional(z.string()), + anchors: z.array( + z.object({ + slug: z.string(), + excerpt: z.string(), + }), + ), +}); +type DocsPage = z.infer; + +export async function getDocsIndex(config: DocsConfig): Promise { + const url = new URL("api/index/", getDocsUrl(config.host)); + return request(url, { + schema: z.array(DocsIndexEntrySchema), + unknownErrorMessage: "Failed to fetch documentation index", + }); +} + +export async function getDocsPageIndex(path: string, config: DocsConfig): Promise { + const url = new URL(`api/index/${path}`, getDocsUrl(config.host)); + return request(url, { + schema: DocsPageSchema, + notFoundMessage: `Documentation page not found: ${path}`, + unknownErrorMessage: "Failed to fetch documentation index", + }); +} + +export async function getDocsPageContent(path: string, config: DocsConfig): Promise { + const url = new URL(path, getDocsUrl(config.host)); + return request(url, { + headers: { Accept: "text/markdown", ...config.headers }, + schema: z.string(), + notFoundMessage: `Page not found: ${path}`, + unknownErrorMessage: "Failed to fetch documentation page", + }); +} + +function getDocsUrl(host = DEFAULT_DOCS_HOST): URL { + return new URL(`https://${host}/docs/`); +} + +// Authentication + +type PrismicRequestOptions = RequestOptions & { headers?: Record }; + +function bearerRequest( + url: URL, + config: Config, + options: PrismicRequestOptions = {}, +): Promise { + return request(url, { + ...options, + headers: { ...options.headers, Authorization: `Bearer ${config.token}` }, + }); +} + +function cookieRequest( + url: URL, + config: Config, + options: PrismicRequestOptions = {}, +): Promise { + return request(url, { ...options, credentials: { "prismic-auth": config.token } }); +} diff --git a/src/lib/prismic/clients/auth.ts b/src/lib/prismic/clients/auth.ts deleted file mode 100644 index 49e425c4..00000000 --- a/src/lib/prismic/clients/auth.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as z from "zod/mini"; - -import { request } from "../../request"; - -type AuthConfig = { host: string }; - -export async function refreshToken(token: string, config: AuthConfig): Promise { - const url = new URL("refreshtoken", getAuthServiceUrl(config.host)); - url.searchParams.set("token", token); - return request(url, { schema: z.string() }); -} - -const SessionSchema = z.union([ - z.object({ type: z.literal("USER"), email: z.string(), shortId: z.string() }), - z.object({ type: z.literal("Machine2Machine"), domain: z.string(), appName: z.string() }), -]); - -export async function validateToken( - token: string | undefined, - config: AuthConfig, -): Promise> { - const url = new URL("validate", getAuthServiceUrl(config.host)); - if (token) url.searchParams.set("token", token); - return request(url, { schema: SessionSchema }); -} - -function getAuthServiceUrl(host: string): URL { - return new URL(`https://auth.${host}/`); -} diff --git a/src/lib/prismic/clients/core.ts b/src/lib/prismic/clients/core.ts deleted file mode 100644 index 8d271f38..00000000 --- a/src/lib/prismic/clients/core.ts +++ /dev/null @@ -1,163 +0,0 @@ -import * as z from "zod/mini"; - -import { request, type RequestOptions } from "../../request"; - -type CoreConfig = { - repo: string; - token: string | undefined; - host: string; -}; - -const PreviewSchema = z.object({ - id: z.string(), - label: z.string(), - url: z.string(), -}); - -const GetPreviewsResponseSchema = z.object({ - results: z.array(PreviewSchema), -}); - -export type Preview = z.infer; - -export async function getPreviews(config: CoreConfig): Promise { - const { repo, host } = config; - const url = new URL("core/repository/preview_configs", getCoreServiceUrl(repo, host)); - const response = await coreServiceRequest(url, config, { - schema: GetPreviewsResponseSchema, - unknownErrorMessage: "Failed to load previews", - }); - return response.results; -} - -export async function addPreview( - previewConfig: { - name: string; - websiteURL: string; - resolverPath: string | undefined; - }, - config: CoreConfig, -): Promise { - const { repo, host } = config; - const url = new URL("previews/new", getCoreServiceUrl(repo, host)); - await coreServiceRequest(url, config, { - method: "POST", - json: { - name: previewConfig.name, - websiteURL: previewConfig.websiteURL, - resolverPath: previewConfig.resolverPath, - }, - unknownErrorMessage: "Failed to add preview", - }); -} - -export async function removePreview(id: string, config: CoreConfig): Promise { - const { repo, host } = config; - const url = new URL(`previews/delete/${encodeURIComponent(id)}`, getCoreServiceUrl(repo, host)); - await coreServiceRequest(url, config, { - method: "POST", - json: {}, - notFoundMessage: `Preview not found: ${id}`, - unknownErrorMessage: "Failed to remove preview", - }); -} - -const EnvironmentSchema = z.object({ - kind: z.enum(["prod", "stage", "dev"]), - name: z.string(), - domain: z.string(), - users: z.array(z.object({ id: z.string() })), -}); -export type Environment = z.infer; - -export async function getEnvironments(config: CoreConfig): Promise { - const { repo, host } = config; - const url = new URL("core/environments", getCoreServiceUrl(repo, host)); - const response = await coreServiceRequest(url, config, { - schema: z.object({ results: z.array(EnvironmentSchema) }), - }); - return response.results; -} - -const RepositoryResponseSchema = z.object({ - simulator_url: z.optional(z.string()), -}); - -export async function getSimulatorUrl(config: CoreConfig): Promise { - const { repo, host } = config; - const url = new URL("core/repository", getCoreServiceUrl(repo, host)); - const response = await coreServiceRequest(url, config, { - schema: RepositoryResponseSchema, - unknownErrorMessage: "Failed to load simulator URL", - }); - return response.simulator_url; -} - -export async function setSimulatorUrl(simulatorUrl: string, config: CoreConfig): Promise { - const { repo, host } = config; - const url = new URL("core/repository", getCoreServiceUrl(repo, host)); - await coreServiceRequest(url, config, { - method: "PATCH", - json: { simulator_url: simulatorUrl }, - unknownErrorMessage: "Failed to set simulator URL", - }); -} - -const DocumentSearchTotalSchema = z.object({ - total: z.number(), -}); - -export async function getDocumentTotalByCustomTypes( - customTypeId: string, - config: CoreConfig, -): Promise { - const { repo, host } = config; - const url = new URL("core/documents/search", getCoreServiceUrl(repo, host)); - const response = await coreServiceRequest(url, config, { - method: "POST", - json: { customTypes: [customTypeId], limit: 0 }, - schema: DocumentSearchTotalSchema, - }); - return response.total; -} - -export async function createRelease( - release: { label: string; hidden?: boolean }, - config: CoreConfig, -): Promise { - const { repo, host } = config; - const url = new URL("core/releases", getCoreServiceUrl(repo, host)); - const response = await coreServiceRequest(url, config, { - method: "POST", - json: release, - schema: z.looseObject({ id: z.string() }), - unknownErrorMessage: "Failed to create a release", - }); - return response.id; -} - -export async function deleteRelease(id: string, config: CoreConfig): Promise { - const { repo, host } = config; - const url = new URL(`core/releases/${encodeURIComponent(id)}`, getCoreServiceUrl(repo, host)); - await coreServiceRequest(url, config, { - method: "DELETE", - notFoundMessage: `Release not found: ${id}`, - unknownErrorMessage: "Failed to delete the release", - }); -} - -function coreServiceRequest( - url: URL, - config: CoreConfig, - options: RequestOptions = {}, -): Promise { - return request(url, { - credentials: { "prismic-auth": config.token }, - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); -} - -function getCoreServiceUrl(repo: string, host: string): URL { - return new URL(`https://${repo}.${host}/`); -} diff --git a/src/lib/prismic/clients/custom-types.ts b/src/lib/prismic/clients/custom-types.ts deleted file mode 100644 index 29aed13c..00000000 --- a/src/lib/prismic/clients/custom-types.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { createHash } from "node:crypto"; - -import type { DynamicCustomTypeModel, SharedSliceModel } from "@prismicio/types-internal"; -import * as z from "zod/mini"; - -import { request, type RequestOptions } from "../../request"; -import { appendTrailingSlash } from "../../url"; - -export type CustomTypesConfig = { - repo: string; - token: string | undefined; - host: string; - releaseId?: string; -}; - -export function getCustomTypes(config: CustomTypesConfig): Promise { - const url = new URL("customtypes", getCustomTypesServiceUrl(config.host)); - return customTypesServiceRequest(url, config); -} - -export function getSlices(config: CustomTypesConfig): Promise { - const url = new URL("slices", getCustomTypesServiceUrl(config.host)); - return customTypesServiceRequest(url, config); -} - -export type BulkChange = { - type: `${"CUSTOM_TYPE" | "SLICE"}_${"INSERT" | "UPDATE" | "DELETE"}`; - id: string; - payload: DynamicCustomTypeModel | SharedSliceModel | { id: string }; -}; - -export async function bulkUpdate(changes: BulkChange[], config: CustomTypesConfig): Promise { - const url = new URL("bulk-update", getCustomTypesServiceUrl(config.host)); - await customTypesServiceRequest(url, config, { - method: "POST", - json: { changes }, - unknownErrorMessage: "Failed to update models", - }); -} - -const ScreenshotPresignedUrlResponseSchema = z.object({ - values: z.object({ - url: z.string(), - fields: z.record(z.string(), z.string()), - }), - imgixEndpoint: z.string(), -}); - -const SUPPORTED_IMAGE_MIME_TYPES: Record = { - "image/png": ".png", - "image/jpeg": ".jpg", - "image/gif": ".gif", - "image/webp": ".webp", -}; - -export async function deleteScreenshots(sliceId: string, config: CustomTypesConfig): Promise { - const url = new URL("delete", getScreenshotServiceUrl(config.host)); - await screenshotServiceRequest(url, config, { - method: "POST", - json: { sliceId }, - }); -} - -export async function uploadScreenshot( - blob: Blob, - config: { - sliceId: string; - variationId: string; - repo: string; - token: string | undefined; - host: string; - }, -): Promise { - const { sliceId, variationId, repo, host } = config; - - const type = blob.type; - if (!(type in SUPPORTED_IMAGE_MIME_TYPES)) { - throw new UnsupportedFileTypeError(type); - } - - const presignedUrl = new URL("presigned-url", getScreenshotServiceUrl(host)); - const presigned = await screenshotServiceRequest(presignedUrl, config, { - schema: ScreenshotPresignedUrlResponseSchema, - }); - - const extension = SUPPORTED_IMAGE_MIME_TYPES[type]; - const digest = createHash("sha1") - .update(new Uint8Array(await blob.arrayBuffer())) - .digest("hex"); - const key = `${repo}/shared-slices/${sliceId}/${variationId}/${digest}${extension}`; - - const formData = new FormData(); - for (const [field, value] of Object.entries(presigned.values.fields)) { - formData.append(field, value); - } - formData.set("key", key); - formData.set("Content-Type", type); - formData.set("file", blob); - - await request(presigned.values.url, { method: "POST", body: formData }); - - const url = new URL(key, appendTrailingSlash(presigned.imgixEndpoint)); - url.searchParams.set("auto", "compress,format"); - - return url; -} - -export class UnsupportedFileTypeError extends Error { - name = "UnsupportedFileTypeError"; - - constructor(mimeType: string) { - const supportedTypes = Object.keys(SUPPORTED_IMAGE_MIME_TYPES); - super( - `Unsupported file type: ${mimeType || "unknown"}. Supported: ${supportedTypes.join(", ")}`, - ); - } -} - -function customTypesServiceRequest( - url: URL, - config: CustomTypesConfig, - options: RequestOptions = {}, -): Promise { - if (config.releaseId) url.searchParams.set("release", config.releaseId); - return request(url, { - headers: { - repository: config.repo, - Authorization: `Bearer ${config.token}`, - }, - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); -} - -function screenshotServiceRequest( - url: URL, - config: CustomTypesConfig, - options: RequestOptions = {}, -): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return request(scopedUrl, { - headers: { - repository: config.repo, - Authorization: `Bearer ${config.token}`, - }, - ...options, - }); -} - -function getCustomTypesServiceUrl(host: string): URL { - return new URL(`https://customtypes.${host}/`); -} - -function getScreenshotServiceUrl(host: string): URL { - return new URL(`https://api.internal.${host}/screenshot/`); -} diff --git a/src/lib/prismic/clients/docs.ts b/src/lib/prismic/clients/docs.ts deleted file mode 100644 index 7b15dcc2..00000000 --- a/src/lib/prismic/clients/docs.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as z from "zod/mini"; - -import { request } from "../../request"; - -// Documentation is only published at prismic.io; the host option exists for -// overrides only. -const DEFAULT_DOCS_HOST = "prismic.io"; - -type DocsConfig = { host?: string; headers?: Record }; - -const DocsIndexEntrySchema = z.object({ - path: z.string(), - title: z.string(), - description: z.optional(z.string()), -}); -type DocsIndexEntry = z.infer; - -const DocsPageSchema = z.object({ - path: z.string(), - title: z.string(), - description: z.optional(z.string()), - anchors: z.array( - z.object({ - slug: z.string(), - excerpt: z.string(), - }), - ), -}); -type DocsPage = z.infer; - -export async function getDocsIndex(config: DocsConfig): Promise { - const url = new URL("api/index/", getDocsServiceUrl(config.host)); - return request(url, { - schema: z.array(DocsIndexEntrySchema), - unknownErrorMessage: "Failed to fetch documentation index", - }); -} - -export async function getDocsPageIndex(path: string, config: DocsConfig): Promise { - const url = new URL(`api/index/${path}`, getDocsServiceUrl(config.host)); - return request(url, { - schema: DocsPageSchema, - notFoundMessage: `Documentation page not found: ${path}`, - unknownErrorMessage: "Failed to fetch documentation index", - }); -} - -export async function getDocsPageContent(path: string, config: DocsConfig): Promise { - const url = new URL(path, getDocsServiceUrl(config.host)); - return request(url, { - headers: { Accept: "text/markdown", ...config.headers }, - schema: z.string(), - notFoundMessage: `Page not found: ${path}`, - unknownErrorMessage: "Failed to fetch documentation page", - }); -} - -function getDocsServiceUrl(host = DEFAULT_DOCS_HOST): URL { - return new URL(`https://${host}/docs/`); -} diff --git a/src/lib/prismic/clients/locale.ts b/src/lib/prismic/clients/locale.ts deleted file mode 100644 index 48b730d1..00000000 --- a/src/lib/prismic/clients/locale.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as z from "zod/mini"; - -import { request, type RequestOptions } from "../../request"; - -type LocaleConfig = { - repo: string; - token: string | undefined; - host: string; -}; - -const LocaleSchema = z.object({ - id: z.string(), - label: z.string(), - customName: z.nullable(z.string()), - isMaster: z.boolean(), -}); - -export type Locale = z.infer; - -export async function getLocales(config: LocaleConfig): Promise { - const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); - const response = await localeServiceRequest(url, config, { - schema: z.object({ results: z.array(LocaleSchema) }), - unknownErrorMessage: "Failed to load locales", - }); - return response.results; -} - -export async function upsertLocale( - locale: { id: string; isMaster?: boolean; customName?: string }, - config: LocaleConfig, -): Promise { - const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); - return localeServiceRequest(url, config, { - method: "POST", - json: { - id: locale.id, - isMaster: locale.isMaster ?? false, - ...(locale.customName ? { customName: locale.customName } : {}), - }, - schema: LocaleSchema, - unknownErrorMessage: "Failed to save locale", - }); -} - -export async function removeLocale(code: string, config: LocaleConfig): Promise { - const url = new URL( - `repository/locales/${encodeURIComponent(code)}`, - getLocaleServiceUrl(config.host), - ); - await localeServiceRequest(url, config, { - method: "DELETE", - notFoundMessage: `Locale not found: ${code}`, - unknownErrorMessage: "Failed to remove locale", - }); -} - -function localeServiceRequest( - url: URL, - config: LocaleConfig, - options?: RequestOptions, -): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return request(scopedUrl, { - headers: { Authorization: `Bearer ${config.token}` }, - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); -} - -function getLocaleServiceUrl(host: string): URL { - return new URL(`https://api.internal.${host}/locale/`); -} diff --git a/src/lib/prismic/clients/repository.ts b/src/lib/prismic/clients/repository.ts deleted file mode 100644 index 302b2165..00000000 --- a/src/lib/prismic/clients/repository.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as z from "zod/mini"; - -import { request, type RequestOptions } from "../../request"; - -type RepositoryConfig = { - repo: string; - token: string | undefined; - host: string; -}; - -const RepositoryStarterSchema = z.object({ - id: z.string(), - revision: z.string(), - framework: z.string(), - deploymentUrl: z.url(), -}); - -const RepositorySchema = z.object({ - name: z.optional(z.string()), - starter: z.nullish(RepositoryStarterSchema), - framework: z.optional(z.string()), - quotas: z.optional( - z.object({ - sliceMachineEnabled: z.boolean(), - }), - ), -}); - -export type Repository = z.infer; - -export function getRepository(config: RepositoryConfig): Promise { - const url = getRepositoryServiceUrl(config.host); - return repositoryServiceRequest(url, config, { schema: RepositorySchema }); -} - -export async function enableTypeBuilder( - framework: string, - config: RepositoryConfig, -): Promise { - const url = new URL("type-builder", getRepositoryServiceUrl(config.host)); - await repositoryServiceRequest(url, config, { method: "PATCH", json: { framework } }); -} - -const OnboardingStateSchema = z.object({ - completedSteps: z.array(z.string()), -}); -type OnboardingState = z.infer; - -export async function getOnboardingState(config: RepositoryConfig): Promise { - const url = new URL("onboarding", getRepositoryServiceUrl(config.host)); - return onboardingServiceRequest(url, config, { - schema: OnboardingStateSchema, - }); -} - -export async function toggleOnboardingStep( - stepId: string, - config: RepositoryConfig, -): Promise { - const url = new URL( - `onboarding/${encodeURIComponent(stepId)}/toggle`, - getRepositoryServiceUrl(config.host), - ); - return onboardingServiceRequest(url, config, { - method: "PATCH", - schema: OnboardingStateSchema, - }); -} - -function repositoryServiceRequest( - url: URL, - config: RepositoryConfig, - options: RequestOptions = {}, -): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return request(scopedUrl, { - headers: { - Authorization: `Bearer ${config.token}`, - repository: config.repo, - }, - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); -} - -function onboardingServiceRequest( - url: URL, - config: RepositoryConfig, - options: RequestOptions = {}, -): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return request(scopedUrl, { - credentials: { "prismic-auth": config.token }, - ...options, - }); -} - -function getRepositoryServiceUrl(host: string): URL { - return new URL(`https://api.internal.${host}/repository/`); -} diff --git a/src/lib/prismic/clients/user.ts b/src/lib/prismic/clients/user.ts deleted file mode 100644 index af4c2252..00000000 --- a/src/lib/prismic/clients/user.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as z from "zod/mini"; - -import { request } from "../../request"; - -const ProfileSchema = z.object({ - email: z.string(), - shortId: z.string(), - intercomHash: z.string(), -}); -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; -}): Promise { - const url = new URL("user/profile", `https://api.internal.${config.host}/`); - return request(url, { - headers: { Authorization: `Bearer ${config.token}` }, - schema: ProfileSchema, - 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/src/lib/prismic/clients/wroom.ts b/src/lib/prismic/clients/wroom.ts deleted file mode 100644 index e088a414..00000000 --- a/src/lib/prismic/clients/wroom.ts +++ /dev/null @@ -1,326 +0,0 @@ -import * as z from "zod/mini"; - -import { request, type RequestOptions } from "../../request"; - -type WroomConfig = { - repo: string; - token: string | undefined; - host: string; -}; - -const WebhookTriggersSchema = z.object({ - documentsPublished: z.boolean(), - documentsUnpublished: z.boolean(), - releasesCreated: z.boolean(), - releasesUpdated: z.boolean(), - tagsCreated: z.boolean(), - tagsDeleted: z.boolean(), -}); - -export const WEBHOOK_TRIGGERS = Object.keys(WebhookTriggersSchema.shape); - -const WebhookSchema = z.object({ - config: z.extend(WebhookTriggersSchema, { - _id: z.string(), - url: z.string(), - active: z.boolean(), - name: z.nullable(z.string()), - secret: z.nullable(z.string()), - headers: z.record(z.string(), z.string()), - }), -}); -type Webhook = z.infer; - -export function getWebhooks(config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL("app/settings/webhooks", getWroomRepoServiceUrl(repo, host)); - return wroomRepoServiceRequest(url, config, { - schema: z.array(WebhookSchema), - unknownErrorMessage: "Failed to load webhooks", - }); -} - -export async function createWebhook( - webhookConfig: Omit, - config: WroomConfig, -): Promise { - const body = toWebhookFormData({ ...webhookConfig, active: true, headers: {} }); - const { repo, host } = config; - const url = new URL("app/settings/webhooks/create", getWroomRepoServiceUrl(repo, host)); - await wroomRepoServiceRequest(url, config, { - method: "POST", - body, - unknownErrorMessage: "Failed to create webhook", - }); -} - -export async function updateWebhook( - id: string, - webhookConfig: Omit, - config: WroomConfig, -): Promise { - const body = toWebhookFormData(webhookConfig); - const { repo, host } = config; - const url = new URL( - `app/settings/webhooks/${encodeURIComponent(id)}`, - getWroomRepoServiceUrl(repo, host), - ); - await wroomRepoServiceRequest(url, config, { - method: "POST", - body, - notFoundMessage: `Webhook not found: ${id}`, - unknownErrorMessage: "Failed to update webhook", - }); -} - -export async function deleteWebhook(id: string, config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL( - `app/settings/webhooks/${encodeURIComponent(id)}/delete`, - getWroomRepoServiceUrl(repo, host), - ); - await wroomRepoServiceRequest(url, config, { - method: "POST", - notFoundMessage: `Webhook not found: ${id}`, - unknownErrorMessage: "Failed to delete webhook", - }); -} - -function toWebhookFormData(webhookConfig: Omit): FormData { - const body = new FormData(); - body.set("url", webhookConfig.url); - body.set("name", webhookConfig.name ?? ""); - body.set("secret", webhookConfig.secret ?? ""); - body.set("headers", JSON.stringify(webhookConfig.headers ?? {})); - body.set("active", webhookConfig.active ? "on" : "off"); - body.set("documentsPublished", webhookConfig.documentsPublished.toString()); - body.set("documentsUnpublished", webhookConfig.documentsUnpublished.toString()); - body.set("releasesCreated", webhookConfig.releasesCreated.toString()); - body.set("releasesUpdated", webhookConfig.releasesUpdated.toString()); - body.set("tagsCreated", webhookConfig.tagsCreated.toString()); - body.set("tagsDeleted", webhookConfig.tagsDeleted.toString()); - return body; -} - -const AccessTokenSchema = z.object({ - id: z.string(), - scope: z.string(), - token: z.string(), - created_at: z.object({ $date: z.number() }), -}); -type AccessToken = z.infer; - -const OAuthAppSchema = z.object({ - id: z.string(), - name: z.string(), - wroom_auths: z.array(AccessTokenSchema), -}); -type OAuthApp = z.infer; - -const WriteTokenSchema = z.object({ - app_name: z.string(), - token: z.string(), - timestamp: z.number(), -}); -type WriteToken = z.infer; - -const WriteTokensInfoSchema = z.object({ - max_tokens: z.number(), - tokens: z.array(WriteTokenSchema), -}); -type WriteTokensInfo = z.infer; - -export function getOAuthApps(config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL("settings/security/contentapi", getWroomRepoServiceUrl(repo, host)); - return wroomRepoServiceRequest(url, config, { - schema: z.array(OAuthAppSchema), - unknownErrorMessage: "Failed to load OAuth apps", - }); -} - -export async function createOAuthApp(name: string, config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL("settings/security/oauthapp", getWroomRepoServiceUrl(repo, host)); - return wroomRepoServiceRequest(url, config, { - method: "POST", - json: { app_name: name }, - schema: OAuthAppSchema, - unknownErrorMessage: "Failed to create OAuth app", - }); -} - -export async function createOAuthAuthorization( - appId: string, - scope: string, - config: WroomConfig, -): Promise { - const { repo, host } = config; - const url = new URL("settings/security/authorizations", getWroomRepoServiceUrl(repo, host)); - return wroomRepoServiceRequest(url, config, { - method: "POST", - json: { app: appId, scope }, - schema: AccessTokenSchema, - unknownErrorMessage: "Failed to create token", - }); -} - -export async function deleteOAuthAuthorization(authId: string, config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL( - `settings/security/authorizations/${encodeURIComponent(authId)}`, - getWroomRepoServiceUrl(repo, host), - ); - await wroomRepoServiceRequest(url, config, { - method: "DELETE", - notFoundMessage: `Token not found: ${authId}`, - unknownErrorMessage: "Failed to delete token", - }); -} - -export function getWriteTokens(config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL("settings/security/customtypesapi", getWroomRepoServiceUrl(repo, host)); - return wroomRepoServiceRequest(url, config, { - schema: WriteTokensInfoSchema, - unknownErrorMessage: "Failed to load write tokens", - }); -} - -export async function createWriteToken(name: string, config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL("settings/security/token", getWroomRepoServiceUrl(repo, host)); - return wroomRepoServiceRequest(url, config, { - method: "POST", - json: { app_name: name }, - schema: WriteTokenSchema, - unknownErrorMessage: "Failed to create write token", - }); -} - -export async function deleteWriteToken(tokenValue: string, config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL( - `settings/security/token/${encodeURIComponent(tokenValue)}`, - getWroomRepoServiceUrl(repo, host), - ); - await wroomRepoServiceRequest(url, config, { - method: "DELETE", - notFoundMessage: "Token not found", - unknownErrorMessage: "Failed to delete write token", - }); -} - -export async function checkIsDomainAvailable(config: { - domain: string; - token: string | undefined; - host: string; -}): Promise { - const { domain, host } = config; - const url = new URL( - `app/dashboard/repositories/${encodeURIComponent(domain)}/exists`, - getWroomServiceUrl(host), - ); - return wroomServiceRequest(url, config, { schema: z.boolean() }); -} - -export async function createRepository(config: { - domain: string; - name: string; - framework: string; - agent: string | undefined; - token: string | undefined; - host: string; -}): Promise { - const { domain, name, framework, agent, host } = config; - const url = new URL("app/dashboard/repositories", getWroomServiceUrl(host)); - url.searchParams.set("app", "cli"); - if (agent) url.searchParams.set("agent", agent); - await wroomServiceRequest(url, config, { - method: "POST", - json: { domain, name, framework, plan: "personal" }, - unknownErrorMessage: "Failed to create repository", - }); -} - -const SyncStateSchema = z.object({ - repository: z.object({ - api_access: z.string(), - }), -}); - -export async function getRepositoryAccess(config: WroomConfig): Promise { - const { repo, host } = config; - const url = new URL("syncState", getWroomRepoServiceUrl(repo, host)); - const response = await wroomRepoServiceRequest(url, config, { - schema: SyncStateSchema, - unknownErrorMessage: "Failed to load repository access", - }); - return response.repository.api_access; -} - -export type RepositoryAccessLevel = "private" | "public" | "open"; - -export async function setRepositoryAccess( - level: RepositoryAccessLevel, - config: WroomConfig, -): Promise { - const { repo, host } = config; - const url = new URL("settings/security/apiaccess", getWroomRepoServiceUrl(repo, host)); - await wroomRepoServiceRequest(url, config, { - method: "POST", - json: { api_access: level }, - unknownErrorMessage: "Failed to set repository access", - }); -} - -const SetNameResponseSchema = z.object({ - repository: z.object({ - name: z.string(), - }), -}); - -export async function setRepositoryName(name: string, config: WroomConfig): Promise { - const formData = new FormData(); - formData.set("displayname", name); - const { repo, host } = config; - const url = new URL("app/settings/repository", getWroomRepoServiceUrl(repo, host)); - const response = await wroomRepoServiceRequest(url, config, { - method: "POST", - body: formData, - schema: SetNameResponseSchema, - unknownErrorMessage: "Failed to set repository name", - }); - return response.repository.name; -} - -function wroomRepoServiceRequest( - url: URL, - config: WroomConfig, - options: RequestOptions = {}, -): Promise { - return request(url, { - credentials: { "prismic-auth": config.token }, - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); -} - -function wroomServiceRequest( - url: URL, - config: { token: string | undefined; host: string }, - options: RequestOptions = {}, -): Promise { - return request(url, { - credentials: { "prismic-auth": config.token }, - ...options, - }); -} - -function getWroomRepoServiceUrl(repo: string, host: string): URL { - return new URL(`https://${repo}.${host}/`); -} - -function getWroomServiceUrl(host: string): URL { - return new URL(`https://${host}/`); -} diff --git a/src/lib/prismic/environments.ts b/src/lib/prismic/environments.ts index ba526ced..82b52a3a 100644 --- a/src/lib/prismic/environments.ts +++ b/src/lib/prismic/environments.ts @@ -1,6 +1,5 @@ import { dedent } from "../string"; -import { validateToken } from "./clients/auth"; -import { type Environment, getEnvironments } from "./clients/core"; +import { type Environment, getEnvironments, validateToken } from "./client"; export async function getUserEnvironments(config: { repo: string; diff --git a/src/lib/prismic/models.ts b/src/lib/prismic/models.ts index 694903f9..bb1daefc 100644 --- a/src/lib/prismic/models.ts +++ b/src/lib/prismic/models.ts @@ -13,7 +13,7 @@ import { type CustomTypesConfig, getCustomTypes, getSlices, -} from "./clients/custom-types"; +} from "./client"; type Fields = Record; diff --git a/src/lib/prismic/onboarding.ts b/src/lib/prismic/onboarding.ts index 4abb7850..2dd30bf7 100644 --- a/src/lib/prismic/onboarding.ts +++ b/src/lib/prismic/onboarding.ts @@ -1,4 +1,4 @@ -import { getOnboardingState, toggleOnboardingStep } from "./clients/repository"; +import { getOnboardingState, toggleOnboardingStep } from "./client"; type OnboardingConfig = { repo: string; diff --git a/src/tracking.ts b/src/tracking.ts index 90f9183d..fbfa08ce 100644 --- a/src/tracking.ts +++ b/src/tracking.ts @@ -8,7 +8,7 @@ import { DEFAULT_PRISMIC_HOST, env } from "./env"; import { detectAgent } from "./lib/ai"; import { readJsonFile, writeFileRecursive } from "./lib/file"; import { stringify } from "./lib/json"; -import type { Profile } from "./lib/prismic/clients/user"; +import type { Profile } from "./lib/prismic/client"; import { initSegment, trackEvent, trackIdentity } from "./lib/segment"; import { appendTrailingSlash } from "./lib/url"; From ae5fe65d8c3358fca84de3c536bb4600fc153d66 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:21:52 +0000 Subject: [PATCH 2/8] refactor: inline single-use response schemas Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- src/lib/prismic/client.ts | 58 +++++++++------------------------------ 1 file changed, 13 insertions(+), 45 deletions(-) diff --git a/src/lib/prismic/client.ts b/src/lib/prismic/client.ts index 54f74411..2f323909 100644 --- a/src/lib/prismic/client.ts +++ b/src/lib/prismic/client.ts @@ -57,10 +57,6 @@ const UserRepositorySchema = z.object({ role: z.optional(z.union([z.string(), z.record(z.string(), z.string())])), }); -const UserRepositoryListSchema = z.object({ - repositories: z.array(UserRepositorySchema), -}); - export async function getProfile(config: Config): Promise { const url = new URL("user/profile", getUserServiceUrl(config.host)); return bearerRequest(url, config, { @@ -74,7 +70,7 @@ export async function getRepositories( ): Promise[]> { const url = new URL("user/repositories", getUserServiceUrl(config.host)); const response = await bearerRequest(url, config, { - schema: UserRepositoryListSchema, + schema: z.object({ repositories: z.array(UserRepositorySchema) }), unknownErrorMessage: "Failed to load your repositories", }); return response.repositories; @@ -86,22 +82,18 @@ function getUserServiceUrl(host: string): URL { // Repository service -const RepositoryStarterSchema = z.object({ - id: z.string(), - revision: z.string(), - framework: z.string(), - deploymentUrl: z.url(), -}); - const RepositorySchema = z.object({ name: z.optional(z.string()), - starter: z.nullish(RepositoryStarterSchema), - framework: z.optional(z.string()), - quotas: z.optional( + starter: z.nullish( z.object({ - sliceMachineEnabled: z.boolean(), + id: z.string(), + revision: z.string(), + framework: z.string(), + deploymentUrl: z.url(), }), ), + framework: z.optional(z.string()), + quotas: z.optional(z.object({ sliceMachineEnabled: z.boolean() })), }); export type Repository = z.infer; @@ -384,16 +376,12 @@ const PreviewSchema = z.object({ url: z.string(), }); -const GetPreviewsResponseSchema = z.object({ - results: z.array(PreviewSchema), -}); - export type Preview = z.infer; export async function getPreviews(config: RepositoryConfig): Promise { const url = new URL("core/repository/preview_configs", getRepositoryDomainUrl(config)); const response = await repositoryDomainRequest(url, config, { - schema: GetPreviewsResponseSchema, + schema: z.object({ results: z.array(PreviewSchema) }), unknownErrorMessage: "Failed to load previews", }); return response.results; @@ -445,14 +433,10 @@ export async function getEnvironments(config: RepositoryConfig): Promise { const url = new URL("core/repository", getRepositoryDomainUrl(config)); const response = await repositoryDomainRequest(url, config, { - schema: SimulatorUrlResponseSchema, + schema: z.object({ simulator_url: z.optional(z.string()) }), unknownErrorMessage: "Failed to load simulator URL", }); return response.simulator_url; @@ -470,10 +454,6 @@ export async function setSimulatorUrl( }); } -const DocumentSearchTotalSchema = z.object({ - total: z.number(), -}); - export async function getDocumentTotalByCustomTypes( customTypeId: string, config: RepositoryConfig, @@ -482,7 +462,7 @@ export async function getDocumentTotalByCustomTypes( const response = await repositoryDomainRequest(url, config, { method: "POST", json: { customTypes: [customTypeId], limit: 0 }, - schema: DocumentSearchTotalSchema, + schema: z.object({ total: z.number() }), }); return response.total; } @@ -711,16 +691,10 @@ export async function deleteWriteToken( }); } -const SyncStateSchema = z.object({ - repository: z.object({ - api_access: z.string(), - }), -}); - export async function getRepositoryAccess(config: RepositoryConfig): Promise { const url = new URL("syncState", getRepositoryDomainUrl(config)); const response = await repositoryDomainRequest(url, config, { - schema: SyncStateSchema, + schema: z.object({ repository: z.object({ api_access: z.string() }) }), unknownErrorMessage: "Failed to load repository access", }); return response.repository.api_access; @@ -740,12 +714,6 @@ export async function setRepositoryAccess( }); } -const SetNameResponseSchema = z.object({ - repository: z.object({ - name: z.string(), - }), -}); - export async function setRepositoryName(name: string, config: RepositoryConfig): Promise { const formData = new FormData(); formData.set("displayname", name); @@ -753,7 +721,7 @@ export async function setRepositoryName(name: string, config: RepositoryConfig): const response = await repositoryDomainRequest(url, config, { method: "POST", body: formData, - schema: SetNameResponseSchema, + schema: z.object({ repository: z.object({ name: z.string() }) }), unknownErrorMessage: "Failed to set repository name", }); return response.repository.name; From c10c6e4d7785ae5cb0e7022afe78cb066f20f3aa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:24:09 +0000 Subject: [PATCH 3/8] refactor: build service URLs inside the request helpers Each service request helper now takes a path. The helper builds the URL from the service base, so the separate base URL helpers are not needed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- src/lib/prismic/client.ts | 199 +++++++++++++------------------------- 1 file changed, 68 insertions(+), 131 deletions(-) diff --git a/src/lib/prismic/client.ts b/src/lib/prismic/client.ts index 2f323909..b873ca43 100644 --- a/src/lib/prismic/client.ts +++ b/src/lib/prismic/client.ts @@ -99,16 +99,14 @@ const RepositorySchema = z.object({ export type Repository = z.infer; export function getRepository(config: RepositoryConfig): Promise { - const url = getRepositoryServiceUrl(config.host); - return repositoryServiceRequest(url, config, { schema: RepositorySchema }); + return repositoryServiceRequest("", config, { schema: RepositorySchema }); } export async function enableTypeBuilder( framework: string, config: RepositoryConfig, ): Promise { - const url = new URL("type-builder", getRepositoryServiceUrl(config.host)); - await repositoryServiceRequest(url, config, { method: "PATCH", json: { framework } }); + await repositoryServiceRequest("type-builder", config, { method: "PATCH", json: { framework } }); } const OnboardingStateSchema = z.object({ @@ -117,8 +115,7 @@ const OnboardingStateSchema = z.object({ type OnboardingState = z.infer; export async function getOnboardingState(config: RepositoryConfig): Promise { - const url = new URL("onboarding", getRepositoryServiceUrl(config.host)); - return onboardingServiceRequest(url, config, { + return onboardingServiceRequest("onboarding", config, { schema: OnboardingStateSchema, }); } @@ -127,24 +124,20 @@ export async function toggleOnboardingStep( stepId: string, config: RepositoryConfig, ): Promise { - const url = new URL( - `onboarding/${encodeURIComponent(stepId)}/toggle`, - getRepositoryServiceUrl(config.host), - ); - return onboardingServiceRequest(url, config, { + return onboardingServiceRequest(`onboarding/${encodeURIComponent(stepId)}/toggle`, config, { method: "PATCH", schema: OnboardingStateSchema, }); } function repositoryServiceRequest( - url: URL, + path: string, config: RepositoryConfig, options: PrismicRequestOptions = {}, ): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return bearerRequest(scopedUrl, config, { + const url = new URL(path, `https://api.internal.${config.host}/repository/`); + url.searchParams.set("repository", config.repo); + return bearerRequest(url, config, { headers: { repository: config.repo }, notFoundMessage: `Repository not found: ${config.repo}`, ...options, @@ -152,17 +145,13 @@ function repositoryServiceRequest( } function onboardingServiceRequest( - url: URL, + path: string, config: RepositoryConfig, options: PrismicRequestOptions = {}, ): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return cookieRequest(scopedUrl, config, options); -} - -function getRepositoryServiceUrl(host: string): URL { - return new URL(`https://api.internal.${host}/repository/`); + const url = new URL(path, `https://api.internal.${config.host}/repository/`); + url.searchParams.set("repository", config.repo); + return cookieRequest(url, config, options); } // Custom types service @@ -170,13 +159,11 @@ function getRepositoryServiceUrl(host: string): URL { export type CustomTypesConfig = RepositoryConfig & { releaseId?: string }; export function getCustomTypes(config: CustomTypesConfig): Promise { - const url = new URL("customtypes", getCustomTypesServiceUrl(config.host)); - return customTypesServiceRequest(url, config); + return customTypesServiceRequest("customtypes", config); } export function getSlices(config: CustomTypesConfig): Promise { - const url = new URL("slices", getCustomTypesServiceUrl(config.host)); - return customTypesServiceRequest(url, config); + return customTypesServiceRequest("slices", config); } export type BulkChange = { @@ -186,8 +173,7 @@ export type BulkChange = { }; export async function bulkUpdate(changes: BulkChange[], config: CustomTypesConfig): Promise { - const url = new URL("bulk-update", getCustomTypesServiceUrl(config.host)); - await customTypesServiceRequest(url, config, { + await customTypesServiceRequest("bulk-update", config, { method: "POST", json: { changes }, unknownErrorMessage: "Failed to update models", @@ -195,10 +181,11 @@ export async function bulkUpdate(changes: BulkChange[], config: CustomTypesConfi } function customTypesServiceRequest( - url: URL, + path: string, config: CustomTypesConfig, options: PrismicRequestOptions = {}, ): Promise { + const url = new URL(path, `https://customtypes.${config.host}/`); if (config.releaseId) url.searchParams.set("release", config.releaseId); return bearerRequest(url, config, { headers: { repository: config.repo }, @@ -207,10 +194,6 @@ function customTypesServiceRequest( }); } -function getCustomTypesServiceUrl(host: string): URL { - return new URL(`https://customtypes.${host}/`); -} - // Screenshot service const ScreenshotPresignedUrlResponseSchema = z.object({ @@ -229,8 +212,7 @@ const SUPPORTED_IMAGE_MIME_TYPES: Record = { }; export async function deleteScreenshots(sliceId: string, config: RepositoryConfig): Promise { - const url = new URL("delete", getScreenshotServiceUrl(config.host)); - await screenshotServiceRequest(url, config, { + await screenshotServiceRequest("delete", config, { method: "POST", json: { sliceId }, }); @@ -240,15 +222,14 @@ export async function uploadScreenshot( blob: Blob, config: RepositoryConfig & { sliceId: string; variationId: string }, ): Promise { - const { sliceId, variationId, repo, host } = config; + const { sliceId, variationId, repo } = config; const type = blob.type; if (!(type in SUPPORTED_IMAGE_MIME_TYPES)) { throw new UnsupportedFileTypeError(type); } - const presignedUrl = new URL("presigned-url", getScreenshotServiceUrl(host)); - const presigned = await screenshotServiceRequest(presignedUrl, config, { + const presigned = await screenshotServiceRequest("presigned-url", config, { schema: ScreenshotPresignedUrlResponseSchema, }); @@ -286,22 +267,18 @@ export class UnsupportedFileTypeError extends Error { } function screenshotServiceRequest( - url: URL, + path: string, config: RepositoryConfig, options: PrismicRequestOptions = {}, ): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return bearerRequest(scopedUrl, config, { + const url = new URL(path, `https://api.internal.${config.host}/screenshot/`); + url.searchParams.set("repository", config.repo); + return bearerRequest(url, config, { headers: { repository: config.repo }, ...options, }); } -function getScreenshotServiceUrl(host: string): URL { - return new URL(`https://api.internal.${host}/screenshot/`); -} - // Locale service const LocaleSchema = z.object({ @@ -314,8 +291,7 @@ const LocaleSchema = z.object({ export type Locale = z.infer; export async function getLocales(config: RepositoryConfig): Promise { - const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); - const response = await localeServiceRequest(url, config, { + const response = await localeServiceRequest("repository/locales", config, { schema: z.object({ results: z.array(LocaleSchema) }), unknownErrorMessage: "Failed to load locales", }); @@ -326,8 +302,7 @@ export async function upsertLocale( locale: { id: string; isMaster?: boolean; customName?: string }, config: RepositoryConfig, ): Promise { - const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); - return localeServiceRequest(url, config, { + return localeServiceRequest("repository/locales", config, { method: "POST", json: { id: locale.id, @@ -340,11 +315,7 @@ export async function upsertLocale( } export async function removeLocale(code: string, config: RepositoryConfig): Promise { - const url = new URL( - `repository/locales/${encodeURIComponent(code)}`, - getLocaleServiceUrl(config.host), - ); - await localeServiceRequest(url, config, { + await localeServiceRequest(`repository/locales/${encodeURIComponent(code)}`, config, { method: "DELETE", notFoundMessage: `Locale not found: ${code}`, unknownErrorMessage: "Failed to remove locale", @@ -352,22 +323,18 @@ export async function removeLocale(code: string, config: RepositoryConfig): Prom } function localeServiceRequest( - url: URL, + path: string, config: RepositoryConfig, options: PrismicRequestOptions = {}, ): Promise { - const scopedUrl = new URL(url); - scopedUrl.searchParams.set("repository", config.repo); - return bearerRequest(scopedUrl, config, { + const url = new URL(path, `https://api.internal.${config.host}/locale/`); + url.searchParams.set("repository", config.repo); + return bearerRequest(url, config, { notFoundMessage: `Repository not found: ${config.repo}`, ...options, }); } -function getLocaleServiceUrl(host: string): URL { - return new URL(`https://api.internal.${host}/locale/`); -} - // Repository domain (Core and Wroom) const PreviewSchema = z.object({ @@ -379,8 +346,7 @@ const PreviewSchema = z.object({ export type Preview = z.infer; export async function getPreviews(config: RepositoryConfig): Promise { - const url = new URL("core/repository/preview_configs", getRepositoryDomainUrl(config)); - const response = await repositoryDomainRequest(url, config, { + const response = await repositoryDomainRequest("core/repository/preview_configs", config, { schema: z.object({ results: z.array(PreviewSchema) }), unknownErrorMessage: "Failed to load previews", }); @@ -395,8 +361,7 @@ export async function addPreview( }, config: RepositoryConfig, ): Promise { - const url = new URL("previews/new", getRepositoryDomainUrl(config)); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest("previews/new", config, { method: "POST", json: { name: previewConfig.name, @@ -408,8 +373,7 @@ export async function addPreview( } export async function removePreview(id: string, config: RepositoryConfig): Promise { - const url = new URL(`previews/delete/${encodeURIComponent(id)}`, getRepositoryDomainUrl(config)); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest(`previews/delete/${encodeURIComponent(id)}`, config, { method: "POST", json: {}, notFoundMessage: `Preview not found: ${id}`, @@ -426,16 +390,14 @@ const EnvironmentSchema = z.object({ export type Environment = z.infer; export async function getEnvironments(config: RepositoryConfig): Promise { - const url = new URL("core/environments", getRepositoryDomainUrl(config)); - const response = await repositoryDomainRequest(url, config, { + const response = await repositoryDomainRequest("core/environments", config, { schema: z.object({ results: z.array(EnvironmentSchema) }), }); return response.results; } export async function getSimulatorUrl(config: RepositoryConfig): Promise { - const url = new URL("core/repository", getRepositoryDomainUrl(config)); - const response = await repositoryDomainRequest(url, config, { + const response = await repositoryDomainRequest("core/repository", config, { schema: z.object({ simulator_url: z.optional(z.string()) }), unknownErrorMessage: "Failed to load simulator URL", }); @@ -446,8 +408,7 @@ export async function setSimulatorUrl( simulatorUrl: string, config: RepositoryConfig, ): Promise { - const url = new URL("core/repository", getRepositoryDomainUrl(config)); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest("core/repository", config, { method: "PATCH", json: { simulator_url: simulatorUrl }, unknownErrorMessage: "Failed to set simulator URL", @@ -458,8 +419,7 @@ export async function getDocumentTotalByCustomTypes( customTypeId: string, config: RepositoryConfig, ): Promise { - const url = new URL("core/documents/search", getRepositoryDomainUrl(config)); - const response = await repositoryDomainRequest(url, config, { + const response = await repositoryDomainRequest("core/documents/search", config, { method: "POST", json: { customTypes: [customTypeId], limit: 0 }, schema: z.object({ total: z.number() }), @@ -471,8 +431,7 @@ export async function createRelease( release: { label: string; hidden?: boolean }, config: RepositoryConfig, ): Promise { - const url = new URL("core/releases", getRepositoryDomainUrl(config)); - const response = await repositoryDomainRequest(url, config, { + const response = await repositoryDomainRequest("core/releases", config, { method: "POST", json: release, schema: z.looseObject({ id: z.string() }), @@ -482,8 +441,7 @@ export async function createRelease( } export async function deleteRelease(id: string, config: RepositoryConfig): Promise { - const url = new URL(`core/releases/${encodeURIComponent(id)}`, getRepositoryDomainUrl(config)); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest(`core/releases/${encodeURIComponent(id)}`, config, { method: "DELETE", notFoundMessage: `Release not found: ${id}`, unknownErrorMessage: "Failed to delete the release", @@ -514,8 +472,7 @@ const WebhookSchema = z.object({ type Webhook = z.infer; export function getWebhooks(config: RepositoryConfig): Promise { - const url = new URL("app/settings/webhooks", getRepositoryDomainUrl(config)); - return repositoryDomainRequest(url, config, { + return repositoryDomainRequest("app/settings/webhooks", config, { schema: z.array(WebhookSchema), unknownErrorMessage: "Failed to load webhooks", }); @@ -526,8 +483,7 @@ export async function createWebhook( config: RepositoryConfig, ): Promise { const body = toWebhookFormData({ ...webhookConfig, active: true, headers: {} }); - const url = new URL("app/settings/webhooks/create", getRepositoryDomainUrl(config)); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest("app/settings/webhooks/create", config, { method: "POST", body, unknownErrorMessage: "Failed to create webhook", @@ -540,11 +496,7 @@ export async function updateWebhook( config: RepositoryConfig, ): Promise { const body = toWebhookFormData(webhookConfig); - const url = new URL( - `app/settings/webhooks/${encodeURIComponent(id)}`, - getRepositoryDomainUrl(config), - ); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest(`app/settings/webhooks/${encodeURIComponent(id)}`, config, { method: "POST", body, notFoundMessage: `Webhook not found: ${id}`, @@ -553,11 +505,7 @@ export async function updateWebhook( } export async function deleteWebhook(id: string, config: RepositoryConfig): Promise { - const url = new URL( - `app/settings/webhooks/${encodeURIComponent(id)}/delete`, - getRepositoryDomainUrl(config), - ); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest(`app/settings/webhooks/${encodeURIComponent(id)}/delete`, config, { method: "POST", notFoundMessage: `Webhook not found: ${id}`, unknownErrorMessage: "Failed to delete webhook", @@ -609,16 +557,14 @@ const WriteTokensInfoSchema = z.object({ type WriteTokensInfo = z.infer; export function getOAuthApps(config: RepositoryConfig): Promise { - const url = new URL("settings/security/contentapi", getRepositoryDomainUrl(config)); - return repositoryDomainRequest(url, config, { + return repositoryDomainRequest("settings/security/contentapi", config, { schema: z.array(OAuthAppSchema), unknownErrorMessage: "Failed to load OAuth apps", }); } export async function createOAuthApp(name: string, config: RepositoryConfig): Promise { - const url = new URL("settings/security/oauthapp", getRepositoryDomainUrl(config)); - return repositoryDomainRequest(url, config, { + return repositoryDomainRequest("settings/security/oauthapp", config, { method: "POST", json: { app_name: name }, schema: OAuthAppSchema, @@ -631,8 +577,7 @@ export async function createOAuthAuthorization( scope: string, config: RepositoryConfig, ): Promise { - const url = new URL("settings/security/authorizations", getRepositoryDomainUrl(config)); - return repositoryDomainRequest(url, config, { + return repositoryDomainRequest("settings/security/authorizations", config, { method: "POST", json: { app: appId, scope }, schema: AccessTokenSchema, @@ -644,20 +589,19 @@ export async function deleteOAuthAuthorization( authId: string, config: RepositoryConfig, ): Promise { - const url = new URL( + await repositoryDomainRequest( `settings/security/authorizations/${encodeURIComponent(authId)}`, - getRepositoryDomainUrl(config), + config, + { + method: "DELETE", + notFoundMessage: `Token not found: ${authId}`, + unknownErrorMessage: "Failed to delete token", + }, ); - await repositoryDomainRequest(url, config, { - method: "DELETE", - notFoundMessage: `Token not found: ${authId}`, - unknownErrorMessage: "Failed to delete token", - }); } export function getWriteTokens(config: RepositoryConfig): Promise { - const url = new URL("settings/security/customtypesapi", getRepositoryDomainUrl(config)); - return repositoryDomainRequest(url, config, { + return repositoryDomainRequest("settings/security/customtypesapi", config, { schema: WriteTokensInfoSchema, unknownErrorMessage: "Failed to load write tokens", }); @@ -667,8 +611,7 @@ export async function createWriteToken( name: string, config: RepositoryConfig, ): Promise { - const url = new URL("settings/security/token", getRepositoryDomainUrl(config)); - return repositoryDomainRequest(url, config, { + return repositoryDomainRequest("settings/security/token", config, { method: "POST", json: { app_name: name }, schema: WriteTokenSchema, @@ -680,20 +623,19 @@ export async function deleteWriteToken( tokenValue: string, config: RepositoryConfig, ): Promise { - const url = new URL( + await repositoryDomainRequest( `settings/security/token/${encodeURIComponent(tokenValue)}`, - getRepositoryDomainUrl(config), + config, + { + method: "DELETE", + notFoundMessage: "Token not found", + unknownErrorMessage: "Failed to delete write token", + }, ); - await repositoryDomainRequest(url, config, { - method: "DELETE", - notFoundMessage: "Token not found", - unknownErrorMessage: "Failed to delete write token", - }); } export async function getRepositoryAccess(config: RepositoryConfig): Promise { - const url = new URL("syncState", getRepositoryDomainUrl(config)); - const response = await repositoryDomainRequest(url, config, { + const response = await repositoryDomainRequest("syncState", config, { schema: z.object({ repository: z.object({ api_access: z.string() }) }), unknownErrorMessage: "Failed to load repository access", }); @@ -706,8 +648,7 @@ export async function setRepositoryAccess( level: RepositoryAccessLevel, config: RepositoryConfig, ): Promise { - const url = new URL("settings/security/apiaccess", getRepositoryDomainUrl(config)); - await repositoryDomainRequest(url, config, { + await repositoryDomainRequest("settings/security/apiaccess", config, { method: "POST", json: { api_access: level }, unknownErrorMessage: "Failed to set repository access", @@ -717,8 +658,7 @@ export async function setRepositoryAccess( export async function setRepositoryName(name: string, config: RepositoryConfig): Promise { const formData = new FormData(); formData.set("displayname", name); - const url = new URL("app/settings/repository", getRepositoryDomainUrl(config)); - const response = await repositoryDomainRequest(url, config, { + const response = await repositoryDomainRequest("app/settings/repository", config, { method: "POST", body: formData, schema: z.object({ repository: z.object({ name: z.string() }) }), @@ -728,20 +668,17 @@ export async function setRepositoryName(name: string, config: RepositoryConfig): } function repositoryDomainRequest( - url: URL, + path: string, config: RepositoryConfig, options: PrismicRequestOptions = {}, ): Promise { + const url = new URL(path, `https://${config.repo}.${config.host}/`); return cookieRequest(url, config, { notFoundMessage: `Repository not found: ${config.repo}`, ...options, }); } -function getRepositoryDomainUrl(config: RepositoryConfig): URL { - return new URL(`https://${config.repo}.${config.host}/`); -} - // Dashboard (Wroom) export async function checkIsDomainAvailable( From e1ebcb6bf4ea68d302c38f4ca6c0449625203373 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:37:53 +0000 Subject: [PATCH 4/8] refactor: call request directly in each client function Each client function now builds its own request: the URL, the token, and the error messages. A reader can see the full request in one function, and a new endpoint starts from a copy of a similar one. Small helpers still give the base URL of each service. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- src/lib/prismic/client.ts | 359 +++++++++++++++++++++++--------------- 1 file changed, 218 insertions(+), 141 deletions(-) diff --git a/src/lib/prismic/client.ts b/src/lib/prismic/client.ts index b873ca43..cd088e71 100644 --- a/src/lib/prismic/client.ts +++ b/src/lib/prismic/client.ts @@ -3,7 +3,7 @@ import { createHash } from "node:crypto"; import type { DynamicCustomTypeModel, SharedSliceModel } from "@prismicio/types-internal"; import * as z from "zod/mini"; -import { request, type RequestOptions } from "../request"; +import { request } from "../request"; import { appendTrailingSlash } from "../url"; type Config = { @@ -59,7 +59,8 @@ const UserRepositorySchema = z.object({ export async function getProfile(config: Config): Promise { const url = new URL("user/profile", getUserServiceUrl(config.host)); - return bearerRequest(url, config, { + return request(url, { + headers: { Authorization: `Bearer ${config.token}` }, schema: ProfileSchema, unknownErrorMessage: "Failed to load your profile", }); @@ -69,7 +70,8 @@ export async function getRepositories( config: Config, ): Promise[]> { const url = new URL("user/repositories", getUserServiceUrl(config.host)); - const response = await bearerRequest(url, config, { + const response = await request(url, { + headers: { Authorization: `Bearer ${config.token}` }, schema: z.object({ repositories: z.array(UserRepositorySchema) }), unknownErrorMessage: "Failed to load your repositories", }); @@ -99,14 +101,27 @@ const RepositorySchema = z.object({ export type Repository = z.infer; export function getRepository(config: RepositoryConfig): Promise { - return repositoryServiceRequest("", config, { schema: RepositorySchema }); + const url = new URL("", getRepositoryServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + return request(url, { + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, + schema: RepositorySchema, + notFoundMessage: `Repository not found: ${config.repo}`, + }); } export async function enableTypeBuilder( framework: string, config: RepositoryConfig, ): Promise { - await repositoryServiceRequest("type-builder", config, { method: "PATCH", json: { framework } }); + const url = new URL("type-builder", getRepositoryServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + await request(url, { + method: "PATCH", + json: { framework }, + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, + notFoundMessage: `Repository not found: ${config.repo}`, + }); } const OnboardingStateSchema = z.object({ @@ -115,7 +130,10 @@ const OnboardingStateSchema = z.object({ type OnboardingState = z.infer; export async function getOnboardingState(config: RepositoryConfig): Promise { - return onboardingServiceRequest("onboarding", config, { + const url = new URL("onboarding", getRepositoryServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + return request(url, { + credentials: { "prismic-auth": config.token }, schema: OnboardingStateSchema, }); } @@ -124,34 +142,20 @@ export async function toggleOnboardingStep( stepId: string, config: RepositoryConfig, ): Promise { - return onboardingServiceRequest(`onboarding/${encodeURIComponent(stepId)}/toggle`, config, { + const url = new URL( + `onboarding/${encodeURIComponent(stepId)}/toggle`, + getRepositoryServiceUrl(config.host), + ); + url.searchParams.set("repository", config.repo); + return request(url, { method: "PATCH", + credentials: { "prismic-auth": config.token }, schema: OnboardingStateSchema, }); } -function repositoryServiceRequest( - path: string, - config: RepositoryConfig, - options: PrismicRequestOptions = {}, -): Promise { - const url = new URL(path, `https://api.internal.${config.host}/repository/`); - url.searchParams.set("repository", config.repo); - return bearerRequest(url, config, { - headers: { repository: config.repo }, - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); -} - -function onboardingServiceRequest( - path: string, - config: RepositoryConfig, - options: PrismicRequestOptions = {}, -): Promise { - const url = new URL(path, `https://api.internal.${config.host}/repository/`); - url.searchParams.set("repository", config.repo); - return cookieRequest(url, config, options); +function getRepositoryServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/repository/`); } // Custom types service @@ -159,11 +163,21 @@ function onboardingServiceRequest( export type CustomTypesConfig = RepositoryConfig & { releaseId?: string }; export function getCustomTypes(config: CustomTypesConfig): Promise { - return customTypesServiceRequest("customtypes", config); + const url = new URL("customtypes", getCustomTypesServiceUrl(config.host)); + if (config.releaseId) url.searchParams.set("release", config.releaseId); + return request(url, { + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, + notFoundMessage: `Repository not found: ${config.repo}`, + }); } export function getSlices(config: CustomTypesConfig): Promise { - return customTypesServiceRequest("slices", config); + const url = new URL("slices", getCustomTypesServiceUrl(config.host)); + if (config.releaseId) url.searchParams.set("release", config.releaseId); + return request(url, { + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, + notFoundMessage: `Repository not found: ${config.repo}`, + }); } export type BulkChange = { @@ -173,25 +187,19 @@ export type BulkChange = { }; export async function bulkUpdate(changes: BulkChange[], config: CustomTypesConfig): Promise { - await customTypesServiceRequest("bulk-update", config, { + const url = new URL("bulk-update", getCustomTypesServiceUrl(config.host)); + if (config.releaseId) url.searchParams.set("release", config.releaseId); + await request(url, { method: "POST", json: { changes }, + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to update models", }); } -function customTypesServiceRequest( - path: string, - config: CustomTypesConfig, - options: PrismicRequestOptions = {}, -): Promise { - const url = new URL(path, `https://customtypes.${config.host}/`); - if (config.releaseId) url.searchParams.set("release", config.releaseId); - return bearerRequest(url, config, { - headers: { repository: config.repo }, - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); +function getCustomTypesServiceUrl(host: string): URL { + return new URL(`https://customtypes.${host}/`); } // Screenshot service @@ -212,9 +220,12 @@ const SUPPORTED_IMAGE_MIME_TYPES: Record = { }; export async function deleteScreenshots(sliceId: string, config: RepositoryConfig): Promise { - await screenshotServiceRequest("delete", config, { + const url = new URL("delete", getScreenshotServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + await request(url, { method: "POST", json: { sliceId }, + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, }); } @@ -229,7 +240,10 @@ export async function uploadScreenshot( throw new UnsupportedFileTypeError(type); } - const presigned = await screenshotServiceRequest("presigned-url", config, { + const presignedUrl = new URL("presigned-url", getScreenshotServiceUrl(config.host)); + presignedUrl.searchParams.set("repository", config.repo); + const presigned = await request(presignedUrl, { + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, schema: ScreenshotPresignedUrlResponseSchema, }); @@ -266,17 +280,8 @@ export class UnsupportedFileTypeError extends Error { } } -function screenshotServiceRequest( - path: string, - config: RepositoryConfig, - options: PrismicRequestOptions = {}, -): Promise { - const url = new URL(path, `https://api.internal.${config.host}/screenshot/`); - url.searchParams.set("repository", config.repo); - return bearerRequest(url, config, { - headers: { repository: config.repo }, - ...options, - }); +function getScreenshotServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/screenshot/`); } // Locale service @@ -291,8 +296,12 @@ const LocaleSchema = z.object({ export type Locale = z.infer; export async function getLocales(config: RepositoryConfig): Promise { - const response = await localeServiceRequest("repository/locales", config, { + const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + const response = await request(url, { + headers: { Authorization: `Bearer ${config.token}` }, schema: z.object({ results: z.array(LocaleSchema) }), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load locales", }); return response.results; @@ -302,37 +311,38 @@ export async function upsertLocale( locale: { id: string; isMaster?: boolean; customName?: string }, config: RepositoryConfig, ): Promise { - return localeServiceRequest("repository/locales", config, { + const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + return request(url, { method: "POST", json: { id: locale.id, isMaster: locale.isMaster ?? false, ...(locale.customName ? { customName: locale.customName } : {}), }, + headers: { Authorization: `Bearer ${config.token}` }, schema: LocaleSchema, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to save locale", }); } export async function removeLocale(code: string, config: RepositoryConfig): Promise { - await localeServiceRequest(`repository/locales/${encodeURIComponent(code)}`, config, { + const url = new URL( + `repository/locales/${encodeURIComponent(code)}`, + getLocaleServiceUrl(config.host), + ); + url.searchParams.set("repository", config.repo); + await request(url, { method: "DELETE", + headers: { Authorization: `Bearer ${config.token}` }, notFoundMessage: `Locale not found: ${code}`, unknownErrorMessage: "Failed to remove locale", }); } -function localeServiceRequest( - path: string, - config: RepositoryConfig, - options: PrismicRequestOptions = {}, -): Promise { - const url = new URL(path, `https://api.internal.${config.host}/locale/`); - url.searchParams.set("repository", config.repo); - return bearerRequest(url, config, { - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); +function getLocaleServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/locale/`); } // Repository domain (Core and Wroom) @@ -346,8 +356,14 @@ const PreviewSchema = z.object({ export type Preview = z.infer; export async function getPreviews(config: RepositoryConfig): Promise { - const response = await repositoryDomainRequest("core/repository/preview_configs", config, { + const url = new URL( + "core/repository/preview_configs", + getRepositoryDomainUrl(config.repo, config.host), + ); + const response = await request(url, { + credentials: { "prismic-auth": config.token }, schema: z.object({ results: z.array(PreviewSchema) }), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load previews", }); return response.results; @@ -361,21 +377,29 @@ export async function addPreview( }, config: RepositoryConfig, ): Promise { - await repositoryDomainRequest("previews/new", config, { + const url = new URL("previews/new", getRepositoryDomainUrl(config.repo, config.host)); + await request(url, { method: "POST", json: { name: previewConfig.name, websiteURL: previewConfig.websiteURL, resolverPath: previewConfig.resolverPath, }, + credentials: { "prismic-auth": config.token }, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to add preview", }); } export async function removePreview(id: string, config: RepositoryConfig): Promise { - await repositoryDomainRequest(`previews/delete/${encodeURIComponent(id)}`, config, { + const url = new URL( + `previews/delete/${encodeURIComponent(id)}`, + getRepositoryDomainUrl(config.repo, config.host), + ); + await request(url, { method: "POST", json: {}, + credentials: { "prismic-auth": config.token }, notFoundMessage: `Preview not found: ${id}`, unknownErrorMessage: "Failed to remove preview", }); @@ -390,15 +414,21 @@ const EnvironmentSchema = z.object({ export type Environment = z.infer; export async function getEnvironments(config: RepositoryConfig): Promise { - const response = await repositoryDomainRequest("core/environments", config, { + const url = new URL("core/environments", getRepositoryDomainUrl(config.repo, config.host)); + const response = await request(url, { + credentials: { "prismic-auth": config.token }, schema: z.object({ results: z.array(EnvironmentSchema) }), + notFoundMessage: `Repository not found: ${config.repo}`, }); return response.results; } export async function getSimulatorUrl(config: RepositoryConfig): Promise { - const response = await repositoryDomainRequest("core/repository", config, { + const url = new URL("core/repository", getRepositoryDomainUrl(config.repo, config.host)); + const response = await request(url, { + credentials: { "prismic-auth": config.token }, schema: z.object({ simulator_url: z.optional(z.string()) }), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load simulator URL", }); return response.simulator_url; @@ -408,9 +438,12 @@ export async function setSimulatorUrl( simulatorUrl: string, config: RepositoryConfig, ): Promise { - await repositoryDomainRequest("core/repository", config, { + const url = new URL("core/repository", getRepositoryDomainUrl(config.repo, config.host)); + await request(url, { method: "PATCH", json: { simulator_url: simulatorUrl }, + credentials: { "prismic-auth": config.token }, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to set simulator URL", }); } @@ -419,10 +452,13 @@ export async function getDocumentTotalByCustomTypes( customTypeId: string, config: RepositoryConfig, ): Promise { - const response = await repositoryDomainRequest("core/documents/search", config, { + const url = new URL("core/documents/search", getRepositoryDomainUrl(config.repo, config.host)); + const response = await request(url, { method: "POST", json: { customTypes: [customTypeId], limit: 0 }, + credentials: { "prismic-auth": config.token }, schema: z.object({ total: z.number() }), + notFoundMessage: `Repository not found: ${config.repo}`, }); return response.total; } @@ -431,18 +467,26 @@ export async function createRelease( release: { label: string; hidden?: boolean }, config: RepositoryConfig, ): Promise { - const response = await repositoryDomainRequest("core/releases", config, { + const url = new URL("core/releases", getRepositoryDomainUrl(config.repo, config.host)); + const response = await request(url, { method: "POST", json: release, + credentials: { "prismic-auth": config.token }, schema: z.looseObject({ id: z.string() }), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to create a release", }); return response.id; } export async function deleteRelease(id: string, config: RepositoryConfig): Promise { - await repositoryDomainRequest(`core/releases/${encodeURIComponent(id)}`, config, { + const url = new URL( + `core/releases/${encodeURIComponent(id)}`, + getRepositoryDomainUrl(config.repo, config.host), + ); + await request(url, { method: "DELETE", + credentials: { "prismic-auth": config.token }, notFoundMessage: `Release not found: ${id}`, unknownErrorMessage: "Failed to delete the release", }); @@ -472,8 +516,11 @@ const WebhookSchema = z.object({ type Webhook = z.infer; export function getWebhooks(config: RepositoryConfig): Promise { - return repositoryDomainRequest("app/settings/webhooks", config, { + const url = new URL("app/settings/webhooks", getRepositoryDomainUrl(config.repo, config.host)); + return request(url, { + credentials: { "prismic-auth": config.token }, schema: z.array(WebhookSchema), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load webhooks", }); } @@ -483,9 +530,15 @@ export async function createWebhook( config: RepositoryConfig, ): Promise { const body = toWebhookFormData({ ...webhookConfig, active: true, headers: {} }); - await repositoryDomainRequest("app/settings/webhooks/create", config, { + const url = new URL( + "app/settings/webhooks/create", + getRepositoryDomainUrl(config.repo, config.host), + ); + await request(url, { method: "POST", body, + credentials: { "prismic-auth": config.token }, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to create webhook", }); } @@ -496,17 +549,27 @@ export async function updateWebhook( config: RepositoryConfig, ): Promise { const body = toWebhookFormData(webhookConfig); - await repositoryDomainRequest(`app/settings/webhooks/${encodeURIComponent(id)}`, config, { + const url = new URL( + `app/settings/webhooks/${encodeURIComponent(id)}`, + getRepositoryDomainUrl(config.repo, config.host), + ); + await request(url, { method: "POST", body, + credentials: { "prismic-auth": config.token }, notFoundMessage: `Webhook not found: ${id}`, unknownErrorMessage: "Failed to update webhook", }); } export async function deleteWebhook(id: string, config: RepositoryConfig): Promise { - await repositoryDomainRequest(`app/settings/webhooks/${encodeURIComponent(id)}/delete`, config, { + const url = new URL( + `app/settings/webhooks/${encodeURIComponent(id)}/delete`, + getRepositoryDomainUrl(config.repo, config.host), + ); + await request(url, { method: "POST", + credentials: { "prismic-auth": config.token }, notFoundMessage: `Webhook not found: ${id}`, unknownErrorMessage: "Failed to delete webhook", }); @@ -557,17 +620,29 @@ const WriteTokensInfoSchema = z.object({ type WriteTokensInfo = z.infer; export function getOAuthApps(config: RepositoryConfig): Promise { - return repositoryDomainRequest("settings/security/contentapi", config, { + const url = new URL( + "settings/security/contentapi", + getRepositoryDomainUrl(config.repo, config.host), + ); + return request(url, { + credentials: { "prismic-auth": config.token }, schema: z.array(OAuthAppSchema), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load OAuth apps", }); } export async function createOAuthApp(name: string, config: RepositoryConfig): Promise { - return repositoryDomainRequest("settings/security/oauthapp", config, { + const url = new URL( + "settings/security/oauthapp", + getRepositoryDomainUrl(config.repo, config.host), + ); + return request(url, { method: "POST", json: { app_name: name }, + credentials: { "prismic-auth": config.token }, schema: OAuthAppSchema, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to create OAuth app", }); } @@ -577,10 +652,16 @@ export async function createOAuthAuthorization( scope: string, config: RepositoryConfig, ): Promise { - return repositoryDomainRequest("settings/security/authorizations", config, { + const url = new URL( + "settings/security/authorizations", + getRepositoryDomainUrl(config.repo, config.host), + ); + return request(url, { method: "POST", json: { app: appId, scope }, + credentials: { "prismic-auth": config.token }, schema: AccessTokenSchema, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to create token", }); } @@ -589,20 +670,27 @@ export async function deleteOAuthAuthorization( authId: string, config: RepositoryConfig, ): Promise { - await repositoryDomainRequest( + const url = new URL( `settings/security/authorizations/${encodeURIComponent(authId)}`, - config, - { - method: "DELETE", - notFoundMessage: `Token not found: ${authId}`, - unknownErrorMessage: "Failed to delete token", - }, + getRepositoryDomainUrl(config.repo, config.host), ); + await request(url, { + method: "DELETE", + credentials: { "prismic-auth": config.token }, + notFoundMessage: `Token not found: ${authId}`, + unknownErrorMessage: "Failed to delete token", + }); } export function getWriteTokens(config: RepositoryConfig): Promise { - return repositoryDomainRequest("settings/security/customtypesapi", config, { + const url = new URL( + "settings/security/customtypesapi", + getRepositoryDomainUrl(config.repo, config.host), + ); + return request(url, { + credentials: { "prismic-auth": config.token }, schema: WriteTokensInfoSchema, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load write tokens", }); } @@ -611,10 +699,13 @@ export async function createWriteToken( name: string, config: RepositoryConfig, ): Promise { - return repositoryDomainRequest("settings/security/token", config, { + const url = new URL("settings/security/token", getRepositoryDomainUrl(config.repo, config.host)); + return request(url, { method: "POST", json: { app_name: name }, + credentials: { "prismic-auth": config.token }, schema: WriteTokenSchema, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to create write token", }); } @@ -623,20 +714,24 @@ export async function deleteWriteToken( tokenValue: string, config: RepositoryConfig, ): Promise { - await repositoryDomainRequest( + const url = new URL( `settings/security/token/${encodeURIComponent(tokenValue)}`, - config, - { - method: "DELETE", - notFoundMessage: "Token not found", - unknownErrorMessage: "Failed to delete write token", - }, + getRepositoryDomainUrl(config.repo, config.host), ); + await request(url, { + method: "DELETE", + credentials: { "prismic-auth": config.token }, + notFoundMessage: "Token not found", + unknownErrorMessage: "Failed to delete write token", + }); } export async function getRepositoryAccess(config: RepositoryConfig): Promise { - const response = await repositoryDomainRequest("syncState", config, { + const url = new URL("syncState", getRepositoryDomainUrl(config.repo, config.host)); + const response = await request(url, { + credentials: { "prismic-auth": config.token }, schema: z.object({ repository: z.object({ api_access: z.string() }) }), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load repository access", }); return response.repository.api_access; @@ -648,9 +743,15 @@ export async function setRepositoryAccess( level: RepositoryAccessLevel, config: RepositoryConfig, ): Promise { - await repositoryDomainRequest("settings/security/apiaccess", config, { + const url = new URL( + "settings/security/apiaccess", + getRepositoryDomainUrl(config.repo, config.host), + ); + await request(url, { method: "POST", json: { api_access: level }, + credentials: { "prismic-auth": config.token }, + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to set repository access", }); } @@ -658,25 +759,20 @@ export async function setRepositoryAccess( export async function setRepositoryName(name: string, config: RepositoryConfig): Promise { const formData = new FormData(); formData.set("displayname", name); - const response = await repositoryDomainRequest("app/settings/repository", config, { + const url = new URL("app/settings/repository", getRepositoryDomainUrl(config.repo, config.host)); + const response = await request(url, { method: "POST", body: formData, + credentials: { "prismic-auth": config.token }, schema: z.object({ repository: z.object({ name: z.string() }) }), + notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to set repository name", }); return response.repository.name; } -function repositoryDomainRequest( - path: string, - config: RepositoryConfig, - options: PrismicRequestOptions = {}, -): Promise { - const url = new URL(path, `https://${config.repo}.${config.host}/`); - return cookieRequest(url, config, { - notFoundMessage: `Repository not found: ${config.repo}`, - ...options, - }); +function getRepositoryDomainUrl(repo: string, host: string): URL { + return new URL(`https://${repo}.${host}/`); } // Dashboard (Wroom) @@ -688,7 +784,10 @@ export async function checkIsDomainAvailable( `app/dashboard/repositories/${encodeURIComponent(config.domain)}/exists`, getDashboardUrl(config.host), ); - return cookieRequest(url, config, { schema: z.boolean() }); + return request(url, { + credentials: { "prismic-auth": config.token }, + schema: z.boolean(), + }); } export async function createRepository( @@ -703,9 +802,10 @@ export async function createRepository( const url = new URL("app/dashboard/repositories", getDashboardUrl(host)); url.searchParams.set("app", "cli"); if (agent) url.searchParams.set("agent", agent); - await cookieRequest(url, config, { + await request(url, { method: "POST", json: { domain, name, framework, plan: "personal" }, + credentials: { "prismic-auth": config.token }, unknownErrorMessage: "Failed to create repository", }); } @@ -772,26 +872,3 @@ export async function getDocsPageContent(path: string, config: DocsConfig): Prom function getDocsUrl(host = DEFAULT_DOCS_HOST): URL { return new URL(`https://${host}/docs/`); } - -// Authentication - -type PrismicRequestOptions = RequestOptions & { headers?: Record }; - -function bearerRequest( - url: URL, - config: Config, - options: PrismicRequestOptions = {}, -): Promise { - return request(url, { - ...options, - headers: { ...options.headers, Authorization: `Bearer ${config.token}` }, - }); -} - -function cookieRequest( - url: URL, - config: Config, - options: PrismicRequestOptions = {}, -): Promise { - return request(url, { ...options, credentials: { "prismic-auth": config.token } }); -} From 83b3634d22b49459eaf614ea792d6bfd8157dd72 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:43:35 +0000 Subject: [PATCH 5/8] refactor: return API responses from client functions as they are Client functions now return the response that the API sends. The callers take the values that they need from the response. A client function then only describes one request. The screenshot upload gets its upload form from its own client function. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- src/adapters/index.ts | 4 +- src/commands/dev.ts | 2 +- src/commands/init.ts | 2 +- src/commands/locale-list.ts | 2 +- src/commands/locale-set-master.ts | 2 +- src/commands/preview-list.ts | 2 +- src/commands/preview-remove.ts | 2 +- src/commands/push.ts | 2 +- src/commands/repo-list.ts | 2 +- src/commands/repo-set-name.ts | 4 +- src/commands/repo-view.ts | 6 +-- src/lib/prismic/client.ts | 72 +++++++++++++++++-------------- src/lib/prismic/environments.ts | 2 +- 13 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index a0033319..4d5bd40f 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -167,10 +167,10 @@ export abstract class Adapter { if (setup) await this.setupProject(); const config = { repo: await getRepositoryName(), ...(await getCredentials()) }; - if (!(await getSimulatorUrl(config))) { + if (!(await getSimulatorUrl(config)).simulator_url) { await setSimulatorUrl(this.localSimulatorUrl, config); } - if ((await getPreviews(config)).length === 0) { + if ((await getPreviews(config)).results.length === 0) { await addPreview(this.localPreviewConfig, config); } } diff --git a/src/commands/dev.ts b/src/commands/dev.ts index c5a05fb8..803ba98f 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -174,7 +174,7 @@ async function saveSession(sessionPath: URL, session: Session): Promise { } async function createSession({ repo, token, host }: CustomTypesConfig): Promise { - const releaseId = await createRelease( + const { id: releaseId } = await createRelease( { label: "prismic dev", hidden: true }, { repo, token, host }, ).catch((error) => { diff --git a/src/commands/init.ts b/src/commands/init.ts index ba702796..680c6275 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -353,7 +353,7 @@ async function completeStarterHandoff( config: { repo: string; token: string | undefined; host: string }, ): Promise { try { - const previews = await getPreviews(config); + const { results: previews } = await getPreviews(config); await Promise.all( previews .filter((preview) => preview.label === "Starter Preview") diff --git a/src/commands/locale-list.ts b/src/commands/locale-list.ts index 6e02b584..b801bb5e 100644 --- a/src/commands/locale-list.ts +++ b/src/commands/locale-list.ts @@ -30,7 +30,7 @@ export default createCommand(config, async ({ values }) => { const { token, host } = await getCredentials(); - const locales = await getLocales({ repo, token, host }); + const { results: locales } = await getLocales({ repo, token, host }); if (json) { console.info(stringify(locales)); diff --git a/src/commands/locale-set-master.ts b/src/commands/locale-set-master.ts index ea18d21d..f47da0d2 100644 --- a/src/commands/locale-set-master.ts +++ b/src/commands/locale-set-master.ts @@ -31,7 +31,7 @@ export default createCommand(config, async ({ positionals, values }) => { const { token, host } = await getCredentials(); - const locales = await getLocales({ repo, token, host }); + const { results: locales } = await getLocales({ repo, token, host }); const locale = locales.find((l) => l.id === code); diff --git a/src/commands/preview-list.ts b/src/commands/preview-list.ts index eb6be79a..f17ecf73 100644 --- a/src/commands/preview-list.ts +++ b/src/commands/preview-list.ts @@ -30,7 +30,7 @@ export default createCommand(config, async ({ values }) => { const { token, host } = await getCredentials(); - const [previews, simulatorUrl] = await Promise.all([ + const [{ results: previews }, { simulator_url: simulatorUrl }] = await Promise.all([ getPreviews({ repo, token, host }), getSimulatorUrl({ repo, token, host }), ]); diff --git a/src/commands/preview-remove.ts b/src/commands/preview-remove.ts index d36120cf..a6049730 100644 --- a/src/commands/preview-remove.ts +++ b/src/commands/preview-remove.ts @@ -31,7 +31,7 @@ export default createCommand(config, async ({ positionals, values }) => { const { token, host } = await getCredentials(); - const previews = await getPreviews({ repo, token, host }); + const { results: previews } = await getPreviews({ repo, token, host }); const preview = previews.find((p) => p.url === previewUrl); if (!preview) { diff --git a/src/commands/push.ts b/src/commands/push.ts index bccc05c6..cdd1f5f4 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -172,7 +172,7 @@ async function explainExistingDocuments( let documentCount: number; try { - documentCount = await getDocumentTotalByCustomTypes(id, config); + documentCount = (await getDocumentTotalByCustomTypes(id, config)).total; } catch { return new CommandError( `Could not check whether type "${id}" has associated pages. ` + diff --git a/src/commands/repo-list.ts b/src/commands/repo-list.ts index 8eaeee19..371348c2 100644 --- a/src/commands/repo-list.ts +++ b/src/commands/repo-list.ts @@ -17,7 +17,7 @@ export default createCommand(config, async ({ values }) => { const { token, host } = await getCredentials(); - const repos = await getRepositories({ token, host }); + const { repositories: repos } = await getRepositories({ token, host }); if (json) { console.info( diff --git a/src/commands/repo-set-name.ts b/src/commands/repo-set-name.ts index 4463f0b9..844c78f3 100644 --- a/src/commands/repo-set-name.ts +++ b/src/commands/repo-set-name.ts @@ -25,7 +25,7 @@ export default createCommand(config, async ({ positionals, values }) => { const { token, host } = await getCredentials(); - const confirmedName = await setRepositoryName(displayName, { repo, token, host }); + const { repository } = await setRepositoryName(displayName, { repo, token, host }); - console.info(`Repository name set to: ${confirmedName}`); + console.info(`Repository name set to: ${repository.name}`); }); diff --git a/src/commands/repo-view.ts b/src/commands/repo-view.ts index 65bd43b7..4ba7e92a 100644 --- a/src/commands/repo-view.ts +++ b/src/commands/repo-view.ts @@ -33,7 +33,7 @@ export default createCommand(config, async ({ values }) => { return; } - const [repository, access] = await Promise.all([ + const [repository, syncState] = await Promise.all([ getRepository({ repo, token, host }), getRepositoryAccess({ repo, token, host }).catch((error) => { if (!isInvalidAuthContextError(error)) throw error; @@ -46,7 +46,7 @@ export default createCommand(config, async ({ values }) => { domain: repo, name: repository.name ?? null, url, - apiAccess: access ?? null, + apiAccess: syncState?.repository.api_access ?? null, }), ); return; @@ -55,5 +55,5 @@ export default createCommand(config, async ({ values }) => { const name = repository.name || "(no name)"; console.info(`Name: ${name}`); console.info(`URL: ${url}`); - console.info(`Content API: ${access ?? "(unavailable)"}`); + console.info(`Content API: ${syncState?.repository.api_access ?? "(unavailable)"}`); }); diff --git a/src/lib/prismic/client.ts b/src/lib/prismic/client.ts index cd088e71..28fcfa5a 100644 --- a/src/lib/prismic/client.ts +++ b/src/lib/prismic/client.ts @@ -68,14 +68,13 @@ export async function getProfile(config: Config): Promise { export async function getRepositories( config: Config, -): Promise[]> { +): Promise<{ repositories: z.infer[] }> { const url = new URL("user/repositories", getUserServiceUrl(config.host)); - const response = await request(url, { + return request(url, { headers: { Authorization: `Bearer ${config.token}` }, schema: z.object({ repositories: z.array(UserRepositorySchema) }), unknownErrorMessage: "Failed to load your repositories", }); - return response.repositories; } function getUserServiceUrl(host: string): URL { @@ -240,12 +239,7 @@ export async function uploadScreenshot( throw new UnsupportedFileTypeError(type); } - const presignedUrl = new URL("presigned-url", getScreenshotServiceUrl(config.host)); - presignedUrl.searchParams.set("repository", config.repo); - const presigned = await request(presignedUrl, { - headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, - schema: ScreenshotPresignedUrlResponseSchema, - }); + const presigned = await getScreenshotUploadForm(config); const extension = SUPPORTED_IMAGE_MIME_TYPES[type]; const digest = createHash("sha1") @@ -269,6 +263,17 @@ export async function uploadScreenshot( return url; } +function getScreenshotUploadForm( + config: RepositoryConfig, +): Promise> { + const url = new URL("presigned-url", getScreenshotServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + return request(url, { + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, + schema: ScreenshotPresignedUrlResponseSchema, + }); +} + export class UnsupportedFileTypeError extends Error { name = "UnsupportedFileTypeError"; @@ -295,16 +300,15 @@ const LocaleSchema = z.object({ export type Locale = z.infer; -export async function getLocales(config: RepositoryConfig): Promise { +export async function getLocales(config: RepositoryConfig): Promise<{ results: Locale[] }> { const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); url.searchParams.set("repository", config.repo); - const response = await request(url, { + return request(url, { headers: { Authorization: `Bearer ${config.token}` }, schema: z.object({ results: z.array(LocaleSchema) }), notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load locales", }); - return response.results; } export async function upsertLocale( @@ -355,18 +359,17 @@ const PreviewSchema = z.object({ export type Preview = z.infer; -export async function getPreviews(config: RepositoryConfig): Promise { +export async function getPreviews(config: RepositoryConfig): Promise<{ results: Preview[] }> { const url = new URL( "core/repository/preview_configs", getRepositoryDomainUrl(config.repo, config.host), ); - const response = await request(url, { + return request(url, { credentials: { "prismic-auth": config.token }, schema: z.object({ results: z.array(PreviewSchema) }), notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load previews", }); - return response.results; } export async function addPreview( @@ -413,25 +416,27 @@ const EnvironmentSchema = z.object({ }); export type Environment = z.infer; -export async function getEnvironments(config: RepositoryConfig): Promise { +export async function getEnvironments( + config: RepositoryConfig, +): Promise<{ results: Environment[] }> { const url = new URL("core/environments", getRepositoryDomainUrl(config.repo, config.host)); - const response = await request(url, { + return request(url, { credentials: { "prismic-auth": config.token }, schema: z.object({ results: z.array(EnvironmentSchema) }), notFoundMessage: `Repository not found: ${config.repo}`, }); - return response.results; } -export async function getSimulatorUrl(config: RepositoryConfig): Promise { +export async function getSimulatorUrl( + config: RepositoryConfig, +): Promise<{ simulator_url?: string }> { const url = new URL("core/repository", getRepositoryDomainUrl(config.repo, config.host)); - const response = await request(url, { + return request(url, { credentials: { "prismic-auth": config.token }, schema: z.object({ simulator_url: z.optional(z.string()) }), notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load simulator URL", }); - return response.simulator_url; } export async function setSimulatorUrl( @@ -451,24 +456,23 @@ export async function setSimulatorUrl( export async function getDocumentTotalByCustomTypes( customTypeId: string, config: RepositoryConfig, -): Promise { +): Promise<{ total: number }> { const url = new URL("core/documents/search", getRepositoryDomainUrl(config.repo, config.host)); - const response = await request(url, { + return request(url, { method: "POST", json: { customTypes: [customTypeId], limit: 0 }, credentials: { "prismic-auth": config.token }, schema: z.object({ total: z.number() }), notFoundMessage: `Repository not found: ${config.repo}`, }); - return response.total; } export async function createRelease( release: { label: string; hidden?: boolean }, config: RepositoryConfig, -): Promise { +): Promise<{ id: string }> { const url = new URL("core/releases", getRepositoryDomainUrl(config.repo, config.host)); - const response = await request(url, { + return request(url, { method: "POST", json: release, credentials: { "prismic-auth": config.token }, @@ -476,7 +480,6 @@ export async function createRelease( notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to create a release", }); - return response.id; } export async function deleteRelease(id: string, config: RepositoryConfig): Promise { @@ -726,15 +729,16 @@ export async function deleteWriteToken( }); } -export async function getRepositoryAccess(config: RepositoryConfig): Promise { +export async function getRepositoryAccess( + config: RepositoryConfig, +): Promise<{ repository: { api_access: string } }> { const url = new URL("syncState", getRepositoryDomainUrl(config.repo, config.host)); - const response = await request(url, { + return request(url, { credentials: { "prismic-auth": config.token }, schema: z.object({ repository: z.object({ api_access: z.string() }) }), notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to load repository access", }); - return response.repository.api_access; } export type RepositoryAccessLevel = "private" | "public" | "open"; @@ -756,11 +760,14 @@ export async function setRepositoryAccess( }); } -export async function setRepositoryName(name: string, config: RepositoryConfig): Promise { +export async function setRepositoryName( + name: string, + config: RepositoryConfig, +): Promise<{ repository: { name: string } }> { const formData = new FormData(); formData.set("displayname", name); const url = new URL("app/settings/repository", getRepositoryDomainUrl(config.repo, config.host)); - const response = await request(url, { + return request(url, { method: "POST", body: formData, credentials: { "prismic-auth": config.token }, @@ -768,7 +775,6 @@ export async function setRepositoryName(name: string, config: RepositoryConfig): notFoundMessage: `Repository not found: ${config.repo}`, unknownErrorMessage: "Failed to set repository name", }); - return response.repository.name; } function getRepositoryDomainUrl(repo: string, host: string): URL { diff --git a/src/lib/prismic/environments.ts b/src/lib/prismic/environments.ts index 82b52a3a..cdd7d009 100644 --- a/src/lib/prismic/environments.ts +++ b/src/lib/prismic/environments.ts @@ -7,7 +7,7 @@ export async function getUserEnvironments(config: { host: string; }): Promise { const { repo, token, host } = config; - const [session, environments] = await Promise.all([ + const [session, { results: environments }] = await Promise.all([ validateToken(token, { host }), getEnvironments({ repo, token, host }), ]); From b3419f3662344b77c0c0d2e8e321749ed842562c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:00:02 +0000 Subject: [PATCH 6/8] refactor: name the created release in dev Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- src/commands/dev.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 803ba98f..e9c54c14 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -174,13 +174,13 @@ async function saveSession(sessionPath: URL, session: Session): Promise { } async function createSession({ repo, token, host }: CustomTypesConfig): Promise { - const { id: releaseId } = await createRelease( + const release = await createRelease( { label: "prismic dev", hidden: true }, { repo, token, host }, ).catch((error) => { throw explainRequestError(error); }); - return { repo, releaseId, pid: process.pid, synced: {} }; + return { repo, releaseId: release.id, pid: process.pid, synced: {} }; } async function endSession({ releaseId, ...config }: Release, sessionPath: URL): Promise { From 33adef012c599a99d47c9ed650cc6f59deab7de8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:02:59 +0000 Subject: [PATCH 7/8] refactor: remove the service section comments from the client Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- src/lib/prismic/client.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/lib/prismic/client.ts b/src/lib/prismic/client.ts index 28fcfa5a..6a612dfe 100644 --- a/src/lib/prismic/client.ts +++ b/src/lib/prismic/client.ts @@ -13,8 +13,6 @@ type Config = { type RepositoryConfig = Config & { repo: string }; -// Auth service - export async function refreshToken(token: string, config: { host: string }): Promise { const url = new URL("refreshtoken", getAuthServiceUrl(config.host)); url.searchParams.set("token", token); @@ -39,8 +37,6 @@ function getAuthServiceUrl(host: string): URL { return new URL(`https://auth.${host}/`); } -// User service - const ProfileSchema = z.object({ email: z.string(), shortId: z.string(), @@ -81,8 +77,6 @@ function getUserServiceUrl(host: string): URL { return new URL(`https://api.internal.${host}/`); } -// Repository service - const RepositorySchema = z.object({ name: z.optional(z.string()), starter: z.nullish( @@ -157,8 +151,6 @@ function getRepositoryServiceUrl(host: string): URL { return new URL(`https://api.internal.${host}/repository/`); } -// Custom types service - export type CustomTypesConfig = RepositoryConfig & { releaseId?: string }; export function getCustomTypes(config: CustomTypesConfig): Promise { @@ -201,8 +193,6 @@ function getCustomTypesServiceUrl(host: string): URL { return new URL(`https://customtypes.${host}/`); } -// Screenshot service - const ScreenshotPresignedUrlResponseSchema = z.object({ values: z.object({ url: z.string(), @@ -289,8 +279,6 @@ function getScreenshotServiceUrl(host: string): URL { return new URL(`https://api.internal.${host}/screenshot/`); } -// Locale service - const LocaleSchema = z.object({ id: z.string(), label: z.string(), @@ -349,8 +337,6 @@ function getLocaleServiceUrl(host: string): URL { return new URL(`https://api.internal.${host}/locale/`); } -// Repository domain (Core and Wroom) - const PreviewSchema = z.object({ id: z.string(), label: z.string(), @@ -781,8 +767,6 @@ function getRepositoryDomainUrl(repo: string, host: string): URL { return new URL(`https://${repo}.${host}/`); } -// Dashboard (Wroom) - export async function checkIsDomainAvailable( config: Config & { domain: string }, ): Promise { @@ -820,8 +804,6 @@ function getDashboardUrl(host: string): URL { return new URL(`https://${host}/`); } -// Documentation - // Documentation is only published at prismic.io; the host option exists for // overrides only. const DEFAULT_DOCS_HOST = "prismic.io"; From 09efad423cac207ad35772e742b6a078029c40aa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:05:30 +0000 Subject: [PATCH 8/8] refactor: name client functions after what they act on In one client file, a name like bulkUpdate or removeLocale does not say what it acts on. The names now include the object and stay close to the API endpoint, for example bulkUpdateModels and getUserProfile. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L1dGD6G4hXCqeewEhox2Du --- src/commands/init.ts | 8 +++---- src/commands/locale-remove.ts | 4 ++-- src/commands/preview-remove.ts | 4 ++-- src/commands/push.ts | 6 +++--- src/commands/repo-list.ts | 4 ++-- src/commands/repo-set-api-access.ts | 8 +++---- src/commands/repo-view.ts | 4 ++-- src/commands/slice-add-variation.ts | 4 ++-- src/commands/slice-edit-variation.ts | 4 ++-- src/index.ts | 4 ++-- src/lib/prismic/client.ts | 32 +++++++++++++++++----------- src/lib/prismic/models.ts | 8 +++---- 12 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 680c6275..e965a345 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -16,11 +16,11 @@ import { } from "../lib/packageJson"; import { addPreview, + deletePreview, enableTypeBuilder, getPreviews, - getProfile, getRepository, - removePreview, + getUserProfile, type Repository, setSimulatorUrl, } from "../lib/prismic/client"; @@ -119,7 +119,7 @@ export default createCommand(config, async ({ values }) => { const { host, token: initialToken } = await getCredentials(); let token = initialToken; try { - await getProfile({ token, host }); + await getUserProfile({ token, host }); } catch (error) { if (error instanceof UnauthorizedRequestError || error instanceof ForbiddenRequestError) { if (env.PRISMIC_TOKEN) throw error; @@ -357,7 +357,7 @@ async function completeStarterHandoff( await Promise.all( previews .filter((preview) => preview.label === "Starter Preview") - .map((preview) => removePreview(preview.id, config)), + .map((preview) => deletePreview(preview.id, config)), ); const hasDevelopmentPreview = previews.some( (preview) => preview.url === adapter.localPreviewUrl, diff --git a/src/commands/locale-remove.ts b/src/commands/locale-remove.ts index 133a59dd..605400ef 100644 --- a/src/commands/locale-remove.ts +++ b/src/commands/locale-remove.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; -import { removeLocale } from "../lib/prismic/client"; +import { deleteLocale } from "../lib/prismic/client"; const config = { name: "prismic locale remove", @@ -31,7 +31,7 @@ export default createCommand(config, async ({ positionals, values }) => { const { token, host } = await getCredentials(); - await removeLocale(code, { repo, token, host }); + await deleteLocale(code, { repo, token, host }); console.info(`Locale removed: ${code}`); }); diff --git a/src/commands/preview-remove.ts b/src/commands/preview-remove.ts index a6049730..4a948916 100644 --- a/src/commands/preview-remove.ts +++ b/src/commands/preview-remove.ts @@ -1,7 +1,7 @@ import { getActiveRepositoryName } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { getPreviews, removePreview } from "../lib/prismic/client"; +import { deletePreview, getPreviews } from "../lib/prismic/client"; const config = { name: "prismic preview remove", @@ -38,7 +38,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw new CommandError(`Preview not found: ${previewUrl}`); } - await removePreview(preview.id, { repo, token, host }); + await deletePreview(preview.id, { repo, token, host }); console.info(`Preview removed: ${previewUrl}`); }); diff --git a/src/commands/push.ts b/src/commands/push.ts index cdd1f5f4..a9a7fc16 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -6,7 +6,7 @@ import { getAdapter } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { getDirtyPaths, getGitRoot } from "../lib/git"; -import { deleteScreenshots, getDocumentTotalByCustomTypes } from "../lib/prismic/client"; +import { deleteSliceScreenshots, getDocumentTotalByCustomType } from "../lib/prismic/client"; import { diffModels, getRemoteModels, writeRemoteModels } from "../lib/prismic/models"; import { completeOnboardingSteps, type OnboardingStep } from "../lib/prismic/onboarding"; import { ForbiddenRequestError } from "../lib/request"; @@ -122,7 +122,7 @@ export default createCommand(config, async ({ values }) => { throw await explainExistingDocuments(error, diff.customTypes.delete, { repo, token, host }); } for (const { id } of diff.slices.delete) { - await deleteScreenshots(id, { repo, token, host }).catch((error) => { + await deleteSliceScreenshots(id, { repo, token, host }).catch((error) => { const message = error instanceof Error ? error.message : String(error); console.warn( `Failed to delete screenshots for slice "${id}"${message ? `: ${message}` : "."}`, @@ -172,7 +172,7 @@ async function explainExistingDocuments( let documentCount: number; try { - documentCount = (await getDocumentTotalByCustomTypes(id, config)).total; + documentCount = (await getDocumentTotalByCustomType(id, config)).total; } catch { return new CommandError( `Could not check whether type "${id}" has associated pages. ` + diff --git a/src/commands/repo-list.ts b/src/commands/repo-list.ts index 371348c2..cd9daa3b 100644 --- a/src/commands/repo-list.ts +++ b/src/commands/repo-list.ts @@ -1,7 +1,7 @@ import { getCredentials } from "../auth"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; -import { getRepositories } from "../lib/prismic/client"; +import { getUserRepositories } from "../lib/prismic/client"; import { formatTable } from "../lib/string"; const config = { @@ -17,7 +17,7 @@ export default createCommand(config, async ({ values }) => { const { token, host } = await getCredentials(); - const { repositories: repos } = await getRepositories({ token, host }); + const { repositories: repos } = await getUserRepositories({ token, host }); if (json) { console.info( diff --git a/src/commands/repo-set-api-access.ts b/src/commands/repo-set-api-access.ts index ca44f233..04f2de16 100644 --- a/src/commands/repo-set-api-access.ts +++ b/src/commands/repo-set-api-access.ts @@ -1,9 +1,9 @@ import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { type RepositoryAccessLevel, setRepositoryAccess } from "../lib/prismic/client"; +import { type RepositoryApiAccessLevel, setRepositoryApiAccess } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; -const VALID_LEVELS: RepositoryAccessLevel[] = ["private", "public", "open"]; +const VALID_LEVELS: RepositoryApiAccessLevel[] = ["private", "public", "open"]; const config = { name: "prismic repo set-api-access", @@ -27,7 +27,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [level] = positionals; const { repo = await getRepositoryName() } = values; - if (!VALID_LEVELS.includes(level as RepositoryAccessLevel)) { + if (!VALID_LEVELS.includes(level as RepositoryApiAccessLevel)) { throw new CommandError( `Invalid access level: ${level}. Must be one of: ${VALID_LEVELS.join(", ")}`, ); @@ -35,7 +35,7 @@ export default createCommand(config, async ({ positionals, values }) => { const { token, host } = await getCredentials(); - await setRepositoryAccess(level as RepositoryAccessLevel, { repo, token, host }); + await setRepositoryApiAccess(level as RepositoryApiAccessLevel, { repo, token, host }); console.info(`Repository access set to: ${level}`); }); diff --git a/src/commands/repo-view.ts b/src/commands/repo-view.ts index 4ba7e92a..8deb129d 100644 --- a/src/commands/repo-view.ts +++ b/src/commands/repo-view.ts @@ -3,7 +3,7 @@ import { openBrowser } from "../lib/browser"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { isInvalidAuthContextError } from "../lib/prismic/auth"; -import { getRepository, getRepositoryAccess } from "../lib/prismic/client"; +import { getRepository, getRepositoryApiAccess } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { @@ -35,7 +35,7 @@ export default createCommand(config, async ({ values }) => { const [repository, syncState] = await Promise.all([ getRepository({ repo, token, host }), - getRepositoryAccess({ repo, token, host }).catch((error) => { + getRepositoryApiAccess({ repo, token, host }).catch((error) => { if (!isInvalidAuthContextError(error)) throw error; }), ]); diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index 57633236..3d6e06c5 100644 --- a/src/commands/slice-add-variation.ts +++ b/src/commands/slice-add-variation.ts @@ -6,7 +6,7 @@ import { getAdapter } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { readURLFile } from "../lib/file"; -import { uploadScreenshot } from "../lib/prismic/client"; +import { uploadSliceScreenshot } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { @@ -44,7 +44,7 @@ export default createCommand(config, async ({ positionals, values }) => { const url = /^https?:\/\//i.test(screenshot) ? new URL(screenshot) : pathToFileURL(screenshot); const blob = await readURLFile(url); - const screenshotUrl = await uploadScreenshot(blob, { + const screenshotUrl = await uploadSliceScreenshot(blob, { sliceId: slice.id, variationId: id, repo, diff --git a/src/commands/slice-edit-variation.ts b/src/commands/slice-edit-variation.ts index 5d01dc01..8ebaeeef 100644 --- a/src/commands/slice-edit-variation.ts +++ b/src/commands/slice-edit-variation.ts @@ -4,7 +4,7 @@ import { getAdapter } from "../adapters"; import { getCredentials } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { readURLFile } from "../lib/file"; -import { uploadScreenshot } from "../lib/prismic/client"; +import { uploadSliceScreenshot } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { @@ -40,7 +40,7 @@ export default createCommand(config, async ({ positionals, values }) => { const url = /^https?:\/\//i.test(screenshot) ? new URL(screenshot) : pathToFileURL(screenshot); const blob = await readURLFile(url); - const screenshotUrl = await uploadScreenshot(blob, { + const screenshotUrl = await uploadSliceScreenshot(blob, { sliceId: slice.id, variationId: variation.id, repo, diff --git a/src/index.ts b/src/index.ts index 1ed07e9c..eaf925fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import { detectAgent } from "./lib/ai"; import { CommandError } from "./lib/command"; import { decodePayload } from "./lib/jwt"; import { MissingPackageJson } from "./lib/packageJson"; -import { getProfile, UnsupportedFileTypeError } from "./lib/prismic/client"; +import { getUserProfile, UnsupportedFileTypeError } from "./lib/prismic/client"; import { InvalidEnvironmentError } from "./lib/prismic/environments"; import { FieldExistsError, @@ -174,7 +174,7 @@ async function main(): Promise { if (knownUserId) { sentrySetUser({ id: knownUserId }); } else if (!exp || exp > now) { - getProfile({ token, host }) + getUserProfile({ token, host }) .then((profile) => { trackUser(profile); sentrySetUser({ id: profile.shortId }); diff --git a/src/lib/prismic/client.ts b/src/lib/prismic/client.ts index 6a612dfe..2674f3d4 100644 --- a/src/lib/prismic/client.ts +++ b/src/lib/prismic/client.ts @@ -53,7 +53,7 @@ const UserRepositorySchema = z.object({ role: z.optional(z.union([z.string(), z.record(z.string(), z.string())])), }); -export async function getProfile(config: Config): Promise { +export async function getUserProfile(config: Config): Promise { const url = new URL("user/profile", getUserServiceUrl(config.host)); return request(url, { headers: { Authorization: `Bearer ${config.token}` }, @@ -62,7 +62,7 @@ export async function getProfile(config: Config): Promise { }); } -export async function getRepositories( +export async function getUserRepositories( config: Config, ): Promise<{ repositories: z.infer[] }> { const url = new URL("user/repositories", getUserServiceUrl(config.host)); @@ -171,13 +171,16 @@ export function getSlices(config: CustomTypesConfig): Promise { +export async function bulkUpdateModels( + changes: BulkModelChange[], + config: CustomTypesConfig, +): Promise { const url = new URL("bulk-update", getCustomTypesServiceUrl(config.host)); if (config.releaseId) url.searchParams.set("release", config.releaseId); await request(url, { @@ -208,7 +211,10 @@ const SUPPORTED_IMAGE_MIME_TYPES: Record = { "image/webp": ".webp", }; -export async function deleteScreenshots(sliceId: string, config: RepositoryConfig): Promise { +export async function deleteSliceScreenshots( + sliceId: string, + config: RepositoryConfig, +): Promise { const url = new URL("delete", getScreenshotServiceUrl(config.host)); url.searchParams.set("repository", config.repo); await request(url, { @@ -218,7 +224,7 @@ export async function deleteScreenshots(sliceId: string, config: RepositoryConfi }); } -export async function uploadScreenshot( +export async function uploadSliceScreenshot( blob: Blob, config: RepositoryConfig & { sliceId: string; variationId: string }, ): Promise { @@ -319,7 +325,7 @@ export async function upsertLocale( }); } -export async function removeLocale(code: string, config: RepositoryConfig): Promise { +export async function deleteLocale(code: string, config: RepositoryConfig): Promise { const url = new URL( `repository/locales/${encodeURIComponent(code)}`, getLocaleServiceUrl(config.host), @@ -380,7 +386,7 @@ export async function addPreview( }); } -export async function removePreview(id: string, config: RepositoryConfig): Promise { +export async function deletePreview(id: string, config: RepositoryConfig): Promise { const url = new URL( `previews/delete/${encodeURIComponent(id)}`, getRepositoryDomainUrl(config.repo, config.host), @@ -439,7 +445,7 @@ export async function setSimulatorUrl( }); } -export async function getDocumentTotalByCustomTypes( +export async function getDocumentTotalByCustomType( customTypeId: string, config: RepositoryConfig, ): Promise<{ total: number }> { @@ -715,7 +721,7 @@ export async function deleteWriteToken( }); } -export async function getRepositoryAccess( +export async function getRepositoryApiAccess( config: RepositoryConfig, ): Promise<{ repository: { api_access: string } }> { const url = new URL("syncState", getRepositoryDomainUrl(config.repo, config.host)); @@ -727,10 +733,10 @@ export async function getRepositoryAccess( }); } -export type RepositoryAccessLevel = "private" | "public" | "open"; +export type RepositoryApiAccessLevel = "private" | "public" | "open"; -export async function setRepositoryAccess( - level: RepositoryAccessLevel, +export async function setRepositoryApiAccess( + level: RepositoryApiAccessLevel, config: RepositoryConfig, ): Promise { const url = new URL( diff --git a/src/lib/prismic/models.ts b/src/lib/prismic/models.ts index bb1daefc..7e8e9128 100644 --- a/src/lib/prismic/models.ts +++ b/src/lib/prismic/models.ts @@ -8,8 +8,8 @@ import type { import { type ArrayDiff, diffArrays } from "../diff"; import { - type BulkChange, - bulkUpdate, + type BulkModelChange, + bulkUpdateModels, type CustomTypesConfig, getCustomTypes, getSlices, @@ -183,7 +183,7 @@ export async function writeRemoteModels( { customTypes, slices }: ModelsDiff, config: CustomTypesConfig, ): Promise { - const change = (type: BulkChange["type"], payload: BulkChange["payload"]) => ({ + const change = (type: BulkModelChange["type"], payload: BulkModelChange["payload"]) => ({ type, id: payload.id, payload, @@ -196,7 +196,7 @@ export async function writeRemoteModels( ...slices.update.map((model) => change("SLICE_UPDATE", model)), ...slices.delete.map(({ id }) => change("SLICE_DELETE", { id })), ]; - if (changes.length > 0) await bulkUpdate(changes, config); + if (changes.length > 0) await bulkUpdateModels(changes, config); } export function diffModels(