import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { type OwnedBytes } from "../lib/skill-bundle.js"; import type { BlobStorageKind, ServerArtifact, ServerRunRecord, ServerSkillBundle } from "./types.js"; export interface ArtifactBody { relativePath: string; bodyText: string; contentType: string; } /** The slice of the AWS client the storage uses; injectable so tests can stand in an in-memory bucket. */ export interface S3ClientLike { send(command: PutObjectCommand | GetObjectCommand | DeleteObjectCommand): Promise<{ Body?: unknown; }>; } export interface ArtifactStorageOptions { bucket?: string; prefix?: string; /** Separate expiring run outputs from durable bundle/version objects. Defaults to prefix for compatibility. */ runPrefix?: string; region?: string; /** Overrides the real S3 client (tests). Ignored when no bucket is configured. */ client?: S3ClientLike; } export declare class ArtifactStorage { private bucket?; private prefix; private runPrefix; private s3?; constructor(options?: ArtifactStorageOptions); get usesS3(): boolean; /** * Object key for a run artifact: `///`. * The tenant segment is structural, never optional - a bucket policy or a * lifecycle rule can key on it, and two tenants can never share a key even * when a run id were ever reused. `relativePath` is caller-supplied, so it is * sanitised: leading slashes and `..` segments are removed, and anything that * survives the sanitisation is what lands in the key. */ objectKeyFor(tenantId: string, runId: string, relativePath: string): string; /** * Object key for a quarantined partial artifact: a sibling namespace under the * same prefix, so one bucket policy covers both and the quarantine is visible * in the object listing without any metadata join. Deliberately keyed by * artifact id, not by the original relative path: the point of the move is * that the artifact's original location no longer resolves. */ quarantineKeyFor(tenantId: string, runId: string, artifactId: string): string; materialize(run: ServerRunRecord, artifact: Omit, body: ArtifactBody): Promise>; readText(artifact: ServerArtifact): Promise; /** * Place a skill bundle's bytes, returning where they went. * * Deliberately a second method on the same class rather than a second class: an * operator configures one bucket and one prefix, and a bundle that silently went * somewhere else than the run artifacts would be the kind of thing discovered during a * restore. The key is content-addressed - the digest is the whole name - so unlike * keyFor() there is no caller-supplied path component to sanitise, only a digest to * refuse if it is not one. * * Binary throughout. `materialize()` above takes `bodyText: string` because a run * artifact is a document; a skill bundle is a gzipped tar and there is no point in the * pipeline where turning it into a string would be lossless. */ putBundle(orgId: string, sha256: string, bytes: OwnedBytes, contentType?: string): Promise<{ storageKind: BlobStorageKind; storageKey?: string; bytes?: OwnedBytes; }>; /** * Read a bundle back. * * Returns null when the bytes cannot be produced - an S3-backed row on a server with no * bucket configured, or a row whose blob column is empty. Null is distinguishable from * an empty bundle by the caller, which knows the recorded byteSize. */ readBundle(bundle: ServerSkillBundle): Promise; /** * Remove a bundle's object, if it had one. * * The store collects the *row* when nothing references a digest any more, but a row is * only half of an S3-backed bundle. Without this the object stayed in the bucket after * the user deleted the skill - unreachable, because the key lived in the row that is * now gone, and therefore also uncollectable. That is a retention problem, not a leak * you can clean up later. * * A no-op when nothing is configured, which is why the caller can invoke it * unconditionally. */ deleteBundle(orgId: string, sha256: string): Promise; /** * Remove an artifact object, if it had one. * * The counterpart of deleteBundle for run artifacts: the row deletion is the * governance store's job, but an S3-backed artifact is only gone when the * object is gone. A no-op when nothing is configured, which is why the caller * can invoke it unconditionally. */ deleteObject(artifact: ServerArtifact): Promise; /** * Move an artifact's object into the quarantine namespace. * * Cancellation quarantines partial artifacts before the run is marked * cancelled, so a cancelled run's half-written outputs are neither served * nor silently deleted: they sit under `.../quarantine///` * and are recorded in a quarantine receipt. * * Returns the new key, or null for a database-backed artifact (its body has * no object to move; the row is marked by the receipt alone). A no-op for * an artifact that has no storage key at all. */ moveToQuarantine(artifact: ServerArtifact): Promise; private keyFor; /** * Version-addressed copy of a published bundle plus its manifest (hasna/apps#1630): * /skills////bundle.tar.gz * /skills////manifest.json * The content-addressed object under bundles/ stays the read path (dedupe); these keys * are the durable, browsable history and are never deleted by orphan collection. * * RETENTION: neither version history nor content-addressed bundles may share an * expiring lifecycle prefix with run outputs. Configure runPrefix separately for * ephemeral artifacts; retain this prefix until reference-aware maintenance removes * the version rows and objects together. Existing persisted storage keys remain valid. * Returns the placement recorded on the version row; in db mode nothing is written. */ putVersionObjects(orgId: string, slug: string, version: string, bytes: OwnedBytes, manifest: Record, contentType?: string): Promise<{ storageKind: BlobStorageKind; storageKey?: string; }>; /** Where putVersionObjects WOULD place a version, without writing: recorded on the row before the objects exist. */ versionPlacement(orgId: string, slug: string, version: string): { storageKind: BlobStorageKind; storageKey?: string; }; versionKeyFor(orgId: string, slug: string, version: string, file: string): string; private bundleKeyFor; }