import type { AssetBytesLocation, AssetChecksum, AssetDescriptor, AssetInput, AssetProvenance } from '../types/images.js'; /** * Contract, errors, and validation shared by every `AssetStore` implementation. * * Extracted when the filesystem and S3 stores arrived. Each store differs only in where the bytes * land; re-implementing the tenant, provenance, expiry, and checksum rules per backend is exactly * how two stores end up disagreeing about what a valid asset is. `images/assets` re-exports all of * it, so existing imports are unchanged. */ /** An asset input whose payload is already available locally as bytes. */ export interface ByteAssetInput extends Omit { /** * The asset's bytes. A store only accepts content it can hold, not a URL or another store's * reference. */ location: AssetBytesLocation; } /** A retrieved asset descriptor whose byte payload is available to the caller. */ export interface ByteAssetDescriptor extends Omit { /** The asset's bytes. */ location: AssetBytesLocation; } /** How an asset is stored: who owns it, where it came from, and how long it is kept. */ export interface AssetPutOptions { /** Tenant that owns the asset. This value is required for every later access. */ tenantId: string; /** Where the asset came from. */ provenance: AssetProvenance; /** Width in pixels, when known. */ width?: number; /** Height in pixels, when known. */ height?: number; /** Relative retention period. Mutually exclusive with `expiresAt`. */ ttlSeconds?: number; /** Absolute retention deadline. Mutually exclusive with `ttlSeconds`. */ expiresAt?: Date | string; } /** What a store knows about an asset without reading its bytes. */ export interface AssetStat { /** The store's id for the asset. */ assetId: string; /** The tenant that owns it. */ tenantId: string; /** Metadata-only descriptor. Its location points back to this store, not to the byte payload. */ descriptor: AssetDescriptor; /** ISO-8601 time it was stored. */ createdAt: string; /** ISO-8601 time it expires, when it does. */ expiresAt?: string; } /** Options for a signed URL. */ export interface AssetSignOptions { /** Requested signed-URL lifetime. Enforcement is delegated to the configured signer. */ expiresInSeconds?: number; } /** What a signer receives: the asset, the time, and the lifetime asked for. */ export interface AssetSignerContext extends AssetStat { /** The current time, ISO-8601. */ now: string; /** Lifetime requested for the URL, in seconds. */ expiresInSeconds?: number; } /** * Turns an asset into a URL a client can fetch, such as a presigned S3 URL or a route on your own * server. */ export type AssetSigner = (context: AssetSignerContext) => string | URL | Promise; /** Bytes as they arrive: a web stream, or any async iterable of chunks, such as a Node.js stream. */ export type AssetByteSource = ReadableStream | AsyncIterable; /** An asset whose bytes arrive as a stream, for content too large to hold in memory at once. */ export interface StreamAssetInput extends Omit { /** The bytes. Read once. */ stream: AssetByteSource; } /** A range of an asset's bytes, both ends inclusive, as an HTTP `Range` header has them. */ export interface AssetByteRange { /** The first byte. Defaults to 0. */ start?: number; /** The last byte. Defaults to the asset's last. */ end?: number; } /** How an asset is read as a stream. */ export interface AssetStreamOptions { /** Only these bytes, for a resumed download or a seek. */ range?: AssetByteRange; } /** An asset read as a stream: what the store knows of it, and its bytes, or the range asked for. */ export interface StreamAssetDescriptor { /** Metadata-only descriptor. Its location points back to the store. */ descriptor: AssetDescriptor; /** The bytes. */ stream: ReadableStream; /** The bytes the stream holds, both ends inclusive. Empty when `end` is below `start`. */ range: { start: number; end: number; }; } /** A value a store method returns, synchronously or asynchronously. */ export type AssetStoreResult = T | Promise; /** * Provider-neutral asset persistence contract. * * A store must treat a missing asset and an asset owned by another tenant identically. Implementations * may complete synchronously or asynchronously so the same contract can cover memory and remote stores. */ export interface AssetStore { /** Stores an asset. Throws `AssetStoreCapacityError` when it would not fit. */ put(input: ByteAssetInput, options: AssetPutOptions): AssetStoreResult; /** * Reads an asset with its bytes, or `undefined` when it does not exist, has expired, or belongs * to another tenant. */ get(assetId: string, tenantId: string): AssetStoreResult; /** Describes an asset without reading its bytes. */ stat(assetId: string, tenantId: string): AssetStoreResult; /** Deletes an asset. Resolves `true` when it existed. */ delete(assetId: string, tenantId: string): AssetStoreResult; /** * Returns a URL for the asset from the configured signer, or `undefined` when the asset does not * exist. */ sign(assetId: string, tenantId: string, options?: AssetSignOptions): AssetStoreResult; /** Deletes expired assets, returning how many went. */ purgeExpired(): AssetStoreResult; /** * Stores an asset whose bytes arrive as a stream, never holding them all at once, and checks its * size limit and checksum as they flow. Optional: `putAssetStream()` falls back to `put()`. */ putStream?(input: StreamAssetInput, options: AssetPutOptions): Promise; /** * Reads an asset, or a range of it, as a stream, or `undefined` as `get()` would. Optional: * `readAssetStream()` falls back to `get()`. */ getStream?(assetId: string, tenantId: string, options?: AssetStreamOptions): Promise; } /** Limits and behaviour for the in-process asset store. */ export interface MemoryAssetStoreOptions { /** Most assets held at once. Defaults to 1,000. */ maxEntries?: number; /** Most bytes held in total. Defaults to 64 MiB. */ maxTotalBytes?: number; /** Largest single asset accepted. Defaults to 10 MiB. */ maxAssetBytes?: number; /** * Lifetime for assets stored without their own, in seconds. Without it, assets are kept until * deleted. */ defaultTtlSeconds?: number; /** Creates asset ids. Defaults to random ids. */ createAssetId?: () => string; /** Replaces the system clock, for tests. */ now?: () => Date; /** Produces URLs for `sign()`. Without one, signing is refused. */ signer?: AssetSigner; } /** What the in-process store currently holds, against its limits. */ export interface MemoryAssetStoreSnapshot { /** Assets held. */ entries: number; /** Bytes held. */ totalBytes: number; /** Most assets allowed. */ maxEntries: number; /** Most bytes allowed. */ maxTotalBytes: number; /** Largest single asset allowed. */ maxAssetBytes: number; } /** Base class for asset store failures. */ export declare class AssetStoreError extends Error { /** Stable code for the failure. */ readonly code: string; /** The underlying error, when there was one. */ readonly cause?: unknown | undefined; constructor(message: string, /** Stable code for the failure. */ code: string, /** The underlying error, when there was one. */ cause?: unknown | undefined); } /** Raised when an asset or its options are invalid. */ export declare class AssetStoreValidationError extends AssetStoreError { constructor(message: string, cause?: unknown); } /** Which store limit a request ran into. */ export type AssetCapacityConstraint = 'maxEntries' | 'maxTotalBytes' | 'maxAssetBytes'; /** * Raised when an asset would exceed a store limit. Carries the limit, what is held, and what was * asked for. */ export declare class AssetStoreCapacityError extends AssetStoreError { /** The limit that was hit. */ readonly constraint: AssetCapacityConstraint; /** The limit's value. */ readonly maximum: number; /** What the store holds now. */ readonly current: number; /** What the asset would add. */ readonly requested: number; constructor( /** The limit that was hit. */ constraint: AssetCapacityConstraint, /** The limit's value. */ maximum: number, /** What the store holds now. */ current: number, /** What the asset would add. */ requested: number); } /** Raised when a signed URL cannot be produced. */ export declare class AssetStoreSigningError extends AssetStoreError { constructor(message: string, cause?: unknown); } export declare function validateByteInput(input: ByteAssetInput): ByteAssetInput; export declare function createStoredDescriptor(assetId: string, input: Pick, provenance: AssetProvenance, checksum: AssetChecksum, byteLength: number, width: number | undefined, height: number | undefined): AssetDescriptor; export declare function cloneDescriptor(descriptor: AssetDescriptor): AssetDescriptor; export declare function cloneAndValidateProvenance(provenance: AssetProvenance): AssetProvenance; export declare function cloneProvenance(provenance: AssetProvenance): AssetProvenance; export declare function cloneChecksum(checksum: AssetChecksum): AssetChecksum; export declare function sha256Checksum(bytes: Uint8Array, supplied?: AssetChecksum): AssetChecksum; export declare function resolveExpiration(options: AssetPutOptions, defaultTtlSeconds: number | undefined, nowMs: number): number | undefined; export declare function capacityOption(value: number | undefined, fallback: number, name: string): number; export declare function optionalPositiveNumber(value: number | undefined, name: string): number | undefined; export declare function optionalDimension(value: number | undefined, name: string): number | undefined; export declare function nonEmptyString(value: string | undefined, name: string): string; /** How much a store that cannot stream reads into memory for `putAssetStream()`. 64 MiB. */ export declare const DEFAULT_BUFFERED_ASSET_BYTES: number; /** Options for `putAssetStream()`: how the asset is stored, and how much a store that cannot stream may buffer. */ export interface PutAssetStreamOptions extends AssetPutOptions { /** Most bytes read into memory for a store without `putStream()`. Defaults to 64 MiB. */ maxBufferedBytes?: number; } /** * Stores a streamed asset in any store: through `putStream()` when the store has it, and otherwise by * reading the stream into memory, up to `maxBufferedBytes`, and calling `put()`. */ export declare function putAssetStream(store: AssetStore, input: StreamAssetInput, options: PutAssetStreamOptions): Promise; /** * Reads any store's asset, or a range of it, as a stream: through `getStream()` when the store has * it, and otherwise from `get()`. */ export declare function readAssetStream(store: AssetStore, assetId: string, tenantId: string, options?: AssetStreamOptions): Promise; /** Held bytes as a stream of the range asked for, under the store's descriptor. */ export declare function bytesAsStream(descriptor: AssetDescriptor, data: Uint8Array, range?: AssetByteRange): StreamAssetDescriptor; /** Checks a streamed asset's input as `validateByteInput()` checks bytes. */ export declare function validateStreamInput(input: StreamAssetInput): StreamAssetInput; /** Each chunk of a byte source, in order, whatever the source is. */ export declare function chunksOf(source: AssetByteSource): AsyncGenerator; /** * What a store learns as a stream flows past: its size and its SHA-256. `chunks()` passes each chunk * on, and fails with `AssetStoreCapacityError` the moment the size passes `maxBytes`. */ export declare class AssetMeter { private readonly maxBytes; /** Bytes seen so far. */ byteLength: number; private readonly hash; constructor(maxBytes: number | undefined); /** The source's chunks, counted, hashed, and checked against the limit. */ chunks(source: AssetByteSource): AsyncGenerator; /** The checksum of everything seen, checked against one the caller supplied. */ checksum(supplied?: AssetChecksum): AssetChecksum; } /** A generator as a web stream, read only as fast as its reader asks. */ export declare function streamFrom(chunks: AsyncIterator): ReadableStream; /** Bytes as a web stream, in chunks of 64 KiB. */ export declare function streamOf(data: Uint8Array): ReadableStream; /** Reads a whole stream into one array, refusing more than `maxBytes`. */ export declare function collectBytes(source: AssetByteSource, maxBytes: number, constraint: AssetCapacityConstraint): Promise; /** * A requested range against an asset of `byteLength` bytes. An end past the asset is cut to its last * byte, as HTTP does; a start past it, or an end before the start, is refused. */ export declare function resolveRange(range: AssetByteRange | undefined, byteLength: number): { start: number; end: number; };