npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@superyachttimes/lib-uploader

v1.3.0

Published

Uploader library for the SuperYacht Times tech stack

Downloads

701

Readme

@superyachttimes/lib-uploader

A TypeScript library for uploading files to S3 via the SuperYacht Times API. Supports two API contracts: the classic legacy API (direct S3 uploads via pre-signed URLs, plus resumable TUS/Transloadit uploads for media processing) and the new v2 API (/v1/files on the v2-api, with server-owned statuses and upload confirmation).

Works in both browser (native File) and Node.js environments.

Table of Contents

Installation

npm install @superyachttimes/lib-uploader

Quick Start

Browser (Direct Upload)

import Uploader from '@superyachttimes/lib-uploader'

const onProgress = (progress, completedBytes, totalBytes, fileData) => {
  console.log(`Upload progress: ${(progress * 100).toFixed(1)}%`)
}

const onUploaded = ({ id, file, result }) => {
  console.log('Upload complete:', id)
}

const uploader = new Uploader(
  'https://api0.superyachtapi.com',
  'MY_API_KEY',
  onProgress,
  onUploaded
)

// file comes from an <input type="file"> element
const { id, file, result } = await uploader.upload(file, 'owner-id', 'yachteye')

Browser (TUS Upload with Transloadit)

import Uploader from '@superyachttimes/lib-uploader'

const onProgress = (progress, completedBytes, totalBytes, fileData) => {
  console.log(`Upload progress: ${(progress * 100).toFixed(1)}%`)
}

const onUploaded = ({ id, file, result }) => {
  console.log('Upload complete:', id)
}

const uploader = new Uploader(
  'https://api0.superyachtapi.com',
  'MY_API_KEY',
  onProgress,
  onUploaded,
  'TRANSLOADIT_API_KEY' // enables TUS mode
)

const { id, file, result } = await uploader.upload(file, 'owner-id', 'yachteye')

Upload Modes

The library supports two upload modes, determined by the transloadit parameter in the constructor.

Direct Upload (S3 Pre-signed URL)

Used when no Transloadit API key is provided. The flow:

  1. Requests a pre-signed S3 URL from the SYT API
  2. PUTs the file directly to S3
  3. Updates file status on the SYT API

This mode is simpler and suitable for files that don't need server-side processing.

TUS Upload (Transloadit)

Used when a Transloadit API key is provided. The flow:

  1. Requests a file record from the SYT API
  2. Creates a Transloadit assembly for the appropriate file type (image/video/pdf)
  3. Uploads the file using the TUS resumable upload protocol
  4. Transloadit processes the file (transcoding, compression, etc.)
  5. Updates file status and variants on the SYT API

This mode supports resumable uploads with automatic retries, and triggers server-side media processing via Transloadit templates.

Constructor

new Uploader(baseURL, apiKey, onProgress?, onUploaded?, transloadit?, debug?)

| Parameter | Type | Required | Default | Description | |---|---|---|---|---| | baseURL | string | Yes | — | Base URL of the SYT API (e.g. https://api0.superyachtapi.com). Automatically normalized to end with /api/uploads/. | | apiKey | string | Yes | — | API key for Bearer token authentication. | | onProgress | OnProgress | No | () => {} | Called with upload progress updates. | | onUploaded | OnUploaded | No | () => {} | Called when the upload completes successfully. | | transloadit | string \| boolean | No | false | Transloadit API key. Pass a string to enable TUS upload mode. Pass false or omit for direct S3 upload. | | debug | boolean | No | false | Enable debug logging to console. |

The constructor throws an Error if baseURL or apiKey are missing or invalid.

Base URL normalization: The constructor accepts flexible URL formats. All of the following are equivalent:

new Uploader('https://api0.superyachtapi.com', key)
new Uploader('https://api0.superyachtapi.com/', key)
new Uploader('https://api0.superyachtapi.com/api', key)
new Uploader('https://api0.superyachtapi.com/api/', key)
new Uploader('https://api0.superyachtapi.com/api/uploads', key)
new Uploader('https://api0.superyachtapi.com/api/uploads/', key)

Options form

The constructor also accepts a single options object (detected by typeof first arg === 'object'), which makes the API contract selectable:

// legacy contract (identical behavior to the positional form):
new Uploader({ baseURL, apiKey, onProgress, onUploaded, transloadit, debug })

// v2 contract:
new Uploader({ mode: 'v2', baseURL, token, visibility, onProgress, onUploaded, debug })

The positional form always uses the legacy contract and is fully backwards compatible.

v2 API Mode

v2 mode targets the v2-api File API (POST /v1/files etc.) instead of the legacy /api/uploads endpoints. Key differences:

  • Auth uses a v2-api OAuth user token or service token — not the legacy DB api key.
  • The server owns file status (pendinguploaded / failed / expired / deleted); the client never writes it.
  • Presigned PUT headers returned by the server are echoed verbatim — they are part of the S3 signature.
  • After the PUT, the client confirms the upload (POST /v1/files/{id}/uploaded) and receives server-verified byte_size, etag, and content_type.
  • Transloadit/TUS is not supported in v2 mode (passing transloadit throws).
  • Single PUT upload, capped server-side (FILES_MAX_UPLOAD_BYTES, 100 MiB on staging) — an over-cap file is rejected at create time (413) before any bytes move.
  • byte_size is required at create time and is signed into the presigned PUT (the returned headers include a signed Content-Length). The declared size must equal the exact byte length of the PUT body — no tolerance. The adapter declares the actual body length automatically; a wrong-sized PUT fails at S3 with 403 and stores nothing.

v2 Quick Start

import Uploader from '@superyachttimes/lib-uploader'

const uploader = new Uploader({
  mode: 'v2',
  baseURL: 'https://api-staging.superyachttimes.com', // v2-api host — configure per environment
  token: myOAuthOrServiceToken,
  visibility: 'private', // default
  onUploaded: ({ id, file }) => console.log('confirmed:', file.status), // 'uploaded'
})

const { id, file } = await uploader.upload(input.files[0], 'user-123', 'sightings')
// file.status === 'uploaded', file.byte_size / file.etag are server-verified

v2 Constructor Options

| Option | Type | Required | Default | Description | |---|---|---|---|---| | mode | 'legacy' \| 'v2' | No | 'legacy' | API contract to use. | | baseURL | string | Yes | — | v2-api host (e.g. https://api-staging.superyachttimes.com). No path suffix is added. | | token | string | Yes (v2) | — | v2-api OAuth user token or service token. (apiKey is accepted as an alias.) | | visibility | 'public' \| 'private' | No | 'private' | Visibility for created files. | | onProgress | OnProgress | No | () => {} | Progress callback. | | onUploaded | OnUploaded | No | () => {} | Called with the server-confirmed file. | | transloadit | — | Invalid | — | Throws in v2 mode. Transloadit stays legacy-only. | | debug | boolean | No | false | Debug logging. |

v2 Upload Flow

upload(file, owner, service) in v2 mode:

  1. POST /v1/files — creates the record (status pending) and returns a short-lived presigned URL + headers (owner is only honored for service tokens; service defaults server-side from the token). The adapter declares byte_size from the actual upload body (the buffer's byte length for NodeJsFile, File.size for browser files), guaranteeing it matches the PUT. Over the server cap → rejects with code: 'file_too_large' (413) before any upload starts.
  2. PUT the file bytes to the presigned URL with the returned headers echoed verbatim. The headers include a signed Content-Length; browsers silently drop it and recompute it from the body — expected and correct, since the body is the declared size. Single PUT, no multipart.
  3. PATCH /v1/files/{id} with { upload_progress: 100 } (cosmetic — failures, including network errors, are swallowed).
  4. POST /v1/files/{id}/uploaded — confirm. Retries briefly on 409 (S3 eventual consistency), then resolves with the confirmed file resource.
  5. Callbacks fire: onUploaded({ id, file, result }) and onProgress(1, size, size, file)file and result are both the server-confirmed resource.

On PUT failure the promise rejects with a RequestError; no status is written — the server owns status, and its S3 event processing (or the record's expires_at) settles the outcome. A PUT 403 means S3 refused the signature — in practice: the body's size no longer matches the declared byte_size (the file was re-picked or edited after presign). The error carries code: 'signature_mismatch'; nothing was stored and the URL can never succeed — create a new file record with the fresh file instead of retrying.

If the client dies before confirming, the record is still flipped server-side by S3 events — confirming just gives immediate consistency.

v2 Method Reference

All public methods keep their legacy signatures and route to the v2 endpoints:

| Method | v2 behavior | Returns | |---|---|---| | upload(f, owner, service) | Full flow above. | { id, file, result }file/result are the confirmed file resource. | | getPresignedURL(f, owner, service) | POST /v1/files. | { url, file, id, headers }headers is new in v2 and must be sent verbatim on your own PUT. file is the pending resource. | | updateFileData(id, data) | Maps the legacy update shape onto v2 — see below. | { file } (matching the legacy PATCH response shape). | | getFileData(id) | GET /v1/files/{id}. | The file resource. | | getFiles(owner, service?) | GET /v1/files?service=…&owner=… (owner sent only when non-empty; only honored for service tokens — user tokens are always scoped to the caller). | Array of file resources (paged server-side). | | downloadUrl(id) | Builds the download endpoint URL — no request made. | string | | uploadWithTus(...), createAssembly(...), startUpload() | Legacy-only. uploadWithTus throws in v2 mode; Transloadit has no v2 equivalent. | — |

updateFileData mapping — the server owns everything except display progress, so legacy-style updates translate instead of passing through:

  • { progress: 0.42 }PATCH /v1/files/{id} with { upload_progress: 42 }. The value is a 0–1 fraction like in legacy mode (values > 1 are treated as already-percent and clamped to 0–100). Rejections are swallowed — the server refuses progress writes once the file is no longer pending, and that's fine.
  • { status: 'UPLOAD_DONE' }POST /v1/files/{id}/uploaded (the confirm call). This is the v2 translation of the legacy "mark it done" write.
  • Every other field (status values other than UPLOAD_DONE, s3Bucket, variants, …) is ignored — those are server-verified in v2.

Manual Uploads (Bring Your Own PUT)

If you need real upload progress or a platform uploader (Expo, XHR), do the PUT yourself — this is the same pattern syt-sightings-app uses in legacy mode, unchanged except that you must forward headers:

const uploader = new Uploader({ mode: 'v2', baseURL, token })

// 1. Create the record + get the presigned URL. `headers` are part of the S3
//    signature — send them on the PUT exactly as received.
const { url, headers, id } = await uploader.getPresignedURL(file, 'user-123', 'sightings')

// 2. Upload however you like (XMLHttpRequest shown for progress events).
await new Promise<void>((resolve, reject) => {
  const xhr = new XMLHttpRequest()
  xhr.open('PUT', url)
  for (const [name, value] of Object.entries(headers ?? {})) xhr.setRequestHeader(name, value)
  xhr.upload.onprogress = (e) => uploader.updateFileData(id, { progress: e.loaded / e.total })
  xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error(`PUT failed: ${xhr.status}`)))
  xhr.onerror = () => reject(new Error('PUT failed'))
  xhr.send(file)
})

// 3. Confirm — flips the record to 'uploaded' with server-verified byte_size/etag.
const { file: confirmed } = await uploader.updateFileData(id, { status: 'UPLOAD_DONE' })

In Expo/React Native, step 2 is FileSystem.createUploadTask(url, fileUri, { httpMethod: 'PUT', headers }) — again forwarding headers verbatim.

Skipping step 3 is safe (S3 events settle the record eventually), but confirming gives you the verified resource immediately.

Size invariant (contract since 2026-07-15): the presign signs the declared byte_size (the returned headers include a signed Content-Length), so the body you PUT must be byte-identical in length to the file you passed to getPresignedURL. In browsers Content-Length is a forbidden header — fetch/XHR drop your value and compute it from the body, which is correct as long as the body is the declared size. In Node (undici), don't send the header alongside a body of a different length (client-side RequestContentLengthMismatchError) — omit it or keep it matching. If the user re-picks or edits the file after presign, the PUT fails at S3 with 403 (nothing stored) — call getPresignedURL again with the fresh file; never retry the old URL.

Progress Reporting in v2

  • uploader.upload() uses fetch for the PUT, which exposes no upload-progress events — so onProgress fires once, at completion (progress = 1), exactly like legacy direct-upload mode. For live progress, use the manual pattern.
  • Progress values passed to updateFileData are 0–1 fractions (legacy convention); the adapter converts to the integer percent (upload_progress: 0–100) the v2 API expects.
  • Progress is display-only: the server never trusts it for facts, rejects it once the file leaves pending, and the adapter swallows those rejections (and network failures) silently.

The v2 File Resource

v2 records look like this (exported as the V2FileResource type):

{
  id: '3f6c…',                          // UUID — also mirrored onto _id for legacy-shaped consumers
  status: 'uploaded',                   // 'pending' | 'uploaded' | 'failed' | 'expired' | 'deleted'
  service: 'sightings',
  visibility: 'private',                // 'public' | 'private'
  declared_filename: 'brochure.pdf',    // as sent by the client
  content_type: 'application/pdf',      // server-verified once uploaded
  byte_size: 1048576,                   // server-verified once uploaded
  etag: '9b2cf5…',                      // from S3
  upload_progress: 100,                 // cosmetic, client-reported
  variants: [],                         // processed derivatives: { kind, content_type, byte_size }
  uploaded_by: { kind: 'user', user_id: '…' },
  expires_at: '…', uploaded_at: '…', created_at: '…', updated_at: '…'
}

Notes:

  • content_type, byte_size, and etag are read back from S3 at confirm time — they are trustworthy, unlike the client-declared values.
  • variants replaces the legacy media.yachteye.com URL building: in v2, processed derivatives arrive on the resource itself. The legacy createVariants() URL scheme does not apply.
  • Every resource returned by this library has _id mirrored from id, so code written against the legacy FileData._id keeps working.

v2 File Statuses

Server-owned status enum — the client never writes it:

pending ──(PUT + confirm, or S3 event)──▶ uploaded
pending ──(failure recorded server-side)─▶ failed
pending ──(presign expired, never PUT)──▶ expired
any ─────(DELETE /v1/files/{id})────────▶ deleted

The legacy UPLOAD_* strings are never sent in v2 mode; updateFileData(id, { status: 'UPLOAD_DONE' }) is translated to the confirm call and all other status writes are ignored.

downloadUrl(id)

Returns the v2 download endpoint URL — {baseURL}/v1/files/{id}/download — without making a request. The endpoint answers with a 302 to a short-lived presigned S3 GET, so hand the URL to <a href>, <img src>, or fetch (which follow redirects) rather than treating it as the file itself.

  • Public files: no auth needed — the URL works bare.
  • Private files: the request needs an owner/admin bearer token, so plain <a href> won't work for those; fetch it with an Authorization header instead.
  • Throws in legacy mode (there is no legacy download endpoint).

v2 Error Handling & Retries

All v2 failures throw the same RequestError shape as legacy mode (name, status, statusText, url, response properties), with these messages: Failed to create file, Failed to upload file (the S3 PUT), Failed to confirm upload, Failed to retrieve file, Failed get files for owner ….

Errors tied to the byte-size contract additionally carry a machine-readable code (exported as the UploadErrorCode type) so UIs can react without string-matching, plus detail (the server's problem-details detail string) where available:

| code | When | Recover by | |---|---|---| | invalid_byte_size | Thrown client-side, before any request: the upload body's byte length isn't a positive integer (e.g. empty file, missing buffer). | Fix the file input. | | file_too_large | Create returned 413 — the file exceeds the server upload cap. Nothing was uploaded. | Show a "file too large" message; pick a smaller file. | | signature_mismatch | The S3 PUT returned 403 — the body's size no longer matches the declared byte_size (file re-picked/edited after presign). Nothing stored; the record stays pending and eventually expires. | Create a new file record with the fresh file. Never retry the old URL. | | size_mismatch | Confirm returned 409 size_mismatch — the stored object contradicts the declaration. The record is now failed and the object deleted server-side. Unreachable if you uphold the size invariant. | Create a new file record. Not retried by the adapter. | | upload_not_found | Confirm exhausted its retries with 409 upload_not_found — the object never became visible in S3. | Usually means the PUT failed or never ran; upload again. |

Special cases:

  • Confirm 409 upload_not_found — the S3 object isn't visible yet. The adapter retries up to 3 times (500 ms / 1 s / 2 s delays, configurable via V2Contract) before throwing. 409 size_mismatch is never retried (see table). Any other non-2xx throws immediately. Confirm is idempotent — calling it again after a success is harmless.
  • Progress PATCH failures — swallowed entirely (HTTP rejections and network errors); they never fail an upload.
  • PUT failures — thrown as-is with no compensating write. Don't retry the same presigned URL after expires_at or a 403; create a new record instead.

Using V2Contract Directly

The Uploader class delegates all HTTP to a contract adapter, and both adapters are exported if you need lower-level access:

import { V2Contract, type V2ContractOptions, type FileApiContract } from '@superyachttimes/lib-uploader'

const contract = new V2Contract(baseURL, token, {
  visibility: 'public',
  confirmRetryDelays: [250, 500, 1000, 2000], // override the 409 retry schedule
})

const { id, uploadUrl, uploadHeaders, file } = await contract.createUpload(f, owner, service)
// ... your own PUT ...
const confirmed = await contract.confirm(id)

V2Contract implements the exported FileApiContract interface (createUpload, updateFile, patchProgress, confirm, getFile, listFiles, downloadUrl). LegacyContract is exported too. Most consumers should stick to Uploader — the contracts are the extension seam, not the primary API.

Migrating from Legacy to v2

| | Legacy | v2 | |---|---|---| | Construction | new Uploader(baseURL, apiKey, …) | new Uploader({ mode: 'v2', baseURL, token, … }) | | Base URL | Any form; normalized to …/api/uploads/ | Bare v2-api host (e.g. https://api-staging.superyachttimes.com) | | Auth | Legacy DB api key | v2-api OAuth user token or service token — the legacy key does not work | | Statuses | Client-written UPLOAD_* strings | Server-owned pending/uploaded/failed/expired/deleted — read, never write | | Record id | file._id | file.id (with _id mirrored for compatibility) | | Media processing | Transloadit/TUS + media.yachteye.com variant URLs | Not supported client-side; derivatives arrive in file.variants | | Marking done | updateFileData(id, { status: 'UPLOAD_DONE' }) | Same call works — translated to the confirm endpoint | | Progress writes | updateFileData(id, { progress }) | Same call works — translated to upload_progress (and safely ignored after pending) |

Steps:

  1. Switch the constructor to the options form with mode: 'v2', the v2-api host, and a v2 bearer token.
  2. Remove any transloadit usage (v2 throws on it).
  3. If you do your own PUT (sightings-app pattern): forward the new headers from getPresignedURL verbatim — this is the only required code change in that flow.
  4. Stop writing statuses other than the UPLOAD_DONE compatibility mapping; read file.status instead.
  5. Prefer file.id over file._id in new code.

API Reference

upload(file, owner, service)

The primary upload method. Automatically delegates to direct S3 upload or TUS upload based on the constructor configuration.

const result = await uploader.upload(file, owner, service)

| Parameter | Type | Description | |---|---|---| | file | File \| NodeJsFile | The file to upload. | | owner | string | Owner identifier (e.g. user ID, yacht ID). | | service | string | Service name (e.g. "yachteye", "sytiq"). |

Returns: Promise<{ id: string, file: FileData | undefined, result: object }>

  • id — The file record ID from the SYT API.
  • file — The updated file data from the server (or undefined if retrieval failed).
  • result — The raw upload response.

uploadWithTus(file, owner, service)

Explicitly uploads using the TUS protocol via Transloadit. Called automatically by upload() when Transloadit is configured, but can also be called directly.

const result = await uploader.uploadWithTus(file, owner, service)

Parameters and return type are the same as upload(). The return value also includes result.variants — an array of processed media variant URLs.

Throws an error if the file type is not image, video, or pdf.

getFiles(owner, service?)

Retrieves a list of files uploaded by a specific owner.

const files = await uploader.getFiles('owner-id')
const files = await uploader.getFiles('owner-id', 'yachteye')

| Parameter | Type | Description | |---|---|---| | owner | string | Owner identifier. | | service | string (optional) | Filter by service name. |

Returns: Promise<object> — The JSON response from the API.

getFileData(id)

Retrieves the file record for a specific file ID.

const fileData = await uploader.getFileData('file-id')

| Parameter | Type | Description | |---|---|---| | id | string | The file record ID. |

Returns: Promise<object> — The file data from the API.

updateFileData(id, data)

Updates a file record with partial data.

const result = await uploader.updateFileData('file-id', { status: 'UPLOAD_DONE', progress: 1 })

| Parameter | Type | Description | |---|---|---| | id | string | The file record ID. | | data | FileDataUpdate | Partial file data to update. |

Returns: Promise<object> — The updated file data from the API.

getPresignedURL(file, owner, service)

Requests a pre-signed S3 upload URL from the SYT API. Also creates the file record on the server.

const { url, file, id } = await uploader.getPresignedURL(file, owner, service)

| Parameter | Type | Description | |---|---|---| | file | File \| NodeJsFile | The file to get an upload URL for. | | owner | string | Owner identifier. | | service | string | Service name. |

Returns: Promise<{ url: string, file: FileData, id: string }>

createAssembly(template_id, fields)

Creates a Transloadit assembly for media processing.

const assembly = await uploader.createAssembly('template-id', { fileId: 'abc', owner: 'xyz', service: 'yachteye' })

| Parameter | Type | Description | |---|---|---| | template_id | string | Transloadit template ID. | | fields | Record<string, string \| number> | Custom fields passed to the template. |

Returns: Promise<object> — The Transloadit assembly response including tus_url and assembly_ssl_url.

startUpload()

Starts (or resumes) the current TUS upload. Called automatically by uploadWithTus(), but can be called manually if needed.

await uploader.startUpload()

log(...args)

Logs messages to the console when debug mode is enabled. All log messages are prefixed with [Uploader].

uploader.log('Custom message', { data: 'value' })

Types

FileData

Represents a file record from the SYT API.

interface FileData {
  _id: string
  status: string
  filename: string
  contentType: string
  s3Bucket: string
  s3Key: string
  extension: string
  uploadedAt: number
  createdAt: number
  updatedAt: number
  progress: number
  variants: any[]
}

FileDataUpdate

A partial version of FileData for update operations.

type FileDataUpdate = Partial<FileData>

OnProgress

Progress callback signature.

type OnProgress = (
  progress: number,       // 0 to 1
  completedBytes: number,
  totalBytes: number,
  fileData?: FileData     // present when server update succeeds
) => void

OnUploaded

Upload complete callback signature.

type OnUploaded = (data: { id: string, file: any, result: any }) => void

NodeJsFile

File wrapper for Node.js environments where the native File API is not available.

interface NodeJsFile {
  name: string
  size: number
  type: string
  buffer: any
  isNodeJsFile: boolean  // must be true
}

Node.js Usage

In Node.js, use the NodeJsFile interface to wrap file buffers:

import Uploader from '@superyachttimes/lib-uploader'
import { promises as fs } from 'fs'

const buffer = await fs.readFile('./photo.jpg')

const file = {
  isNodeJsFile: true,
  buffer,
  name: 'photo.jpg',
  type: 'image/jpeg',
  size: buffer.length,
}

const uploader = new Uploader(
  'https://api0.superyachtapi.com',
  process.env.SYT_API_KEY,
  (progress) => console.log(`${(progress * 100).toFixed(1)}%`),
  ({ id }) => console.log('Done:', id),
  process.env.TRANSLOADIT_API_KEY
)

await uploader.upload(file, 'owner-id', 'yachteye')

Supported File Types

When using TUS/Transloadit mode, the library supports three file categories, each mapped to a dedicated Transloadit processing template:

| Category | Detected by contentType containing | Processing | |---|---|---| | Image | image | Compressed to WebP | | Video | video | Transcoded to MP4 + HLS adaptive streaming | | PDF | pdf | Stored as original |

Files that don't match any category will throw an error in TUS mode. Direct S3 upload mode accepts any file type.

Media Variants

After a TUS upload completes, the library generates variant URLs on media.yachteye.com:

Images:

  • https://media.yachteye.com/photo/{id}/compressed/{id}.webp — Compressed WebP

Videos:

  • https://media.yachteye.com/video/{id}/mp4/{id}.mp4 — Transcoded MP4
  • https://media.yachteye.com/video/{id}/stream/adaptive.m3u8 — HLS adaptive stream

PDFs:

  • https://media.yachteye.com/document/{id}/orig/{id}.pdf — Original PDF

File Status Lifecycle

Files go through the following statuses during upload:

(created) → UPLOAD_IN_PROGRESS → UPLOAD_DONE          (direct upload)
(created) → UPLOAD_IN_PROGRESS → UPLOAD_PROCESSING    (TUS upload)
(created) → UPLOAD_IN_PROGRESS → UPLOAD_FAILED        (TUS error)
(created) → UPLOAD_IN_PROGRESS → FAILED_UPLOAD        (direct upload error)

Error Handling

All methods throw errors on failure. Errors from API calls include additional properties:

try {
  await uploader.upload(file, owner, service)
} catch (err) {
  console.error(err.message)    // Human-readable error message
  console.error(err.name)       // 'RequestError' for API errors
  console.error(err.status)     // HTTP status code
  console.error(err.statusText) // HTTP status text
  console.error(err.url)        // The URL that was called
  console.error(err.response)   // The raw Response object
}

TUS uploads have built-in retry logic with delays of [0, 300, 1000, 3000, 5000, 10000] ms before giving up.

Debug Logging

Enable debug logging by passing true as the last constructor parameter:

const uploader = new Uploader(baseURL, apiKey, onProgress, onUploaded, false, true)

All debug messages are prefixed with [Uploader] and logged via console.log.

Development

# Install dependencies
npm install

# Build (outputs CJS + ESM + type declarations to dist/)
npm run build

# Run unit tests (vitest, mocked fetch — no credentials needed)
npm test

# Run the manual integration test (requires environment variables)
export TRANSLOADIT_API_KEY=your_key
export SYT_API_KEY=your_key
npm run test:integration

# Build and publish to npm manually (fallback — normal releases go through CI, see below)
npm run registry

Releasing

Releases are published to npm by GitHub Actions (.github/workflows/publish.yml) using npm trusted publishing (OIDC). There are no npm tokens or secrets in the repo or in GitHub settings — the workflow authenticates by exchanging a GitHub OIDC token (the id-token: write job permission) against the trusted-publisher configuration on npmjs.com. Provenance attestations are generated automatically.

What the workflow does:

  • PR to main → verify only: npm ci, tests, build, and the version gate — the package.json version must be strictly higher than the version currently published on npm, otherwise CI fails.
  • Push to main → same verification, then npm publish --access public via OIDC.

Cutting a release:

git checkout development
npm version patch --no-git-tag-version   # or minor / major — edits package.json only
git commit -am "chore(release): v<version>"
git push origin development

# release = fast-forward main to development (or merge a PR)
git checkout main
git merge --ff-only development
git push origin main                     # ← this push triggers the publish

Credentials/trusted-publisher configuration (one-time, configured 2026-07-14; repeat only if it's ever removed): on npmjs.com → @superyachttimes/lib-uploader → Settings → Trusted Publisher → GitHub Actions, with organization SuperYachtTimes, repository lib-uploader, workflow filename publish.yml, environment left empty. With that in place, npm tokens can be disallowed entirely in the package's publishing-access settings.

Manual fallback (requires npm login and an OTP): npm run registry.