@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 +31 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/chunk-NYRKB5BB.js +573 -0
- package/dist/chunk-NYRKB5BB.js.map +1 -0
- package/dist/cli.cjs +885 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +37 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.js +307 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +586 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types-D58KVL3U.d.cts +219 -0
- package/dist/types-D58KVL3U.d.ts +219 -0
- package/package.json +64 -0
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
|