UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

577 lines 17.8 kB
import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { StorageMetadata, StorageObject, StoragePort } from "../ports/index.js"; import type { ProviderInstrumentationTarget } from "../providers/index.js"; /** * Any Standard Schema compatible validator. */ export type StandardSchema = StandardSchemaV1<unknown, unknown>; /** * Value or promise of that value. */ export type MaybePromise<T> = T | Promise<T>; /** * Infer the parsed output type from a Standard Schema. */ export type InferSchemaOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>; /** * File metadata declared by a browser before an upload starts. */ export interface UploadFileIntent { /** * Original file name supplied by the client. */ name: string; /** * MIME content type supplied by the client. */ contentType: string; /** * File size in bytes. */ size: number; /** * Optional expected checksum for this file. Direct uploads use this to * verify the object bytes during completion. */ checksum?: UploadFileChecksum; } /** * Checksum algorithms supported by Beignet upload verification. */ export type UploadChecksumAlgorithm = "sha256"; /** * Expected or computed file checksum. */ export interface UploadFileChecksum { /** * Hash algorithm used for the checksum. */ algorithm: UploadChecksumAlgorithm; /** * Lowercase hex digest. */ value: string; } /** * Checksum verification requested by an upload definition. */ export interface UploadChecksumRequirement { /** * Hash algorithm to compute. */ algorithm: UploadChecksumAlgorithm; /** * Whether direct upload preparation/completion must include a checksum. * * @default true */ required?: boolean; } /** * Constraints for files accepted by an upload definition. */ export interface UploadFileConstraints { /** * Accepted MIME content types. Omit to accept any content type. */ contentTypes?: readonly string[]; /** * Maximum accepted file size in bytes. */ maxSizeBytes?: number; /** * Maximum files accepted by one request. * * @default 1 */ maxFiles?: number; /** * Object visibility written to storage. * * @default "private" */ visibility?: "private" | "public"; /** * Cache-Control value written to storage. */ cacheControl?: string; /** * Verify supported MIME types against file signatures instead of trusting * only the client-declared content type. * * @default "signature" */ contentTypeVerification?: "signature" | false; /** * Request checksum verification for direct uploads and expose the requirement * through the upload manifest so browser clients can send expected digests. */ checksum?: UploadChecksumRequirement; } /** * Parsed file intent with a generated upload id and storage key. */ export interface PreparedUploadFile extends UploadFileIntent { /** * Stable upload id used to correlate prepare, direct upload, and complete. */ uploadId: string; /** * Storage key that should receive the file. */ key: string; } /** * Direct upload instruction returned by an upload signer. */ export interface DirectUploadInstruction { method: "PUT"; url: string; headers?: Record<string, string>; expiresAt: string; } /** * Arguments passed to an upload signer. */ export interface SignUploadArgs { uploadName: string; uploadId: string; key: string; file: UploadFileIntent; metadata: unknown; storage: { visibility: "private" | "public"; cacheControl?: string; metadata: StorageMetadata; }; } /** * Port for providers that can create direct-upload instructions. */ export interface UploadSignerPort { /** * Create direct upload instructions for one prepared file. */ sign(args: SignUploadArgs): MaybePromise<DirectUploadInstruction>; } /** * Prepared upload file returned to clients. */ export interface PreparedUploadResultFile extends PreparedUploadFile { /** * Direct upload instruction when a signer is configured. */ direct?: DirectUploadInstruction; } /** * Result returned by `prepare(...)`. */ export interface PrepareUploadResult { uploadName: string; mode: "direct" | "server"; files: PreparedUploadResultFile[]; } /** * File object completed by direct upload or server upload. */ export interface CompletedUploadFile extends PreparedUploadFile { object: StorageObject; } /** * Result returned by upload completion. */ export interface CompleteUploadResult<Result = unknown> { uploadName: string; files: CompletedUploadFile[]; result: Result; } /** * Input for `prepare(...)`. */ export interface PrepareUploadInput { metadata: unknown; files: readonly UploadFileIntent[]; } /** * Input for `complete(...)`. */ export interface CompleteUploadInput { metadata: unknown; files: readonly PreparedUploadFile[]; } /** * Input for server-handled multipart uploads. */ export interface ServerUploadInput { formData: FormData; } /** * Authorization result accepted by upload definitions. */ export type UploadAuthorizeResult = boolean | undefined | { allowed: boolean; reason?: string; }; /** * Upload access mode. * * Protected uploads require an `authorize(...)` hook. Public uploads are * intentionally reachable without that hook. */ export type UploadAccess = "protected" | "public"; /** * Arguments passed to upload definition hooks. */ export interface UploadHookArgs<Metadata, Ctx> { ctx: Ctx; metadata: Metadata; } /** * Arguments passed to file-specific upload hooks. */ export interface UploadFileHookArgs<Metadata, Ctx> extends UploadHookArgs<Metadata, Ctx> { file: UploadFileIntent; uploadId: string; } /** * Arguments passed to `onComplete(...)`. */ export interface UploadCompleteHookArgs<Metadata, Ctx> extends UploadHookArgs<Metadata, Ctx> { files: CompletedUploadFile[]; } /** * Arguments passed to `verifyFile(...)` after a file exists in storage and * before `onComplete(...)` runs. */ export interface UploadVerifyFileHookArgs<Metadata, Ctx> extends UploadHookArgs<Metadata, Ctx> { file: CompletedUploadFile; storage: StoragePort; } /** * Verification result accepted by `verifyFile(...)`. */ export type UploadFileVerificationResult = boolean | undefined | { valid: boolean; reason?: string; details?: unknown; }; /** * Upload definition options. */ export interface DefineUploadOptions<MetadataSchema extends StandardSchema, Ctx, Result> { /** * Metadata schema submitted with prepare, server upload, and complete calls. */ metadata: MetadataSchema; /** * File constraints for this upload workflow. */ file: UploadFileConstraints; /** * Whether this upload may be prepared without an authorize hook. * * @default "protected" */ access?: UploadAccess; /** * Optional human-readable description for docs and tooling. */ description?: string; /** * Check whether the current actor may start this upload. */ authorize?(args: UploadFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<UploadAuthorizeResult>; /** * Build the storage key for one file. */ key(args: UploadFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<string>; /** * Add storage metadata for one file. */ storageMetadata?(args: UploadFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<StorageMetadata>; /** * Run after a file exists in storage and before `onComplete(...)`. Use this * for app-owned scanning, moderation, or quarantine decisions. */ verifyFile?(args: UploadVerifyFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<UploadFileVerificationResult>; /** * Run after files exist in storage. */ onComplete?(args: UploadCompleteHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<Result>; } /** * Upload definition created by `defineUpload(...)`. */ export interface UploadDef<Name extends string = string, MetadataSchema extends StandardSchema = StandardSchema, Ctx = unknown, Result = unknown> { readonly kind: "upload"; readonly name: Name; readonly metadata: MetadataSchema; readonly file: UploadFileConstraints; readonly access?: UploadAccess; readonly description?: string; authorize?(args: UploadFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<UploadAuthorizeResult>; key(args: UploadFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<string>; storageMetadata?(args: UploadFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<StorageMetadata>; verifyFile?(args: UploadVerifyFileHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<UploadFileVerificationResult>; onComplete?(args: UploadCompleteHookArgs<InferSchemaOutput<MetadataSchema>, Ctx>): MaybePromise<Result>; } /** * Infer the parsed metadata type for an upload definition. */ export type InferUploadMetadata<U extends UploadDef> = U["metadata"] extends StandardSchemaV1<unknown, infer Output> ? Output : never; /** * Infer the result returned by an upload definition's completion hook. */ export type InferUploadResult<U extends UploadDef> = U extends UploadDef<string, StandardSchema, unknown, infer Result> ? Result : unknown; /** * Nested upload registry shape used by server registration and typed clients. */ export interface UploadRegistry { /** * Upload definition or nested upload registry. */ readonly [key: string]: UploadDef | UploadRegistry; } /** * Infer every upload definition contained in a nested upload registry. */ export type UploadFromRegistry<Registry> = Registry extends UploadDef ? Registry : Registry extends readonly (infer Upload)[] ? Upload extends UploadDef ? Upload : never : Registry extends object ? { [Key in keyof Registry]: UploadFromRegistry<Registry[Key]>; }[keyof Registry] : never; /** * Client-safe upload metadata generated from server upload definitions. */ export interface UploadManifestEntry { /** * Upload route name. */ name: string; /** * Optional human-readable upload description. */ description?: string; /** * File constraints safe to expose to browser UI code. */ file: UploadFileConstraints; } /** * Request body limits for upload route actions. */ export interface UploadRequestLimits { /** * Maximum JSON body size for prepare and complete actions. * * @default 262144 */ jsonMaxBytes?: number; /** * Maximum multipart body size for server-handled upload actions when * Content-Length is present. File constraints still enforce per-file limits. * * @default 26214400 */ multipartMaxBytes?: number; } /** * Options for `createUploadRouter(...)`. */ export interface CreateUploadRouterOptions<Ctx> { /** * Upload definitions registered with this router. */ uploads: readonly UploadDef<string, StandardSchema, Ctx, unknown>[]; /** * Request context value or lazy context factory. */ ctx: Ctx | (() => MaybePromise<Ctx>); /** * Storage port used for server uploads and direct upload completion checks. */ storage: StoragePort; /** * Optional signer used to prepare direct upload instructions. */ signer?: UploadSignerPort; /** * Request body limits for upload actions. */ limits?: UploadRequestLimits; /** * Optional instrumentation target used by devtools/provider watchers. */ instrumentation?: ProviderInstrumentationTarget; /** * Optional upload id generator for tests or custom id policies. */ id?: () => string; } /** * Framework-neutral upload router. */ export interface UploadRouter { prepare(uploadName: string, input: PrepareUploadInput): Promise<PrepareUploadResult>; complete(uploadName: string, input: CompleteUploadInput): Promise<CompleteUploadResult>; upload(uploadName: string, input: ServerUploadInput): Promise<CompleteUploadResult>; handleRequest(request: Request, options: { uploadName: string; action: "prepare" | "complete" | "upload"; }): Promise<Response>; } /** * Machine-readable upload error codes. */ export type UploadErrorCode = "UPLOAD_NOT_FOUND" | "INVALID_UPLOAD_ACTION" | "INVALID_UPLOAD_METADATA" | "INVALID_UPLOAD_FILE" | "UNAUTHORIZED_UPLOAD" | "UPLOAD_OBJECT_NOT_FOUND" | "UPLOAD_BODY_TOO_LARGE" | "INVALID_UPLOAD_BODY"; /** * Base error for expected upload failures. * * Upload workflows throw one of the specific subclasses, such as * {@link UploadNotFoundError} or {@link UnauthorizedUploadError}. Catch this * base class to handle any expected upload failure; the upload router maps it * to an HTTP response using `code`, `status`, and `details`. */ export declare class UploadError extends Error { readonly code: UploadErrorCode; readonly status: number; readonly details?: unknown; protected constructor(args: { code: UploadErrorCode; message: string; status?: number; details?: unknown; }); } /** * Error thrown when an upload name is not registered on the router. */ export declare class UploadNotFoundError extends UploadError { readonly code: "UPLOAD_NOT_FOUND"; constructor(args: { message: string; details?: unknown; }); } /** * Error thrown when an upload request targets an unknown upload action. */ export declare class InvalidUploadActionError extends UploadError { readonly code: "INVALID_UPLOAD_ACTION"; constructor(args: { message: string; details?: unknown; }); } /** * Error thrown when upload metadata fails schema validation. */ export declare class InvalidUploadMetadataError extends UploadError { readonly code: "INVALID_UPLOAD_METADATA"; constructor(args: { message: string; details?: unknown; }); } /** * Error thrown when a file fails upload validation, such as file count, * content type (415), size limits (413), or prepared-file mismatches. */ export declare class InvalidUploadFileError extends UploadError { readonly code: "INVALID_UPLOAD_FILE"; constructor(args: { message: string; details?: unknown; status?: number; }); } /** * Error thrown when an upload is denied by authorize(...) or lacks * authorization configuration. */ export declare class UnauthorizedUploadError extends UploadError { readonly code: "UNAUTHORIZED_UPLOAD"; constructor(args: { message: string; details?: unknown; }); } /** * Error thrown when a completed upload references a stored object that does * not exist. */ export declare class UploadObjectNotFoundError extends UploadError { readonly code: "UPLOAD_OBJECT_NOT_FOUND"; constructor(args: { message: string; details?: unknown; }); } /** * Error thrown when an upload request body exceeds the configured size limit. */ export declare class UploadBodyTooLargeError extends UploadError { readonly code: "UPLOAD_BODY_TOO_LARGE"; constructor(args: { message: string; details?: unknown; }); } /** * Error thrown when an upload request body cannot be parsed or has an * invalid shape. */ export declare class InvalidUploadBodyError extends UploadError { readonly code: "INVALID_UPLOAD_BODY"; constructor(args: { message: string; details?: unknown; }); } /** * Define a typed upload workflow. */ export declare function defineUpload<const Name extends string, MetadataSchema extends StandardSchema, Ctx = unknown, Result = unknown>(name: Name, options: DefineUploadOptions<MetadataSchema, Ctx, Result>): UploadDef<Name, MetadataSchema, Ctx, Result>; /** * Context-bound upload helper factory. */ export interface Uploads<Ctx> { /** * Define an upload workflow with the bound context type. */ defineUpload<const Name extends string, MetadataSchema extends StandardSchema, Result = unknown>(name: Name, options: DefineUploadOptions<MetadataSchema, Ctx, Result>): UploadDef<Name, MetadataSchema, Ctx, Result>; } /** * Create upload helper methods bound to an application context type. * * Call it once in `lib/uploads.ts`: * * ```ts * export const { defineUpload } = createUploads<AppContext>(); * ``` */ export declare function createUploads<Ctx>(): Uploads<Ctx>; /** * Define a nested upload registry while preserving upload names and metadata * types for client code. */ export declare function defineUploads<const Uploads extends UploadRegistry>(uploads: Uploads): Uploads; /** * Flatten a nested upload registry into the list expected by * `createUploadRouter(...)`. */ export declare function uploadsFromRegistry(uploads: UploadRegistry | readonly UploadDef[]): UploadDef[]; /** * Create client-safe upload metadata for browser helpers. */ export declare function createUploadManifest(uploads: UploadRegistry | readonly UploadDef[]): UploadManifestEntry[]; /** * Create deterministic direct upload instructions for tests. */ export declare function createMemoryUploadSigner(options?: { baseUrl?: string; expiresAt?: string; }): UploadSignerPort; /** * Create a framework-neutral upload router. */ export declare function createUploadRouter<Ctx>(options: CreateUploadRouterOptions<Ctx>): UploadRouter; //# sourceMappingURL=index.d.ts.map