import { z } from "zod"; import { getSanityDomain, getSanityEnv, getApplicationDomain } from "../env"; import { OrganizationId } from "../organizations"; import { ProjectId } from "../projects"; import { AbstractApplication, type AbstractApplicationType, type Interface, type InterfacesOf, InterfaceSchema, } from "./applications"; import type { CoreApp } from "./core-apps"; import type { Studio } from "./studios"; /** * Application ID schema, branded for type safety. * @public */ export const ApplicationId = z.string().nonempty().brand("ApplicationId"); /** * Application ID type, branded for type safety. * @public */ export type ApplicationId = z.output; /** * Validates and brands a string as an ApplicationId. * @public */ export function brandApplicationId(id: string): ApplicationId { return ApplicationId.parse(id); } /** * A studio deployment's workspace record, as brett returns it. * @public */ export const BrettWorkspace = z.object({ id: z.string(), name: z.string(), title: z.string().nullable(), subtitle: z.string().nullable(), projectId: ProjectId, dataset: z.string(), schemaDescriptorId: z.string().nullable(), basePath: z.string().nullable(), icon: z.string().nullable(), }); /** * @public */ export type BrettWorkspace = z.output; /** * An application's active deployment from `GET /applications` with * `include=activeDeployment,interfaces,workspaces`. * @public */ export const ActiveDeployment = z.object({ id: z.string(), applicationId: ApplicationId, size: z.number().nullable(), version: z.string().nullable(), isAutoUpdating: z.boolean().nullable(), isActiveDeployment: z.boolean(), deployedBy: z.string().nullable(), createdAt: z.string(), updatedAt: z.string(), interfaces: z.array(InterfaceSchema).optional(), workspaces: z.array(BrettWorkspace).nullable().optional(), }); const applicationAttributes = z.object({ id: ApplicationId, organizationId: OrganizationId, isSingleton: z.boolean(), // Stable identity, distinct from the mutable `slug` address; keyed on for // config matching (W2). Server-computed (SDK-2333). name: z.string(), // Qualified, globally-unique handle (`sanity/` for singletons, // `/` otherwise). Server-computed (SDK-2333); consumers // read it directly rather than recomposing it. reference: z.string(), // Dashboard listing state — brett moved this off `config.studio` to a // top-level field shared by every application type and added `unlisted`. visibility: z.enum(["default", "unlisted", "disabled"]), icon: z.string().nullable().optional(), createdAt: z.string(), updatedAt: z.string(), activeDeployment: ActiveDeployment.nullable().optional(), }); /** * The internal (Sanity-hosted) hosting variant of the base application; studio * and core-app schemas extend it with their type-specific config. * @internal */ export const InternalApplication = applicationAttributes.extend({ slug: z.string(), externalUrl: z.null(), }); /** * The external hosting variant of the base application. * @internal */ export const ExternalApplication = applicationAttributes.extend({ slug: z.null(), externalUrl: z.string(), }); /** * Attributes every application from `GET /applications` carries; the studio and * core-app schemas extend a hosting variant with their type-specific config. * @public */ export const ApplicationBase = z.union([ InternalApplication, ExternalApplication, ]); /** * The shape every application class instance carries. Each concrete schema * declares its own `config`; the base type only knows the shared `mfManifest` * slot — a federation app serves one, which routes it to the application domain. * @public */ export type ApplicationBase = z.output & { config?: { mfManifest?: unknown }; }; /** * @public */ export type ActiveDeployment = z.output; /** * @public */ export abstract class BrettApplication< TApplication extends Studio | CoreApp, TType extends Extract, > extends AbstractApplication { readonly application: TApplication; readonly id: ApplicationId; /** * For local applications (`isLocal === true`), the deployed application * that shares the same `id` — if one was passed at construction. `null` * for deployed applications or when no remote twin was provided. */ readonly remoteApplication: this | null; readonly #isLocal: boolean; constructor( application: TApplication, type: TType, options: { isLocal?: boolean; remoteApplication?: BrettApplication | null; } = {}, ) { super(type); this.application = application; this.id = brandApplicationId(application.id); this.#isLocal = options.isLocal ?? false; this.remoteApplication = (options.remoteApplication as this | undefined) ?? null; } abstract get subtitle(): string | undefined; /** Stable identity, distinct from the address — what config matching keys on. */ get name(): string { return this.application.name; } /** Qualified, globally-unique handle, read straight off the server-computed field. */ get reference(): string { return this.application.reference; } toJSON(): TApplication { return this.application; } /** * Local dev servers and deployed federation apps (they serve an * mf-manifest) render as federated remotes; everything else in an iframe. */ get isFederated(): boolean { return this.isLocal || Boolean(this.application.config?.mfManifest); } override get isLocal(): boolean { return this.#isLocal; } /** * Interfaces ride the active deployment — a deployed app's own, or the ones a * local dev server's were adapted into upstream, so the read never branches. */ override interfaces( type?: T, ): InterfacesOf { const all = this.application.activeDeployment?.interfaces ?? []; // The runtime narrows correctly; TS can't tie a `filter` back to the // conditional return type, so assert it. return (type ? all.filter((iface) => iface.type === type) : all) as unknown as InterfacesOf; } /** * @returns A fully resolved URL instance for the application. * For Sanity-hosted applications, constructs a URL from the `slug` using the * studio domain pattern resolved from the environment at the consuming * app's build time. For externally hosted applications (including local dev * servers), returns the `externalUrl` as-is. */ get url(): URL { const application: ApplicationBase = this.application; if (application.externalUrl !== null) { return new URL(application.externalUrl); } if (this.isFederated && application.isSingleton) { return new URL( `https://${application.slug}-apps-${application.organizationId}.${getApplicationDomain()}`, ); } if (getSanityEnv() === "production") { return new URL(`https://${application.slug}.sanity.studio`); } return new URL(`https://${application.slug}.studio.${getSanityDomain()}`); } }