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..4d5bd40f 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, @@ -172,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/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..e9c54c14 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, @@ -175,13 +174,13 @@ async function saveSession(sessionPath: URL, session: Session): Promise { } async function createSession({ repo, token, host }: CustomTypesConfig): Promise { - const 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 { 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..e965a345 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 { + deletePreview, enableTypeBuilder, + getPreviews, getRepository, + getUserProfile, 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"; @@ -121,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; @@ -355,11 +353,11 @@ 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") - .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-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..b801bb5e 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 = { @@ -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-remove.ts b/src/commands/locale-remove.ts index 66da2df5..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/clients/locale"; +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/locale-set-master.ts b/src/commands/locale-set-master.ts index 029c1990..f47da0d2 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", @@ -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-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..f17ecf73 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 = { @@ -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 93ef5b9c..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/clients/core"; +import { deletePreview, getPreviews } from "../lib/prismic/client"; const config = { name: "prismic preview remove", @@ -31,14 +31,14 @@ 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) { 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/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..a9a7fc16 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 { 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"; @@ -123,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}` : "."}`, @@ -173,7 +172,7 @@ async function explainExistingDocuments( let documentCount: number; try { - documentCount = await getDocumentTotalByCustomTypes(id, config); + 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-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..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/clients/user"; +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 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 eb9caab7..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/clients/wroom"; +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-set-name.ts b/src/commands/repo-set-name.ts index 3cb27376..844c78f3 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 = { @@ -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 5f6be791..8deb129d 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, getRepositoryApiAccess } from "../lib/prismic/client"; import { getRepositoryName } from "../project"; const config = { @@ -34,9 +33,9 @@ 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) => { + getRepositoryApiAccess({ repo, token, host }).catch((error) => { if (!isInvalidAuthContextError(error)) throw error; }), ]); @@ -47,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; @@ -56,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/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index ce5ad48a..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/clients/custom-types"; +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 dca61c1b..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/clients/custom-types"; +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/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..eaf925fc 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 { getUserProfile, UnsupportedFileTypeError } from "./lib/prismic/client"; import { InvalidEnvironmentError } from "./lib/prismic/environments"; import { FieldExistsError, @@ -175,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 new file mode 100644 index 00000000..2674f3d4 --- /dev/null +++ b/src/lib/prismic/client.ts @@ -0,0 +1,868 @@ +import { createHash } from "node:crypto"; + +import type { DynamicCustomTypeModel, SharedSliceModel } from "@prismicio/types-internal"; +import * as z from "zod/mini"; + +import { request } from "../request"; +import { appendTrailingSlash } from "../url"; + +type Config = { + token: string | undefined; + host: string; +}; + +type RepositoryConfig = Config & { repo: string }; + +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}/`); +} + +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())])), +}); + +export async function getUserProfile(config: Config): Promise { + const url = new URL("user/profile", getUserServiceUrl(config.host)); + return request(url, { + headers: { Authorization: `Bearer ${config.token}` }, + schema: ProfileSchema, + unknownErrorMessage: "Failed to load your profile", + }); +} + +export async function getUserRepositories( + config: Config, +): Promise<{ repositories: z.infer[] }> { + const url = new URL("user/repositories", getUserServiceUrl(config.host)); + return request(url, { + headers: { Authorization: `Bearer ${config.token}` }, + schema: z.object({ repositories: z.array(UserRepositorySchema) }), + unknownErrorMessage: "Failed to load your repositories", + }); +} + +function getUserServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/`); +} + +const RepositorySchema = z.object({ + name: z.optional(z.string()), + starter: z.nullish( + z.object({ + 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; + +export function getRepository(config: RepositoryConfig): Promise { + 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 { + 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({ + completedSteps: z.array(z.string()), +}); +type OnboardingState = z.infer; + +export async function getOnboardingState(config: RepositoryConfig): Promise { + const url = new URL("onboarding", getRepositoryServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + return request(url, { + credentials: { "prismic-auth": config.token }, + schema: OnboardingStateSchema, + }); +} + +export async function toggleOnboardingStep( + stepId: string, + config: RepositoryConfig, +): Promise { + 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 getRepositoryServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/repository/`); +} + +export type CustomTypesConfig = RepositoryConfig & { releaseId?: string }; + +export function getCustomTypes(config: CustomTypesConfig): Promise { + 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 { + 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 BulkModelChange = { + type: `${"CUSTOM_TYPE" | "SLICE"}_${"INSERT" | "UPDATE" | "DELETE"}`; + id: string; + payload: DynamicCustomTypeModel | SharedSliceModel | { id: string }; +}; + +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, { + method: "POST", + json: { changes }, + headers: { Authorization: `Bearer ${config.token}`, repository: config.repo }, + notFoundMessage: `Repository not found: ${config.repo}`, + unknownErrorMessage: "Failed to update models", + }); +} + +function getCustomTypesServiceUrl(host: string): URL { + return new URL(`https://customtypes.${host}/`); +} + +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 deleteSliceScreenshots( + sliceId: string, + config: RepositoryConfig, +): Promise { + 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 }, + }); +} + +export async function uploadSliceScreenshot( + blob: Blob, + config: RepositoryConfig & { sliceId: string; variationId: string }, +): Promise { + const { sliceId, variationId, repo } = config; + + const type = blob.type; + if (!(type in SUPPORTED_IMAGE_MIME_TYPES)) { + throw new UnsupportedFileTypeError(type); + } + + const presigned = await getScreenshotUploadForm(config); + + 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; +} + +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"; + + constructor(mimeType: string) { + const supportedTypes = Object.keys(SUPPORTED_IMAGE_MIME_TYPES); + super( + `Unsupported file type: ${mimeType || "unknown"}. Supported: ${supportedTypes.join(", ")}`, + ); + } +} + +function getScreenshotServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/screenshot/`); +} + +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<{ results: Locale[] }> { + const url = new URL("repository/locales", getLocaleServiceUrl(config.host)); + url.searchParams.set("repository", config.repo); + 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", + }); +} + +export async function upsertLocale( + locale: { id: string; isMaster?: boolean; customName?: string }, + config: RepositoryConfig, +): Promise { + 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 deleteLocale(code: string, config: RepositoryConfig): Promise { + 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 getLocaleServiceUrl(host: string): URL { + return new URL(`https://api.internal.${host}/locale/`); +} + +const PreviewSchema = z.object({ + id: z.string(), + label: z.string(), + url: z.string(), +}); + +export type Preview = z.infer; + +export async function getPreviews(config: RepositoryConfig): Promise<{ results: Preview[] }> { + const url = new URL( + "core/repository/preview_configs", + getRepositoryDomainUrl(config.repo, config.host), + ); + 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", + }); +} + +export async function addPreview( + previewConfig: { + name: string; + websiteURL: string; + resolverPath: string | undefined; + }, + config: RepositoryConfig, +): Promise { + 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 deletePreview(id: string, config: RepositoryConfig): Promise { + 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", + }); +} + +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<{ results: Environment[] }> { + const url = new URL("core/environments", getRepositoryDomainUrl(config.repo, config.host)); + return request(url, { + credentials: { "prismic-auth": config.token }, + schema: z.object({ results: z.array(EnvironmentSchema) }), + notFoundMessage: `Repository not found: ${config.repo}`, + }); +} + +export async function getSimulatorUrl( + config: RepositoryConfig, +): Promise<{ simulator_url?: string }> { + const url = new URL("core/repository", getRepositoryDomainUrl(config.repo, config.host)); + 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", + }); +} + +export async function setSimulatorUrl( + simulatorUrl: string, + config: RepositoryConfig, +): Promise { + 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", + }); +} + +export async function getDocumentTotalByCustomType( + customTypeId: string, + config: RepositoryConfig, +): Promise<{ total: number }> { + const url = new URL("core/documents/search", getRepositoryDomainUrl(config.repo, config.host)); + 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}`, + }); +} + +export async function createRelease( + release: { label: string; hidden?: boolean }, + config: RepositoryConfig, +): Promise<{ id: string }> { + const url = new URL("core/releases", getRepositoryDomainUrl(config.repo, config.host)); + return 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", + }); +} + +export async function deleteRelease(id: string, config: RepositoryConfig): Promise { + 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", + }); +} + +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.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", + }); +} + +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.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", + }); +} + +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.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 { + 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", + }); +} + +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.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 { + 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", + }); +} + +export async function createOAuthAuthorization( + appId: string, + scope: string, + config: RepositoryConfig, +): Promise { + 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", + }); +} + +export async function deleteOAuthAuthorization( + authId: string, + config: RepositoryConfig, +): Promise { + const url = new URL( + `settings/security/authorizations/${encodeURIComponent(authId)}`, + 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 { + 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", + }); +} + +export async function createWriteToken( + name: string, + config: RepositoryConfig, +): Promise { + 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", + }); +} + +export async function deleteWriteToken( + tokenValue: string, + config: RepositoryConfig, +): Promise { + const url = new URL( + `settings/security/token/${encodeURIComponent(tokenValue)}`, + 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 getRepositoryApiAccess( + config: RepositoryConfig, +): Promise<{ repository: { api_access: string } }> { + const url = new URL("syncState", getRepositoryDomainUrl(config.repo, config.host)); + 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", + }); +} + +export type RepositoryApiAccessLevel = "private" | "public" | "open"; + +export async function setRepositoryApiAccess( + level: RepositoryApiAccessLevel, + config: RepositoryConfig, +): Promise { + 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", + }); +} + +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)); + return 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", + }); +} + +function getRepositoryDomainUrl(repo: string, host: string): URL { + return new URL(`https://${repo}.${host}/`); +} + +export async function checkIsDomainAvailable( + config: Config & { domain: string }, +): Promise { + const url = new URL( + `app/dashboard/repositories/${encodeURIComponent(config.domain)}/exists`, + getDashboardUrl(config.host), + ); + return request(url, { + credentials: { "prismic-auth": config.token }, + 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 request(url, { + method: "POST", + json: { domain, name, framework, plan: "personal" }, + credentials: { "prismic-auth": config.token }, + unknownErrorMessage: "Failed to create repository", + }); +} + +function getDashboardUrl(host: string): URL { + return new URL(`https://${host}/`); +} + +// 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/`); +} 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..cdd7d009 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; @@ -8,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 }), ]); diff --git a/src/lib/prismic/models.ts b/src/lib/prismic/models.ts index 694903f9..7e8e9128 100644 --- a/src/lib/prismic/models.ts +++ b/src/lib/prismic/models.ts @@ -8,12 +8,12 @@ import type { import { type ArrayDiff, diffArrays } from "../diff"; import { - type BulkChange, - bulkUpdate, + type BulkModelChange, + bulkUpdateModels, type CustomTypesConfig, getCustomTypes, getSlices, -} from "./clients/custom-types"; +} from "./client"; type Fields = Record; @@ -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( 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";