import { Artifact } from "@opencode/core/artifact" import { KV } from "@opencode/core/kv" import { Location } from "@opencode/core/location" import { LocationServiceMap } from "@opencode/core/location-service-map" import { AbsolutePath } from "@opencode/core/schema" import { Session } from "@opencode/core/session" import { ArtifactNotFoundError, FileNotFoundError, ForbiddenError, InvalidRequestError, ProviderNotFoundError, ServiceUnavailableError, } from "@opencode/protocol/errors" import { Effect } from "effect" import { type HttpServerRequest, HttpServerResponse } from "effect/http" import { HttpApiSchema } from "effect/http-api" import { awaitPlugins, locationErrors, requestRef, response } from "../location" import { missingSession } from "./session-error" /** `artifacts.*` in the `server.experimental` group. Every route needs experimental.artifacts in the resolved Location. */ export const makeArtifact = Effect.gen(function* () { const locations = yield* LocationServiceMap.Service const sessions = yield* Session.Service const kv = yield* KV.Service const explicitDirectory = ( request: HttpServerRequest.HttpServerRequest, location?: { readonly directory?: string; readonly workspace?: string }, ) => { if (location?.directory !== undefined) return location.directory const query = new URL(request.url, "http://localhost").searchParams.get("location[directory]") if (query) return query const header = request.headers["x-opencode-directory"] if (!header) return undefined try { return decodeURIComponent(header) } catch { return header } } const resolveLocation = ( request: HttpServerRequest.HttpServerRequest, options: { readonly location?: { readonly directory?: string; readonly workspace?: string } readonly sessionID?: Session.ID readonly artifactID?: Artifact.ID }, ) => Effect.gen(function* () { const requestedDir = explicitDirectory(request, options.location) if (requestedDir !== undefined) { return Location.Ref.make({ directory: AbsolutePath.make(requestedDir) }) } if (options.sessionID !== undefined) { const session = yield* sessions.get(options.sessionID).pipe(Effect.mapError(missingSession)) return session.location } if (options.artifactID !== undefined) { const directory = yield* Artifact.locate(kv, options.artifactID) if (directory !== undefined) { return Location.Ref.make({ directory: AbsolutePath.make(directory) }) } } return requestRef(request) }) const inLocation = ( request: HttpServerRequest.HttpServerRequest, options: { readonly location?: { readonly directory?: string; readonly workspace?: string } readonly sessionID?: Session.ID readonly artifactID?: Artifact.ID }, effect: Effect.Effect, ) => Effect.gen(function* () { const ref = yield* resolveLocation(request, options) return yield* awaitPlugins.pipe(Effect.andThen(effect), Effect.provide(locations.get(ref)), locationErrors) }) const mapCommon = (effect: Effect.Effect) => effect.pipe( Effect.catchTags({ "Artifact.DisabledError": (error: Artifact.DisabledError) => Effect.fail(new ForbiddenError({ message: error.message })), "Artifact.NotFoundError": (error: Artifact.NotFoundError) => Effect.fail(new ArtifactNotFoundError({ artifactID: error.artifactID, message: error.message })), "Artifact.ProviderNotFoundError": (error: Artifact.ProviderNotFoundError) => Effect.fail(new ProviderNotFoundError({ providerID: error.providerID, message: error.message })), "Artifact.InvalidError": (error: Artifact.InvalidError) => Effect.fail( new InvalidRequestError({ message: error.message, kind: "artifact_invalid", field: error.field }), ), "Artifact.ProviderError": (error: Artifact.ProviderError) => Effect.fail(new ServiceUnavailableError({ message: error.message, service: error.providerID })), "Session.NotFoundError": (error: Session.NotFoundError) => Effect.fail(missingSession(error)), } as never), ) return { providers: ( query: { readonly location?: { readonly directory?: string } }, request: HttpServerRequest.HttpServerRequest, ) => inLocation( request, { location: query.location }, Effect.gen(function* () { const artifacts = yield* Artifact.Service return yield* response(artifacts.providers()) }), ).pipe(mapCommon), list: ( query: { readonly location?: { readonly directory?: string } readonly sessionID?: Session.ID readonly provider?: Artifact.ProviderID }, request: HttpServerRequest.HttpServerRequest, ) => inLocation( request, { location: query.location, sessionID: query.sessionID }, Effect.gen(function* () { const artifacts = yield* Artifact.Service return yield* response(artifacts.list({ sessionID: query.sessionID, provider: query.provider })) }), ).pipe(mapCommon), create: ( query: { readonly location?: { readonly directory?: string } }, payload: Artifact.CreateInput, request: HttpServerRequest.HttpServerRequest, ) => inLocation( request, { location: query.location, sessionID: payload.sessionID }, Effect.gen(function* () { const artifacts = yield* Artifact.Service return yield* response(artifacts.create(payload)) }), ).pipe(mapCommon), get: ( artifactID: Artifact.ID, query: { readonly location?: { readonly directory?: string } }, request: HttpServerRequest.HttpServerRequest, ) => inLocation( request, { location: query.location, artifactID }, Effect.gen(function* () { const artifacts = yield* Artifact.Service return yield* response(artifacts.get(artifactID)) }), ).pipe(mapCommon), update: ( artifactID: Artifact.ID, query: { readonly location?: { readonly directory?: string } }, payload: Artifact.UpdateInput, request: HttpServerRequest.HttpServerRequest, ) => inLocation( request, { location: query.location, sessionID: payload.sessionID, artifactID }, Effect.gen(function* () { const artifacts = yield* Artifact.Service return yield* response(artifacts.update(artifactID, payload)) }), ).pipe(mapCommon), remove: ( artifactID: Artifact.ID, query: { readonly location?: { readonly directory?: string } }, request: HttpServerRequest.HttpServerRequest, ) => inLocation( request, { location: query.location, artifactID }, Effect.gen(function* () { const artifacts = yield* Artifact.Service yield* artifacts.remove(artifactID) return HttpApiSchema.NoContent.make() }), ).pipe(mapCommon), view: ( artifactID: Artifact.ID, query: { readonly location?: { readonly directory?: string } }, request: HttpServerRequest.HttpServerRequest, ) => inLocation( request, { location: query.location, artifactID }, Effect.gen(function* () { const artifacts = yield* Artifact.Service const parsed = new URL(request.url, "http://localhost") const prefix = `/api/experimental/artifacts/${encodeURIComponent(artifactID)}/view` if (parsed.pathname === prefix) { yield* artifacts.info(artifactID) return HttpServerResponse.empty({ status: 308, headers: { location: `${prefix}/${parsed.search}` }, }) } const subpath = extractViewSubpath(request.url, artifactID) const file = yield* artifacts.readLocalFile(artifactID, subpath).pipe( Effect.catchTag("Artifact.FileMissingError", (error) => Effect.fail(new FileNotFoundError({ path: error.path, message: error.message })), ), ) return HttpServerResponse.uint8Array(file.bytes, { status: 200, contentType: file.mime, headers: { "access-control-allow-origin": "*", "cache-control": "private, no-cache", "content-security-policy": "sandbox allow-scripts", "x-content-type-options": "nosniff", }, }) }), ).pipe(mapCommon), } }) function extractViewSubpath(url: string, artifactID: Artifact.ID) { const pathname = new URL(url, "http://localhost").pathname const prefix = `/api/experimental/artifacts/${encodeURIComponent(artifactID)}/view/` if (!pathname.startsWith(prefix)) return undefined const raw = pathname.slice(prefix.length) if (!raw) return undefined try { return decodeURIComponent(raw) } catch { return raw } }