@tinify-dev/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `BatchOperation` now includes `"convert"`, and `CreateBatchParams.options`
13
+ accepts `ConvertOptions` (`format`: `avif` | `webp` | `jpeg` | `png`, plus
14
+ optional `quality_mode`) — matching the server's `CreateBatch` schema.
15
+ Compress batch options take `target_size_bytes` (already part of
16
+ `CompressOptions`).
17
+ - `ImageResultData.target_size_achieved?: boolean` — present only when
18
+ `compress` ran with `target_size_bytes`.
19
+
20
+ ## [0.1.0] - 2026-07-24
21
+
22
+ ### Added
23
+
24
+ - Initial release of `@tinify-dev/client`.
25
+ - `TinifyClient` with `compress`, `resize`, `crop`, `convert` (beta), `usage`, and `download`.
26
+ - Durable batch API: `createBatch`, `uploadBatchFiles`, `commitBatch`, `getBatch`, `cancelBatch`, `waitForBatch`, `downloadBatchArchive`.
27
+ - Automatic idempotency keys (reused across retries) with per-call override.
28
+ - Retries for 429/503/network failures honoring `Retry-After`, with exponential backoff and full jitter.
29
+ - Typed error envelope: `TinifyApiError` (`status`, `code`, `requestId`, `details`, `retryAfter`), `TinifyNetworkError`, `TinifyTimeoutError`.
30
+ - `tinify-dev` CLI: `compress`, `resize`, `crop`, `convert`, `usage`.
31
+ - Dual ESM + CJS build with bundled type declarations. Zero runtime dependencies.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stian Larsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # @tinify-dev/client
2
+
3
+ Zero-dependency TypeScript client for the [Tinify.dev](https://tinify.dev/developers) image API: compress, resize, crop, convert, durable batches, and account usage.
4
+
5
+ - **Zero runtime dependencies** — built on the platform `fetch`, `FormData`, and `Blob`.
6
+ - **Node.js >= 20**, dual ESM + CJS, fully typed.
7
+ - **Honest results** — compression never returns more bytes than you sent; when the API cannot shrink a file it says so via `optimized: false`. (Resize, crop, and conversion report byte changes plainly — conversion can legitimately grow a file.)
8
+ - **Safe by default** — automatic idempotency keys, retries with `Retry-After` support, typed error codes with `request_id` for support.
9
+
10
+ > Not affiliated with TinyPNG's `tinify` package. This client talks to the Tinify.dev API.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @tinify-dev/client
16
+ ```
17
+
18
+ ## Quickstart
19
+
20
+ ```ts
21
+ import { TinifyClient } from "@tinify-dev/client";
22
+
23
+ const client = new TinifyClient({ apiKey: process.env.TINIFY_API_KEY });
24
+
25
+ const result = await client.compress("./photo.png", { quality_mode: "balanced" });
26
+ console.log(result.data.original_bytes, "->", result.data.result_bytes);
27
+
28
+ if (result.data.optimized === false) {
29
+ console.log("Already as small as it gets — the original bytes were returned.");
30
+ }
31
+
32
+ const blob = await client.download(result);
33
+ await fs.writeFile("./photo.min.png", new Uint8Array(await blob.arrayBuffer()));
34
+ ```
35
+
36
+ CJS works too:
37
+
38
+ ```js
39
+ const { TinifyClient } = require("@tinify-dev/client");
40
+ ```
41
+
42
+ ## Authentication
43
+
44
+ Every call needs an API key (`tnf_live_*` for production, `tnf_test_*` for development), created at [tinify.dev/developers](https://tinify.dev/developers). Pass it as `apiKey` or set the `TINIFY_API_KEY` environment variable. Keys are sent as `Authorization: Bearer <key>`.
45
+
46
+ ```ts
47
+ new TinifyClient({
48
+ apiKey: "tnf_live_...", // default: process.env.TINIFY_API_KEY
49
+ baseUrl: "https://api.tinify.dev", // default: process.env.TINIFY_BASE_URL or this value
50
+ maxRetries: 3, // retries after the first attempt (429/503/network)
51
+ timeoutMs: 60_000, // per-attempt timeout
52
+ fetch: customFetch, // injectable, e.g. for proxies or tests
53
+ });
54
+ ```
55
+
56
+ ## Inputs
57
+
58
+ All image methods accept a file path (Node.js only), `Buffer`, `Uint8Array`, `ArrayBuffer`, `Blob`, or `File`.
59
+
60
+ ## Methods
61
+
62
+ | Method | Endpoint | Notes |
63
+ | --- | --- | --- |
64
+ | `compress(input, { quality_mode?, target_size_bytes? })` | `POST /api/v1/images/compress` | `quality_mode`: `balanced` (default), `best_quality`, `lossless`. `target_size_bytes` is **beta** (server rollout). |
65
+ | `resize(input, { width?, height?, scale?, keep_aspect_ratio?, optimize? })` | `POST /api/v1/images/resize` | At least one of width/height/scale. |
66
+ | `crop(input, { x, y, width, height })` | `POST /api/v1/images/crop` | |
67
+ | `convert(input, { format, quality_mode? })` | `POST /api/v1/images/convert` | **Beta** (server rollout). `format`: `avif`, `webp`, `jpeg`, `png`. |
68
+ | `usage()` | `GET /api/v1/usage` | `{ plan, period_start, period_end, included, reserved, used, remaining }`. |
69
+ | `download(resultOrUrl)` | result `download_url` | Returns a `Blob`. URLs are unauthenticated and expire after **2 hours**. |
70
+
71
+ Every call resolves to `{ data, requestId, rateLimit }`:
72
+
73
+ ```ts
74
+ const { data, requestId, rateLimit } = await client.compress(input);
75
+ // rateLimit: { limit, remaining, reset } from the X-RateLimit-* headers (or null)
76
+ // requestId: quote this when contacting support
77
+ ```
78
+
79
+ `lossless` is rejected for JPEG inputs (`lossless_not_supported`) because JPEG has no pixel-preserving lossless mode — the API refuses to pretend otherwise.
80
+
81
+ When `compress` runs with `target_size_bytes`, the result includes `target_size_achieved`: `true` when the output met the byte budget, `false` when it could not. The field is absent otherwise.
82
+
83
+ ## Batch lifecycle
84
+
85
+ Batches (Developer/Pro plans) process up to **200 files** of up to **40 MB** each (JPEG, PNG, WebP, AVIF) durably on the server. The `operation` is `compress`, `resize`, `crop`, or `convert`, and `options` mirror the synchronous endpoints — `convert` requires `format` (`avif`, `webp`, `jpeg`, `png`; files already in the target format fail per job with `same_format_conversion`), and `compress` accepts `quality_mode` plus `target_size_bytes` (skipped per job when it is not smaller than that job's input):
86
+
87
+ ```text
88
+ createBatch(manifest) 201 status: awaiting_upload
89
+ | returns uploads[]: { file_id, client_id, upload_url, headers }
90
+ v
91
+ uploadBatchFiles(session, files) PUT raw bytes to each presigned upload_url
92
+ | (exact headers passed through, NO Authorization header)
93
+ v
94
+ commitBatch(id) 202 status: queued
95
+ |
96
+ waitForBatch(id) polls getBatch(id): queued -> processing -> terminal
97
+ | terminal: succeeded | partially_succeeded | failed
98
+ | | canceled | expired
99
+ v
100
+ downloadBatchArchive(id) 200 ZIP of the successful results
101
+ ```
102
+
103
+ ```ts
104
+ const session = await client.createBatch({
105
+ operation: "compress",
106
+ options: { quality_mode: "balanced" },
107
+ files: [
108
+ { client_id: "hero", filename: "hero.png", size_bytes: 812_331, content_type: "image/png" },
109
+ { client_id: "logo", filename: "logo.jpg", size_bytes: 41_022, content_type: "image/jpeg" },
110
+ ],
111
+ });
112
+
113
+ await client.uploadBatchFiles(session.data, {
114
+ hero: "./hero.png",
115
+ logo: "./logo.jpg",
116
+ }); // concurrency 4 by default
117
+
118
+ await client.commitBatch(session.data.id);
119
+ const finished = await client.waitForBatch(session.data.id); // 1s polls growing x1.5, cap 10s, 10 min budget
120
+
121
+ for (const file of finished.data.files) {
122
+ console.log(file.client_id, file.status, file.original_bytes, "->", file.result_bytes);
123
+ }
124
+ const zip = await client.downloadBatchArchive(session.data.id);
125
+ ```
126
+
127
+ Notes:
128
+
129
+ - `size_bytes` and `content_type` in the manifest must exactly match the bytes you upload — mismatches fail the commit with `uploads_incomplete` (the offending `client_id`s are listed in `error.details.client_ids`).
130
+ - Each `client_id` must be unique (`duplicate_client_id`).
131
+ - Batch download URLs and archives follow the same **2-hour expiry** as synchronous results.
132
+ - `cancelBatch` is idempotent; canceling a finished batch is a no-op.
133
+
134
+ ## Retries and idempotency
135
+
136
+ - Every mutating call automatically sends an `Idempotency-Key` header (a fresh UUID per logical call, **reused across retries** of that call). Override it with `{ idempotencyKey: "your-key" }` (8–160 chars) to make your own retries safe across process restarts.
137
+ - The client retries **only** 429, 503, and network failures — never any other 4xx/5xx.
138
+ - `Retry-After` (seconds or HTTP-date) is honored when present; otherwise exponential backoff with full jitter: `random(0, min(8s, 500ms * 2^attempt))`.
139
+ - Reusing an idempotency key with a *different* request body yields `idempotency_conflict` (409).
140
+
141
+ ## Errors
142
+
143
+ All API failures throw `TinifyApiError` with `status`, `code`, `message`, `requestId`, `details`, and (on 429/503) `retryAfter` seconds. Transport failures throw `TinifyNetworkError`; per-attempt timeouts and exhausted `waitForBatch` budgets throw `TinifyTimeoutError`. All extend `TinifyError`.
144
+
145
+ ```ts
146
+ import { TinifyApiError } from "@tinify-dev/client";
147
+
148
+ try {
149
+ await client.compress(input);
150
+ } catch (error) {
151
+ if (error instanceof TinifyApiError) {
152
+ console.error(error.code, error.status, error.requestId);
153
+ }
154
+ }
155
+ ```
156
+
157
+ ### Error codes
158
+
159
+ | Code | HTTP | Meaning |
160
+ | --- | --- | --- |
161
+ | `missing_authorization` | 401 | No `Authorization: Bearer <token>` header. |
162
+ | `invalid_api_key` | 401 | The API key is invalid or revoked. |
163
+ | `missing_identity` | 401 | The request is not authenticated. |
164
+ | `insufficient_scope` | 403 | The key lacks the required scope. |
165
+ | `account_unavailable` | 403 | The account is unavailable (e.g. on hold). |
166
+ | `batch_plan_required` | 403 | Batches require the Developer or Pro plan. |
167
+ | `invalid_request` | 400 | Malformed body or parameters. |
168
+ | `invalid_idempotency_key` | 400 | Missing or malformed `Idempotency-Key` (8–160 chars). |
169
+ | `batch_not_found` | 404 | Unknown batch id (or not yours). |
170
+ | `idempotency_conflict` | 409 | Key reused with a different request body. |
171
+ | `batch_not_complete` | 409 | Archive requested before the batch finished. |
172
+ | `batch_has_no_results` | 409 | The batch finished without any successful file. |
173
+ | `file_too_large` | 413 | Image exceeds the 40 MB limit. |
174
+ | `unsupported_media_type` | 415 | API v1 supports AVIF, WebP, JPEG, and PNG. |
175
+ | `validation_failed` | 422 | One or more request fields are invalid. |
176
+ | `invalid_quality_mode` | 422 | Quality mode must be `balanced`, `best_quality`, or `lossless`. |
177
+ | `lossless_not_supported` | 422 | JPEG has no pixel-preserving lossless mode. |
178
+ | `invalid_resize` | 422 | Resize needs a positive width, height, or scale. |
179
+ | `invalid_crop` | 422 | Crop needs non-negative x/y and positive width/height. |
180
+ | `invalid_operation` | 422 | Batch operation must be compress, resize, crop, or convert. |
181
+ | `batch_too_large` | 422 | More files than your plan's per-batch limit. |
182
+ | `duplicate_client_id` | 422 | Every batch file needs a unique `client_id`. |
183
+ | `uploads_incomplete` | 422 | Uploaded objects missing or mismatching the manifest. |
184
+ | `invalid_target_format` | 422 | Convert: unknown target format. *(beta)* |
185
+ | `same_format_conversion` | 422 | Convert: target equals the source format. *(beta)* |
186
+ | `invalid_target_size` | 422 | Compress: unusable `target_size_bytes`. *(beta)* |
187
+ | `target_size_conflict` | 422 | Compress: `target_size_bytes` conflicts with the quality mode. *(beta)* |
188
+ | `quota_exhausted` | 429 | Monthly quota used up; check `usage()` and `Retry-After`. |
189
+ | `too_many_upload_sessions` | 429 | Too many concurrent upload sessions. |
190
+ | `upload_session_storage_limit` | 429 | Upload session storage limit reached. |
191
+ | `internal_error` | 500 | Unexpected server failure — quote the `requestId`. |
192
+
193
+ The `code` type is an open union (`KnownTinifyErrorCode | string`), so new server codes never break your compile.
194
+
195
+ ## Limits
196
+
197
+ - **40 MB** (41,943,040 bytes) and **50 MP** per image.
198
+ - **200 files** per batch (plan-dependent, lower on some plans).
199
+ - Result download URLs expire after **2 hours** — download promptly or re-run.
200
+ - Rate-limit state is exposed on every response via `rateLimit` (`X-RateLimit-Limit` / `-Remaining` / `-Reset`).
201
+
202
+ ## CLI
203
+
204
+ The package ships a `tinify-dev` binary:
205
+
206
+ ```sh
207
+ export TINIFY_API_KEY=tnf_live_...
208
+
209
+ tinify-dev compress *.png # writes photo.min.png next to each input
210
+ tinify-dev compress --quality lossless --out dist/ img/*.png
211
+ tinify-dev resize --width 800 photo.jpg
212
+ tinify-dev crop --x 0 --y 0 --width 600 --height 400 photo.png
213
+ tinify-dev convert --format webp photo.png # beta
214
+ tinify-dev usage
215
+ ```
216
+
217
+ Prints a per-file saved-bytes table, warns and skips files over 40 MB, and exits 1 if any file fails. Outputs are written as `<name>.min.<ext>` beside the input (or into `--out <dir>`); the CLI never overwrites your originals.
218
+
219
+ ## Browser usage (read this first)
220
+
221
+ The API rejects untrusted browser origins; use it server-side. CORS is only allowed for trusted first-party origins, so calls from arbitrary web apps will fail with a 403 — proxy through your backend instead. (Keys in browser bundles are public anyway.)
222
+
223
+ ## TypeScript notes
224
+
225
+ - Ships `.d.ts` (ESM) and `.d.cts` (CJS) — correct types under both `"module": "NodeNext"` and bundlers.
226
+ - `TinifyResponse<T>`, `ImageResultData`, `Batch`, `Usage`, and every option type are exported.
227
+ - Response field names mirror the wire format (`snake_case`); client-side concepts (`requestId`, `rateLimit`) are camelCase.
228
+ - `convert()` and `target_size_bytes` are marked `@beta` in TSDoc until the server rollout completes.
229
+
230
+ ## License
231
+
232
+ MIT © Stian Larsen