@beignet/core
Version:
Core framework primitives for Beignet
577 lines • 17.8 kB
TypeScript
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