2.0 beta — new releases can include breaking changes. Stable 1.x docs
dimah-s3v2.0.0-beta.0
@dimah-s3/core

@dimah-s3/core

The protocol, the fetch client, the error catalog, and pure helpers shared by the server and React packages.

@dimah-s3/core is the protocol source of truth. @dimah-s3/server implements it, and @dimah-s3/react calls it. It has no React and no AWS SDK code, so it runs in the browser, on the server, and in any Fetch runtime.

npm i @dimah-s3/core@beta

What lives here

  • createS3Client — the fetch client for the HTTP API. It implements S3Api and takes client plugins.
  • S3_API_ROUTES / S3_API_BASE_PATH — the endpoint paths.
  • S3_ERROR_CODES, APIError, s3Error, isS3ErrorCode — the error catalog and its helpers. See Errors.
  • Limits — S3_MAX_POST_OBJECT_BYTES, DELETE_MANY_MAX_KEYS, ROUTE_NAME_PATTERN, and the other protocol and S3 limits.
  • Zod schemas — in @dimah-s3/core/schema: the request bodies and queries the server validates, such as uploadBodySchema and downloadQuerySchema. They live outside the root entry, so browser bundles never include Zod.
  • Client plugin helpers — defineClientPlugin, createS3Fetch, pluginPath.
  • Pure helpers — validateFile, sha256Blob, resolveInlineDisposition, fitFileName, and more.

The React package does not re-export any of this. Import it from @dimah-s3/core. Display formatters such as formatFileSize and truncateFileName live in @dimah-s3/react/engine, next to the other formatters.


Client

lib/s3-client.ts
import { createS3Client } from "@dimah-s3/core";

export const api = createS3Client({
  basePath: "/api/s3",
  credentials: "include",
  headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),
});
  • basePath must match the server basePath (default /api/s3).
  • baseURL is the absolute API URL, such as https://api.example.com/api/s3. It wins over basePath.
  • headers is an object or a function called before each request.
  • fetch replaces the global fetch, for tests or SSR.

For React apps, use createS3Client from @dimah-s3/react. It takes the same options and adds a bound Provider and useApi.

Calls

Every method takes a payload, then optional { signal, headers }. Per-call headers override the client headers:

const controller = new AbortController();

const presign = await api.upload(
  { route: "avatar", name: file.name, size: file.size, type: file.type },
  { signal: controller.signal, headers: { "X-Request-Id": requestId } },
);
MethodPayloadReturns
upload{ route, name, size, type?, metadata?, checksum? }URL, method, headers or fields, ticket
confirm{ ticket }The verified object
download{ route, key, name?, disposition?, expiresIn? }url, mode, disposition, expiresIn?
catalognoneEvery route's public policy
delete{ route, key }{ key, bucket }
deleteMany{ route, keys }{ deleted, failed }
multipart.init{ route, name, size, type?, metadata? }uploadId, ticket, partSize, partCount, checksum
multipart.signPart{ ticket, partNumber, checksum? }Part URL, size, headers
multipart.listParts{ ticket }{ parts }
multipart.complete{ ticket }The verified object and uploadId
multipart.abort{ ticket }{ key, uploadId }
  • File fields are always name, size, and type. name and type are at most 255 UTF-8 bytes, with no control characters.
  • Confirm and every multipart call after init send only the upload ticket. The server picks the multipart part layout.
  • Send a presigned PUT with exactly the returned headers. They are signed.
  • In proxy mode, download returns a /file URL and no expiresIn. With an absolute baseURL, the client makes that URL absolute too.

The result also has $fetch (the shared fetch for plugin calls), $ERROR_CODES, and $Infer.

Errors

Every failure rejects with an APIError that has a catalog code:

import { isS3ErrorCode } from "@dimah-s3/core";

try {
  await api.confirm({ ticket });
} catch (error) {
  if (isS3ErrorCode(error, "UPLOAD_TICKET_EXPIRED")) {
    // Start the upload again
  }
}

A JSON error body keeps its code, params, and status. A non-JSON error becomes INTERNAL_ERROR (status 500 or above) or UNKNOWN_ERROR, and a request with no response becomes NETWORK_ERROR. An aborted call rejects with the signal's reason. The full list is on Errors.


Protocol

Paths are relative to basePath:

KeyMethodPath
uploadPOST/presign/upload
confirmPOST/presign/upload/confirm
downloadGET/presign/download
fileGET/file
catalogGET/routes
deleteDELETE/delete
deleteManyPOST/delete/batch
multipartInitPOST/presign/multipart/init
multipartSignPartPOST/presign/multipart/part
multipartListPartsPOST/presign/multipart/parts
multipartCompletePOST/presign/multipart/complete
multipartAbortPOST/presign/multipart/abort
  • GET and DELETE read a query. Every other endpoint reads a JSON body and accepts application/json only.
  • file is the file proxy that proxy-mode downloads point to. It is not an S3Api method.
  • deleteMany takes at most DELETE_MANY_MAX_KEYS (100) keys.

To build your own backend for the React hooks, implement S3Api. See Custom Backend.


Client plugins

A client plugin adds methods that call a server plugin's endpoints through the shared $fetch. pluginPath builds the same path the server mounts:

import { defineClientPlugin, pluginPath } from "@dimah-s3/core";

export function auditClient() {
  return defineClientPlugin({
    id: "audit",
    getActions: ($fetch) => ({
      recent: (limit?: number) =>
        $fetch<{ events: unknown[] }>(pluginPath("audit", "recent"), {
          method: "GET",
          query: { limit },
        }),
    }),
  });
}

const api = createS3Client({ plugins: [auditClient()] });
await api.audit.recent(20);

The id becomes api[id]. Core method names such as upload and delete are reserved (RESERVED_CLIENT_KEYS). dbClient() from @dimah-s3/db/client is a client plugin. See Plugins for the server side.


Helpers

import {
  buildPublicObjectUrl,
  resolveInlineDisposition,
  sha256Blob,
  validateFile,
} from "@dimah-s3/core";

// null when valid, otherwise an APIError:
// FILE_TYPE_NOT_ALLOWED, FILE_EMPTY, or FILE_TOO_LARGE
const error = validateFile(file, {
  accept: ["image/*"],
  maxFileSize: 2 * 1024 * 1024,
});

// SHA-256 as unpadded base64, read in 1 MiB chunks
const checksum = await sha256Blob(file, { signal });

// "inline" only when the type is on the allow-list
const { disposition } = resolveInlineDisposition({
  contentType: "image/png",
  requested: "inline",
});

// Public URL for public-read objects or a CDN. It does not sign.
const url = buildPublicObjectUrl({
  baseUrl: "https://cdn.example.com",
  key: "avatar/uuid/avatar.png",
});
HelperDoes
validateFile, matchesFileTypesChecks a file against accept tokens and a size limit
sha256Blob, sha256Base64SHA-256 in the form S3 and the upload checksum expect
resolveInlineDisposition, DEFAULT_INLINE_TYPESThe server's inline rule. Pass the catalog download.inlineTypes as allow
sniffContentType, matchesMagicBytes, isSniffableTypeReads a type from the first bytes, as verifyContent does
buildContentDisposition, parseFileNameBuilds and reads Content-Disposition with Unicode names
buildObjectKey, normalizeObjectKeyJoins key segments, and rejects unsafe keys (.., \, NUL)
objectKeyByteLength, S3_MAX_OBJECT_KEY_BYTESA key's UTF-8 length, and the 1024-byte limit normalizeObjectKey enforces
fitFileName, S3_MAX_FILE_FIELD_BYTESFits a file name to the 255-byte wire rule
buildPublicObjectUrlJoins a public origin and an encoded key
base64UrlEncode, base64UrlDecodebase64url without Buffer

DEFAULT_INLINE_TYPES holds raster images, PDF, audio, video, and text/plain. A wildcard never matches HTML, XML (including SVG and text/xsl), or JavaScript.

fitFileName(name) removes control characters, trims, and cuts a longer name to 255 UTF-8 bytes on a character boundary. It keeps an extension of up to 32 bytes and falls back to "file". The React upload engine applies it before presign, so a long name uploads under a shortened name.

sniffContentType looks past a leading ID3v2 tag, so ID3-tagged FLAC sniffs as audio/flac.


Type reference

import type {
  CreateS3ClientOptions,
  S3Api,
  S3RequestOptions,
  UploadPresignResponse,
  ConfirmedObjectResponse,
  DownloadPresignResponse,
  MultipartInitResponse,
  RouteCatalogEntry,
} from "@dimah-s3/core";

S3RequestOptions

Prop

Type


S3ClientFetchOptions

Prop

Type


ConfirmedObjectResponse

What confirm and multipart.complete return. Every value comes from HeadObject.

Prop

Type


DownloadPresignResponse

Prop

Type


MultipartInitResponse

Prop

Type


MultipartPartResponse

Prop

Type


ResolveInlineDispositionInput

Prop

Type

Frequently asked questions

On this page