@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
- Quick Start
- Upload Modes
- Constructor
- v2 API Mode
- API Reference
- Types
- Node.js Usage
- Supported File Types
- Media Variants
- File Status Lifecycle
- Error Handling
- Debug Logging
- Development
Installation
npm install @superyachttimes/lib-uploaderQuick 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:
- Requests a pre-signed S3 URL from the SYT API
- PUTs the file directly to S3
- 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:
- Requests a file record from the SYT API
- Creates a Transloadit assembly for the appropriate file type (image/video/pdf)
- Uploads the file using the TUS resumable upload protocol
- Transloadit processes the file (transcoding, compression, etc.)
- 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 (
pending→uploaded/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-verifiedbyte_size,etag, andcontent_type. - Transloadit/TUS is not supported in v2 mode (passing
transloaditthrows). - 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_sizeis required at create time and is signed into the presigned PUT (the returned headers include a signedContent-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-verifiedv2 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:
POST /v1/files— creates the record (statuspending) and returns a short-lived presigned URL + headers (owneris only honored for service tokens;servicedefaults server-side from the token). The adapter declaresbyte_sizefrom the actual upload body (the buffer's byte length forNodeJsFile,File.sizefor browser files), guaranteeing it matches the PUT. Over the server cap → rejects withcode: 'file_too_large'(413) before any upload starts.PUTthe file bytes to the presigned URL with the returned headers echoed verbatim. The headers include a signedContent-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.PATCH /v1/files/{id}with{ upload_progress: 100 }(cosmetic — failures, including network errors, are swallowed).POST /v1/files/{id}/uploaded— confirm. Retries briefly on 409 (S3 eventual consistency), then resolves with the confirmed file resource.- Callbacks fire:
onUploaded({ id, file, result })andonProgress(1, size, size, file)—fileandresultare 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 longerpending, 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 (
statusvalues other thanUPLOAD_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()usesfetchfor the PUT, which exposes no upload-progress events — soonProgressfires once, at completion (progress = 1), exactly like legacy direct-upload mode. For live progress, use the manual pattern.- Progress values passed to
updateFileDataare 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, andetagare read back from S3 at confirm time — they are trustworthy, unlike the client-declared values.variantsreplaces the legacymedia.yachteye.comURL building: in v2, processed derivatives arrive on the resource itself. The legacycreateVariants()URL scheme does not apply.- Every resource returned by this library has
_idmirrored fromid, so code written against the legacyFileData._idkeeps 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})────────▶ deletedThe 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 anAuthorizationheader 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 viaV2Contract) before throwing. 409size_mismatchis 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_ator 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:
- Switch the constructor to the options form with
mode: 'v2', the v2-api host, and a v2 bearer token. - Remove any
transloaditusage (v2 throws on it). - If you do your own PUT (sightings-app pattern): forward the new
headersfromgetPresignedURLverbatim — this is the only required code change in that flow. - Stop writing statuses other than the
UPLOAD_DONEcompatibility mapping; readfile.statusinstead. - Prefer
file.idoverfile._idin 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 (orundefinedif 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
) => voidOnUploaded
Upload complete callback signature.
type OnUploaded = (data: { id: string, file: any, result: any }) => voidNodeJsFile
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 MP4https://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 registryReleasing
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 — thepackage.jsonversion must be strictly higher than the version currently published on npm, otherwise CI fails. - Push to
main→ same verification, thennpm publish --access publicvia 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 publishCredentials/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.
