import { type PatchId, type ModuleFilePath, ValModules } from "@valbuild/core"; import type { Patch as PatchT, ParentRef as ParentRefT } from "@valbuild/core/patch"; import { type AuthorId, type BaseSha, BinaryFileType, type CommitSha, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, type PatchGroupMembership, SchemaSha, OrderedPatchesMetadata, OrderedPatches, SourcesSha, type PublishRefusal } from "./ValOps.js"; import { z } from "zod"; import type { HistoryError } from "./history/HistoryError.js"; import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js"; import { ParentRef, ValCommit, ValDeployment, type PatchGroupT } from "@valbuild/shared/internal"; import { result } from "@valbuild/core/fp"; declare const PatchId: z.ZodString & z.ZodType>; declare const CommitSha: z.ZodString & z.ZodType>; declare const BaseSha: z.ZodString & z.ZodType>; declare const AuthorId: z.ZodString & z.ZodType>; declare const ModuleFilePath: z.ZodString & z.ZodType>; export type PatchGroupMutationResult = { patchIds: PatchId[]; status?: undefined; error?: undefined; } | { patchIds: PatchId[]; status: 403 | 409 | 500; error: GenericErrorMessage; }; export declare class ValOpsHttp extends ValOps { private readonly contentUrl; private readonly project; /** * The repository this project's commits are mirrored into, or `null`. * * `null` is a project whose content service is the store of record: it * mints its own commit shas, and it knows this project's branch from the * project itself. Every request below that would have carried a branch and * a commit omits them instead, and the service answers from the project's * own chain -- which is where those answers always came from. * * It is NOT a degraded mode. The one thing that genuinely needs a * repository is producing the `.val.ts` text a commit mirrors, and a * project with no repository has nothing to mirror into. See `git` on * {@link ValApiOptions}. */ private readonly git; private readonly authHeaders; private readonly root; /** Val's content service owns the store. See {@link ValOps.patchesAreLocal}. */ readonly patchesAreLocal = false; /** See {@link ValOps.requiresAuth}. */ readonly requiresAuth = true; /** * A commit mirrors into `.val.ts` only when there is a repository. * * See {@link ValOps.mirrorsSourceFiles}. Set in the constructor rather than * as an initialiser because it depends on `git`, and a class field * initialiser runs before the constructor body has assigned it. */ protected readonly mirrorsSourceFiles: boolean; /** * What the content service last said this project expects of its publisher. * * `null` until something has asked it, which in practice is the first poll. * It is remembered rather than asked for on demand because the answer is * only wanted on the publish path, and that path already fetches the * patches it is publishing -- so a dedicated request would be a second round * trip for two fields that just arrived. * * It can be one poll out of date, and that is the right amount: the thing it * changes is whether this deployment can mirror commits into a repository, * which changes when a project CONNECTS one -- and a project that has just * connected is one whose builds are about to be replaced anyway. */ private projectExpectation; constructor(contentUrl: string, project: string, /** * The repository this project's commits are mirrored into, or `null`. * * `null` is a project whose content service is the store of record: it * mints its own commit shas, and it knows this project's branch from the * project itself. Every request below that would have carried a branch and * a commit omits them instead, and the service answers from the project's * own chain -- which is where those answers always came from. * * It is NOT a degraded mode. The one thing that genuinely needs a * repository is producing the `.val.ts` text a commit mirrors, and a * project with no repository has nothing to mirror into. See `git` on * {@link ValApiOptions}. */ git: { commit: string; branch: string; } | null, /** * An api key (how the app itself authenticates) or a personal access token * (how the CLI authenticates after `val login`). Same two shapes as * getSettings / uploadRemoteFile / getPresignedAuthNonce. */ auth: { apiKey: string; } | { pat: string; }, valModules: ValModules, options?: ValOpsOptions & { /** * Root of project relative to repository. * E.g. if this is a monorepo and the current app is in the /apps/my-app folder, * the root would be /apps/my-app */ root?: string; }); /** * A deployment that cannot mirror a project which expects to be mirrored. * * This is the one shape of "no base" that exists, and it is not the one it * sounds like. A project with no repository is fine: the content service is * the store of record for it and mints its own commit shas, so there is * always somewhere for the commit to go. What is refused is the mismatch -- * a project whose commits are mirrored into a repository, being published by * a build that was made before it had one and so has no commit to produce * that mirror against. * * It is a REAL state rather than a defensive check: it is exactly what a * deployment looks like between a project connecting a repository and its * next build going out. Left unchecked, such a publish writes the data and * silently fails to mirror it, and the repository quietly falls behind the * content nobody is told about. * * `null` when the service did not say (see `project` on the response * schema): an older content service is not evidence of anything, and * refusing every publish against one would be a worse failure than not * checking. */ publishRefusal(): PublishRefusal | null; onInit(): Promise; getPresignedAuthNonce(profileId: string, corsOrigin: string): Promise<{ status: "success"; data: { nonce: string; baseUrl: string; }; } | { status: "error"; statusCode: 401 | 500; error: GenericErrorMessage; }>; getStat(params: { baseSha: BaseSha; schemaSha: SchemaSha; patches?: PatchId[]; profileId?: AuthorId; } | null): Promise<{ type: "request-again" | "no-change"; baseSha: BaseSha; schemaSha: SchemaSha; sourcesSha: SourcesSha; patches: PatchId[]; } | { type: "use-websocket"; url: string; nonce: string; baseSha: BaseSha; schemaSha: SchemaSha; sourcesSha: SourcesSha; /** Absent for a project with no repository. See `git` on ValApiOptions. */ commitSha?: CommitSha; commits: ValCommit[]; deployments: ValDeployment[]; patches: PatchId[]; /** Of `patches`, the ones that have shipped. See the implementation. */ appliedPatches: PatchId[]; /** The newest commit, which is the publish head. */ headCommitSha?: string; } | { type: "error"; error: GenericErrorMessage; unauthorized?: boolean; networkError?: boolean; }>; getWebSocketNonce(profileId: string): Promise<{ status: "success"; data: { nonce: string; url: string; }; } | { status: "error"; error: GenericErrorMessage; }>; fetchPatches(filters: { patchIds?: PatchId[]; excludePatchOps: ExcludePatchOps; }): Promise; fetchPatchesInternal(filters: { patchIds?: PatchId[]; excludePatchOps: ExcludePatchOps; }): Promise; /** * Add patches to a patch group. * * The set arrives closed by the client — `withPatchIds` is the prefix closure * over the patch sets the staged patches belong to. We forward it and do not * second-guess it: deriving the closure needs the content schema, which this * process does have but content.val.build does not, and having two * implementations of the rule would be worse than having one. * * Membership rows are stamped with `coreVersion` on the content side — the * same stamp the patch row carries — so which client wrote a row stays * legible after the fact. */ stagePatches(patchGroupId: string, /** What the user asked to stage. */ patchIds: PatchId[], /** * What has to come with it, because the staged patches are written on top * of it. * * The content API stores each membership row as `explicit` or `dependency` * and treats what it is not told about as a dependency. Folding the two * halves into `patchIds` therefore files the patch somebody clicked as one * the closure dragged in — the exact opposite of what happened, and the * only record anywhere of what the author chose. */ withPatchIds: PatchId[], /** * WHO is asking, so the content API can refuse a group that is not theirs. * * Every call from this class carries the app's API key, which says which * PROJECT is calling and nothing about which editor. Without this the * content API cannot tell one of a project's editors from another, so the * only check on stage and unstage is the one in `ValServer` — and anything * reaching the content API by another route (an API key, a PAT) has none at * all. * * `null` where there is no session. The content API refuses rather than * treating that as a match: a group written by an api key has a null author * too, and `null === null` must not read as ownership. */ authorId: AuthorId | null): Promise; /** * Remove patches from a patch group. * * The set arrives closed FORWARDS by the client: unstaging a patch also * unstages everything built on top of it within its patch sets, and that is * what `withPatchIds` carries. */ unstagePatches(patchGroupId: string, /** What the user asked to unstage. */ patchIds: PatchId[], /** What has to go with it: everything built on top of it. */ withPatchIds: PatchId[], /** See {@link stagePatches} — the content API's half of the ownership check. */ authorId: AuthorId | null): Promise; /** * Every patch group on this branch, with what each holds. * * Read rather than mutated, and used to answer "which pending patches is THIS * person allowed to see". A draft render that skips this shows base + every * pending patch on the branch, including work other people have not * published — which is what independent publish exists to prevent. * * A failure is an empty list rather than a throw, and the caller decides what * that means. For a draft render the honest fallback is "show nothing * pending" rather than "show everything": being shown your own committed * content when the group lookup is down is a worse experience than being * shown somebody else's unpublished draft is a bug. */ /** * The last group lookup, and when it was made. * * A draft render calls `getPatchGroups` once per `fetchVal`, in series with * the whole-chain fetch, and a page that calls `fetchVal` several times pays * the round trip several times. Groups are per branch and change rarely, so a * short window removes the multiplier without letting a stage go unseen for * meaningfully longer than one render. * * Deliberately short. This is a read whose staleness decides whose draft * content someone sees, so it is a per-request de-duplication rather than a * cache: a second render a second later asks again. */ private patchGroupsCache; getPatchGroups(options?: { /** * Ask the content API even if a recent answer is remembered. * * For the checks that DECIDE something rather than render something. * `refuseUnlessOwn` reads this list to say whether a group is yours, and a * group is at its youngest exactly when that matters: the first write * creates it, the save response names it, and the shell flushes its queued * stages immediately — well inside the cache window. Served from a list * fetched before the group existed, every one of those was refused 403 and * dropped, so the queue that exists to survive the post-publish window * persisted nothing in the flow it was built for. * * Not solved by shortening the window: the cache sits on this instance, * which outlives the request, so "recent" is recent for the whole server * and not for one render. */ fresh?: boolean; }): Promise<{ status: "ok"; patchGroups: PatchGroupT[]; } | { status: "unsupported"; } | { status: "error"; message: string; }>; private fetchPatchGroups; private mutatePatchGroup; protected saveSourceFilePatch(path: ModuleFilePath, patch: PatchT, patchId: PatchId, parentRef: ParentRefT, authorId: AuthorId | null, sessionId: string | null, patchGroup?: PatchGroupMembership): Promise; /** * @deprecated For HTTP ops use direct upload instead (i.e. client should upload the files directly) since hosting platforms (Vercel) might have low limits on the size of the request body. */ saveBase64EncodedBinaryFileFromPatch(filePathOrRef: string, parentRef: ParentRef, patchId: PatchId, data: string | null, type: BinaryFileType, metadata: MetadataOfType | undefined): Promise>; private getHttpFiles; protected getSourceFile(path: string): Promise>; getBinaryFile(filePath: string): Promise; getBase64EncodedBinaryFileFromPatch(filePath: string, patchId: PatchId, remote: boolean): Promise; protected getBase64EncodedBinaryFileMetadataFromPatch(filePath: string, type: T, patchId: PatchId, remote: boolean): Promise>; protected getBinaryFileMetadata(filePath: string, type: T): Promise>; deletePatches(patchIds: PatchId[], /** * Patches that are NOT deleted but must lose their group membership. * * Deleting a patch out of the middle of a patch set leaves every group * still holding the rest with a non-prefix intersection — the patches after * the hole were written against a view that had it. The content API cannot * work out which those are (it has no schema), so the client sends the * forward closure and it drops those memberships without deleting anything. */ unstagePatchIds?: PatchId[]): Promise<{ deleted: PatchId[]; errors?: undefined; error?: undefined; } | { deleted: PatchId[]; errors: Record; } | { error: GenericErrorMessage; errors?: undefined; deleted?: undefined; }>; commit(prepared: PreparedCommit, message: string, committer: AuthorId, filesDirectory: string, newBranch?: string, /** * The patch group this commit EMPTIES, if it empties one. * * The content API closes the group it is given — and closes it WITHOUT * checking that the commit shipped all of it, so a caller that names a * group still holding work takes those patches out of every group and * leaves their author unable to publish them. The client therefore sends it * only when the publish accounts for everything the group still holds. * * Omitting it is not neutral: the commit still empties the group (the * content API drops applied ids from every group), but `published_at` is * never set, so the id is reused across publishes instead of a new group * per publish and the "already published" refusal can never fire. */ patchGroupId?: string): Promise<{ isNotFastForward?: boolean; updatedFiles: string[]; commit: CommitSha; /** See `CommitResult.parent`: absent means not reported. */ parent?: string; /** See `CommitResult.tree`: absent means not reported. */ tree?: string; branch: string; error?: undefined; } | { isNotFastForward?: boolean; error: GenericErrorMessage; }>; /** * One GET against the content service, parsed and Result-typed. * * Every history read has the same three failure modes - could not reach the * service, the commit is not there, the answer was not what was expected - * and each of them means something different to a caller deciding whether to * offer a restore. Doing it once here is what keeps that consistent across * the five endpoints. */ private getHistory; listCommits(branch: string, options?: { limit?: number; cursor?: string; }): Promise>; getCommitPatches(commitSha: string): Promise>; getCommitModules(commitSha: string, options?: { asOf?: boolean; moduleFilePath?: ModuleFilePath; }): Promise>; getCommitAffectedFiles(commitSha: string): Promise>; /** * `root` in front, and never a doubled or missing slash. * * `root` is "" for a project at the repository root and something like * `examples/next` otherwise; a ModuleFilePath always starts with "/". Joining * them by hand at each call site is how one of these ends up with "//" in the * middle, which GitHub answers with a 404 that reads like a missing file. */ gitPathOfModule(moduleFilePath: ModuleFilePath): result.Result; getFileAtCommit(commitSha: string, filePath: string, remote: boolean): Promise>; } export {};