import { type OwnedBytes } from "./pack.js"; export declare const BUNDLE_ARCHIVE_FILE = "bundle.tar.zst"; export declare const BUNDLE_MANIFEST_FILE = "manifest.json"; export declare const BUNDLE_LATEST_FILE = "latest.json"; export declare const BUNDLE_ARCHIVE_CONTENT_TYPE = "application/zstd"; /** The bucket name env var. The only configuration this module reads. */ export declare const BUNDLE_BUCKET_ENV = "HASNA_LOOPS_ARTIFACTS_BUCKET"; /** The slice of an object store this needs. Injectable so tests can stand in an in-memory bucket. */ export interface BundleObjectStore { put(key: string, bytes: Uint8Array, contentType: string): Promise; get(key: string): Promise; exists(key: string): Promise; } export interface BundlePlacement { storageKind: "s3" | "db"; storageKey: string; } export interface BundleLatestPointer { version: number; bundleDigest: string; archiveSha256: string; updatedAt: string; } export interface BundleArtifactStorageOptions { bucket?: string; region?: string; prefix?: string; /** Overrides both the bucket and the local directory (tests, and the in-memory fake). */ store?: BundleObjectStore; /** Root for the local fallback placement. Defaults to `/artifacts`. */ localRoot?: string; env?: NodeJS.ProcessEnv; } export declare class BundleArtifactStorage { readonly bucket?: string; private readonly prefix; private readonly store; /** True when a real bucket backs this storage; false for the local fallback. */ readonly usesS3: boolean; /** * What a revision written through this storage should record as its kind. * * Derived from the placement that was actually chosen, never hard-coded: a * row claiming `s3` for bytes sitting on one station's disk is a lie told to * exactly the operator the fallback exists to serve. * * Read the result as "`s3`, or not `s3`". `db` is what the no-bucket * fallback records, and those bytes are FILES under the Loops data home, not * rows in Postgres - `loop_revisions.storage_kind` has a * `CHECK (storage_kind IN ('db','s3'))` on both the Postgres and the SQLite * schema, so there is no third value to record until a migration widens both. * Nothing keys behaviour off the kind: completeness keys on the recorded * `storage_key`, which the local fallback populates with the same scheme the * bucket uses. */ get storageKind(): BundlePlacement["storageKind"]; constructor(options?: BundleArtifactStorageOptions); /** * `////`. * * Both variable segments are percent-encoded even though the bundle-name * charset already excludes `/`: the encoding is what keeps this key builder * safe if that charset is ever relaxed. */ versionKey(tenantId: string, bundleName: string, version: number, file: string): string; latestKey(tenantId: string, bundleName: string): string; /** * Where a version WOULD be placed, without writing. * * Recorded on the revision row BEFORE the objects exist, so a crash between * the insert and the puts leaves a row whose objects are missing (diagnosable, * reported as `incomplete`) rather than an object no row references (invisible, * and therefore uncollectable). */ placement(tenantId: string, bundleName: string, version: number): BundlePlacement; /** Write manifest, then archive, then the latest pointer. Order is load-bearing (see the class doc). */ putVersion(tenantId: string, bundleName: string, version: number, archive: Uint8Array, manifest: Record): Promise; /** * Update the sole mutable object. * * A pointer, never a source of truth: a disagreement with `loop_revisions` is * always resolved in favour of the table, and this object is repaired from it. */ putLatest(tenantId: string, bundleName: string, pointer: BundleLatestPointer): Promise; readLatest(tenantId: string, bundleName: string): Promise; /** Read an archive back. `undefined` means the row exists but its object does not. */ readArchive(storageKey: string): Promise; /** * Is the object behind a recorded storage key actually there? * * A HEAD, not a GET. The version list answers complete/incomplete for every * revision on the page, and doing that with `readArchive` pulled each * archive - up to MAX_ARCHIVE_BYTES apiece - out of the object store only to * throw the bytes away. */ objectExists(storageKey: string): Promise; } /** An in-memory bucket. Exported for tests and for `--dry-run` plumbing. */ export declare function memoryObjectStore(): BundleObjectStore & { keys(): string[]; };