tryiton 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TryItOn
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,98 @@
1
+ # TryItOn Node.js SDK — AI Virtual Try-On API for JavaScript & TypeScript
2
+
3
+ Official Node.js and TypeScript client for the [TryItOn](https://tryiton.now) virtual try-on API. Add photoreal AI virtual try-on for clothing, accessories, hairstyles, and tattoos to your JavaScript or TypeScript application with a few lines of code.
4
+
5
+ - Virtual clothing try-on and accessory try-on (eyewear, footwear, headwear, jewelry)
6
+ - Hairstyle and tattoo try-on
7
+ - Fully typed, zero runtime dependencies (uses the native `fetch` API)
8
+ - Built-in job polling helper
9
+
10
+ Full API reference: [docs.tryiton.now](https://docs.tryiton.now) · Get an API key: [tryiton.now/app/developer](https://tryiton.now/app/developer)
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install tryiton
16
+ ```
17
+
18
+ Requires Node.js 18 or later (or any runtime with a global `fetch`, such as Bun, Deno, or modern browsers). TypeScript types are bundled.
19
+
20
+ ## Quickstart: run a virtual try-on
21
+
22
+ Submit a garment and a model photo, then wait for the generated result image.
23
+
24
+ ```ts
25
+ import { TryItOn } from "tryiton";
26
+
27
+ const client = new TryItOn({ apiKey: process.env.TRYITON_API_KEY });
28
+
29
+ // Submit a clothing try-on
30
+ const jobId = await client.tryOnClothes({
31
+ modelImage: "https://example.com/model.jpg",
32
+ garmentImage: "https://example.com/tshirt.jpg",
33
+ category: "clothing",
34
+ subcategory: "tops",
35
+ });
36
+
37
+ // Poll until the job completes and return the output image URL(s)
38
+ const [resultUrl] = await client.waitForResult(jobId);
39
+ console.log(resultUrl); // CDN URL, available for 72 hours
40
+ ```
41
+
42
+ Image inputs accept a public URL or a base64 data URL (`data:image/png;base64,...`).
43
+
44
+ ## Core parameters
45
+
46
+ `tryOnClothes` covers clothing and accessory try-on. The most important parameters:
47
+
48
+ | Parameter | Type | Required | Description |
49
+ | --------- | ---- | -------- | ----------- |
50
+ | `modelImage` | string | Yes | URL or base64 data URL of the person. |
51
+ | `garmentImage` | string | Yes | URL or base64 data URL of the garment or accessory. |
52
+ | `category` | string | No | Item type: `auto`, `clothing`, `eyewear`, `footwear`, `headwear`, `jewelry`, `accessories`, or `others`. `auto` detects it for you. |
53
+ | `subcategory` | string | No | Required for `clothing` (`tops`, `bottoms`, `dresses`), `jewelry`, and `accessories`. |
54
+
55
+ Additional clothing options (`mode`, `numSamples`, `outputFormat`, `seed`) are documented in the [API reference](https://docs.tryiton.now).
56
+
57
+ ## Other endpoints
58
+
59
+ ```ts
60
+ // Hairstyle try-on (see the HAIRCUTS export for all supported values)
61
+ await client.tryOnHairstyle({ faceImage, haircut: "BuzzCut", hairColor: "ash blonde" });
62
+
63
+ // Tattoo try-on
64
+ await client.tryOnTattoo({ bodyImage, designImage, placement: "on the right forearm, small" });
65
+
66
+ // Poll a job manually, or check your credit balance
67
+ const status = await client.getStatus(jobId); // { status, output, error }
68
+ const credits = await client.getCredits(); // { on_demand, subscription, purchased, reserved }
69
+ ```
70
+
71
+ ## Error handling
72
+
73
+ All failures throw `TryItOnError`, which carries the HTTP status code and the API error name.
74
+
75
+ ```ts
76
+ import { TryItOn, TryItOnError } from "tryiton";
77
+
78
+ try {
79
+ await client.tryOnClothes({ /* ... */ });
80
+ } catch (err) {
81
+ if (err instanceof TryItOnError) {
82
+ console.error(err.status, err.errorName, err.message); // e.g. 429, "OutOfCredits"
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## Notes
88
+
89
+ - Output image URLs expire 72 hours after completion. Download any results you want to keep.
90
+ - Failed jobs are never charged.
91
+
92
+ ## Documentation
93
+
94
+ Full documentation, parameter reference, and guides: [docs.tryiton.now](https://docs.tryiton.now)
95
+
96
+ ## License
97
+
98
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ HAIRCUTS: () => HAIRCUTS,
24
+ TryItOn: () => TryItOn,
25
+ TryItOnError: () => TryItOnError,
26
+ default: () => index_default
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var DEFAULT_BASE_URL = "https://tryiton.now/api/v1";
30
+ var HAIRCUTS = [
31
+ "Afro",
32
+ "BobCut",
33
+ "BowlCut",
34
+ "BoxBraids",
35
+ "BuzzCut",
36
+ "Chignon",
37
+ "CombOver",
38
+ "CornrowBraids",
39
+ "CurlyBob",
40
+ "CurlyShag",
41
+ "DoubleBun",
42
+ "Dreadlocks",
43
+ "FauxHawk",
44
+ "FishtailBraid",
45
+ "LongCurly",
46
+ "LongHairTiedUp",
47
+ "LongHimeCut",
48
+ "LongStraight",
49
+ "LongTwintails",
50
+ "LongWavy",
51
+ "LongWavyCurtainBangs",
52
+ "ManBun",
53
+ "MessyTousled",
54
+ "PixieCut",
55
+ "Pompadour",
56
+ "Ponytail",
57
+ "ShortCurlyPixie",
58
+ "ShortTwintails",
59
+ "ShoulderLengthHair",
60
+ "Spiky",
61
+ "TexturedFringe",
62
+ "TwinBraids",
63
+ "Updo",
64
+ "WavyShag"
65
+ ];
66
+ var TryItOnError = class _TryItOnError extends Error {
67
+ constructor(message, opts = {}) {
68
+ super(message);
69
+ this.name = "TryItOnError";
70
+ this.status = opts.status ?? null;
71
+ this.errorName = opts.errorName ?? null;
72
+ Object.setPrototypeOf(this, _TryItOnError.prototype);
73
+ }
74
+ };
75
+ function sleep(ms, signal) {
76
+ return new Promise((resolve, reject) => {
77
+ if (signal?.aborted) return reject(new TryItOnError("Aborted.", { errorName: "Aborted" }));
78
+ const t = setTimeout(resolve, ms);
79
+ signal?.addEventListener(
80
+ "abort",
81
+ () => {
82
+ clearTimeout(t);
83
+ reject(new TryItOnError("Aborted.", { errorName: "Aborted" }));
84
+ },
85
+ { once: true }
86
+ );
87
+ });
88
+ }
89
+ var TryItOn = class {
90
+ constructor(options) {
91
+ if (!options || !options.apiKey) throw new TryItOnError("An apiKey is required.", { errorName: "ConfigError" });
92
+ this.apiKey = options.apiKey;
93
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
94
+ this.timeoutMs = options.timeoutMs ?? 6e4;
95
+ const f = options.fetch ?? globalThis.fetch;
96
+ if (!f) {
97
+ throw new TryItOnError(
98
+ "No fetch implementation found. Use Node 18+, or pass `fetch` in options.",
99
+ { errorName: "ConfigError" }
100
+ );
101
+ }
102
+ this.fetchImpl = f;
103
+ }
104
+ /** Put a garment or accessory on a person. Returns the job id. */
105
+ async tryOnClothes(params) {
106
+ const body = {
107
+ model_image: params.modelImage,
108
+ garment_image: params.garmentImage,
109
+ category: params.category,
110
+ subcategory: params.subcategory,
111
+ mode: params.mode,
112
+ num_samples: params.numSamples,
113
+ output_format: params.outputFormat,
114
+ seed: params.seed,
115
+ segmentation_free: params.segmentationFree,
116
+ garment_photo_type: params.garmentPhotoType,
117
+ moderation_level: params.moderationLevel
118
+ };
119
+ const res = await this.request("POST", "/tryon/clothes", body);
120
+ return res.jobId;
121
+ }
122
+ /** Restyle a person's hair. Returns the job id. */
123
+ async tryOnHairstyle(params) {
124
+ const body = { face_image: params.faceImage, haircut: params.haircut, hair_color: params.hairColor };
125
+ const res = await this.request("POST", "/tryon/hairstyle", body);
126
+ return res.jobId;
127
+ }
128
+ /** Ink a design onto skin. Returns the job id. */
129
+ async tryOnTattoo(params) {
130
+ const body = { body_image: params.bodyImage, design_image: params.designImage, placement: params.placement };
131
+ const res = await this.request("POST", "/tryon/tattoo", body);
132
+ return res.jobId;
133
+ }
134
+ /** Fetch the current status of a job. */
135
+ async getStatus(jobId) {
136
+ return this.request("GET", `/status/${encodeURIComponent(jobId)}`);
137
+ }
138
+ /** Fetch your current credit balance. */
139
+ async getCredits() {
140
+ const res = await this.request("GET", "/credits");
141
+ return res.credits;
142
+ }
143
+ /**
144
+ * Poll a job until it completes, then resolve with the output image URLs.
145
+ * Throws a TryItOnError if the job fails or the timeout is reached.
146
+ */
147
+ async waitForResult(jobId, opts = {}) {
148
+ const pollInterval = opts.pollIntervalMs ?? 2e3;
149
+ const timeout = opts.timeoutMs ?? 12e4;
150
+ const start = Date.now();
151
+ for (; ; ) {
152
+ const res = await this.getStatus(jobId);
153
+ if (res.status === "completed") return res.output ?? [];
154
+ if (res.status === "failed") {
155
+ throw new TryItOnError(res.error?.message ?? "Try-on failed.", {
156
+ errorName: res.error?.name ?? "ProcessingError"
157
+ });
158
+ }
159
+ if (Date.now() - start > timeout) {
160
+ throw new TryItOnError(`Timed out waiting for job ${jobId} after ${timeout}ms.`, { errorName: "Timeout" });
161
+ }
162
+ await sleep(pollInterval, opts.signal);
163
+ }
164
+ }
165
+ async request(method, path, body) {
166
+ const url = `${this.baseUrl}${path}`;
167
+ const controller = new AbortController();
168
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
169
+ let res;
170
+ try {
171
+ res = await this.fetchImpl(url, {
172
+ method,
173
+ headers: {
174
+ Authorization: `Bearer ${this.apiKey}`,
175
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
176
+ },
177
+ body: body !== void 0 ? JSON.stringify(stripUndefined(body)) : void 0,
178
+ signal: controller.signal
179
+ });
180
+ } catch (err) {
181
+ clearTimeout(timer);
182
+ if (err?.name === "AbortError") {
183
+ throw new TryItOnError(`Request timed out after ${this.timeoutMs}ms.`, { errorName: "Timeout" });
184
+ }
185
+ throw new TryItOnError(`Network error: ${err?.message ?? String(err)}`, { errorName: "NetworkError" });
186
+ } finally {
187
+ clearTimeout(timer);
188
+ }
189
+ const text = await res.text();
190
+ let data = {};
191
+ if (text) {
192
+ try {
193
+ data = JSON.parse(text);
194
+ } catch {
195
+ if (!res.ok) throw new TryItOnError(text || `HTTP ${res.status}`, { status: res.status });
196
+ }
197
+ }
198
+ if (!res.ok) {
199
+ throw new TryItOnError(data?.message ?? `HTTP ${res.status}`, {
200
+ status: res.status,
201
+ errorName: data?.error ?? null
202
+ });
203
+ }
204
+ return data;
205
+ }
206
+ };
207
+ function stripUndefined(obj) {
208
+ if (obj === null || typeof obj !== "object") return obj;
209
+ const out = {};
210
+ for (const [k, v] of Object.entries(obj)) {
211
+ if (v !== void 0) out[k] = v;
212
+ }
213
+ return out;
214
+ }
215
+ var index_default = TryItOn;
216
+ // Annotate the CommonJS export names for ESM import in node:
217
+ 0 && (module.exports = {
218
+ HAIRCUTS,
219
+ TryItOn,
220
+ TryItOnError
221
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * TryItOn — official JavaScript / TypeScript SDK.
3
+ *
4
+ * Wraps the TryItOn virtual try-on REST API (https://docs.tryiton.now).
5
+ * Requires a runtime with a global `fetch` (Node 18+, Bun, Deno, browsers) or a
6
+ * `fetch` implementation passed via options.
7
+ */
8
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
9
+ interface TryItOnOptions {
10
+ /** Your API key from https://tryiton.now/app/developer */
11
+ apiKey: string;
12
+ /** Override the API base URL. Defaults to https://tryiton.now/api/v1 */
13
+ baseUrl?: string;
14
+ /** Per-request timeout in milliseconds. Defaults to 60000. */
15
+ timeoutMs?: number;
16
+ /** Custom fetch implementation (e.g. node-fetch on older Node). */
17
+ fetch?: FetchLike;
18
+ }
19
+ type JobStatus = "processing" | "completed" | "failed";
20
+ interface JobError {
21
+ name: string;
22
+ message: string;
23
+ }
24
+ interface StatusResponse {
25
+ ok: boolean;
26
+ status: JobStatus;
27
+ /** CDN URLs of the result image(s); present when completed. Expire after 72h. */
28
+ output?: string[];
29
+ error?: JobError | null;
30
+ }
31
+ interface Credits {
32
+ on_demand: number;
33
+ subscription: number;
34
+ purchased: number;
35
+ reserved: number;
36
+ }
37
+ type Category = "auto" | "clothing" | "eyewear" | "footwear" | "headwear" | "jewelry" | "accessories" | "others";
38
+ type Mode = "performance" | "balanced" | "quality";
39
+ interface TryOnClothesParams {
40
+ /** URL or base64 data URL of the person. */
41
+ modelImage: string;
42
+ /** URL or base64 data URL of the garment/accessory. */
43
+ garmentImage: string;
44
+ /** What the item is. `auto` detects it for you. */
45
+ category?: Category;
46
+ /** Required for `clothing`, `jewelry`, `accessories` (e.g. "tops"). */
47
+ subcategory?: string;
48
+ /** Quality/speed trade-off (clothing only). */
49
+ mode?: Mode;
50
+ /** Number of output images, 1–4 (clothing only). Charged per image. */
51
+ numSamples?: number;
52
+ /** "png" or "jpeg" (clothing only). */
53
+ outputFormat?: "png" | "jpeg";
54
+ /** Fixes randomness (clothing only). */
55
+ seed?: number;
56
+ /** Advanced (clothing only). */
57
+ segmentationFree?: boolean;
58
+ /** Advanced hint: "auto" | "model" | "flat-lay" (clothing only). */
59
+ garmentPhotoType?: "auto" | "model" | "flat-lay";
60
+ /** Advanced (clothing only). */
61
+ moderationLevel?: string;
62
+ }
63
+ interface TryOnHairstyleParams {
64
+ /** URL or base64 data URL of the person's face. */
65
+ faceImage: string;
66
+ /** One of the supported haircut values (see HAIRCUTS). */
67
+ haircut: string;
68
+ /** Optional free-text color, e.g. "ash blonde". Omit to keep current color. */
69
+ hairColor?: string;
70
+ }
71
+ interface TryOnTattooParams {
72
+ /** URL or base64 data URL — a close-up of bare skin. */
73
+ bodyImage: string;
74
+ /** URL or base64 data URL of the tattoo design on its own. */
75
+ designImage: string;
76
+ /** Optional free-text placement/size, max 140 chars. */
77
+ placement?: string;
78
+ }
79
+ interface WaitOptions {
80
+ /** How often to poll, in ms. Default 2000. */
81
+ pollIntervalMs?: number;
82
+ /** Give up after this many ms. Default 120000. */
83
+ timeoutMs?: number;
84
+ /** Abort the wait. */
85
+ signal?: AbortSignal;
86
+ }
87
+ /** The supported `haircut` values for hairstyle try-on. */
88
+ declare const HAIRCUTS: readonly ["Afro", "BobCut", "BowlCut", "BoxBraids", "BuzzCut", "Chignon", "CombOver", "CornrowBraids", "CurlyBob", "CurlyShag", "DoubleBun", "Dreadlocks", "FauxHawk", "FishtailBraid", "LongCurly", "LongHairTiedUp", "LongHimeCut", "LongStraight", "LongTwintails", "LongWavy", "LongWavyCurtainBangs", "ManBun", "MessyTousled", "PixieCut", "Pompadour", "Ponytail", "ShortCurlyPixie", "ShortTwintails", "ShoulderLengthHair", "Spiky", "TexturedFringe", "TwinBraids", "Updo", "WavyShag"];
89
+ /**
90
+ * Raised for any API-level error (bad request, auth, rate limit, out of credits,
91
+ * server error) and for runtime failures surfaced while polling a job.
92
+ */
93
+ declare class TryItOnError extends Error {
94
+ /** HTTP status code, or null for a runtime (job) failure. */
95
+ readonly status: number | null;
96
+ /** The API error name, e.g. "OutOfCredits" or "ProcessingError". */
97
+ readonly errorName: string | null;
98
+ constructor(message: string, opts?: {
99
+ status?: number | null;
100
+ errorName?: string | null;
101
+ });
102
+ }
103
+ declare class TryItOn {
104
+ private readonly apiKey;
105
+ private readonly baseUrl;
106
+ private readonly timeoutMs;
107
+ private readonly fetchImpl;
108
+ constructor(options: TryItOnOptions);
109
+ /** Put a garment or accessory on a person. Returns the job id. */
110
+ tryOnClothes(params: TryOnClothesParams): Promise<string>;
111
+ /** Restyle a person's hair. Returns the job id. */
112
+ tryOnHairstyle(params: TryOnHairstyleParams): Promise<string>;
113
+ /** Ink a design onto skin. Returns the job id. */
114
+ tryOnTattoo(params: TryOnTattooParams): Promise<string>;
115
+ /** Fetch the current status of a job. */
116
+ getStatus(jobId: string): Promise<StatusResponse>;
117
+ /** Fetch your current credit balance. */
118
+ getCredits(): Promise<Credits>;
119
+ /**
120
+ * Poll a job until it completes, then resolve with the output image URLs.
121
+ * Throws a TryItOnError if the job fails or the timeout is reached.
122
+ */
123
+ waitForResult(jobId: string, opts?: WaitOptions): Promise<string[]>;
124
+ private request;
125
+ }
126
+
127
+ export { type Category, type Credits, type FetchLike, HAIRCUTS, type JobError, type JobStatus, type Mode, type StatusResponse, TryItOn, TryItOnError, type TryItOnOptions, type TryOnClothesParams, type TryOnHairstyleParams, type TryOnTattooParams, type WaitOptions, TryItOn as default };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * TryItOn — official JavaScript / TypeScript SDK.
3
+ *
4
+ * Wraps the TryItOn virtual try-on REST API (https://docs.tryiton.now).
5
+ * Requires a runtime with a global `fetch` (Node 18+, Bun, Deno, browsers) or a
6
+ * `fetch` implementation passed via options.
7
+ */
8
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
9
+ interface TryItOnOptions {
10
+ /** Your API key from https://tryiton.now/app/developer */
11
+ apiKey: string;
12
+ /** Override the API base URL. Defaults to https://tryiton.now/api/v1 */
13
+ baseUrl?: string;
14
+ /** Per-request timeout in milliseconds. Defaults to 60000. */
15
+ timeoutMs?: number;
16
+ /** Custom fetch implementation (e.g. node-fetch on older Node). */
17
+ fetch?: FetchLike;
18
+ }
19
+ type JobStatus = "processing" | "completed" | "failed";
20
+ interface JobError {
21
+ name: string;
22
+ message: string;
23
+ }
24
+ interface StatusResponse {
25
+ ok: boolean;
26
+ status: JobStatus;
27
+ /** CDN URLs of the result image(s); present when completed. Expire after 72h. */
28
+ output?: string[];
29
+ error?: JobError | null;
30
+ }
31
+ interface Credits {
32
+ on_demand: number;
33
+ subscription: number;
34
+ purchased: number;
35
+ reserved: number;
36
+ }
37
+ type Category = "auto" | "clothing" | "eyewear" | "footwear" | "headwear" | "jewelry" | "accessories" | "others";
38
+ type Mode = "performance" | "balanced" | "quality";
39
+ interface TryOnClothesParams {
40
+ /** URL or base64 data URL of the person. */
41
+ modelImage: string;
42
+ /** URL or base64 data URL of the garment/accessory. */
43
+ garmentImage: string;
44
+ /** What the item is. `auto` detects it for you. */
45
+ category?: Category;
46
+ /** Required for `clothing`, `jewelry`, `accessories` (e.g. "tops"). */
47
+ subcategory?: string;
48
+ /** Quality/speed trade-off (clothing only). */
49
+ mode?: Mode;
50
+ /** Number of output images, 1–4 (clothing only). Charged per image. */
51
+ numSamples?: number;
52
+ /** "png" or "jpeg" (clothing only). */
53
+ outputFormat?: "png" | "jpeg";
54
+ /** Fixes randomness (clothing only). */
55
+ seed?: number;
56
+ /** Advanced (clothing only). */
57
+ segmentationFree?: boolean;
58
+ /** Advanced hint: "auto" | "model" | "flat-lay" (clothing only). */
59
+ garmentPhotoType?: "auto" | "model" | "flat-lay";
60
+ /** Advanced (clothing only). */
61
+ moderationLevel?: string;
62
+ }
63
+ interface TryOnHairstyleParams {
64
+ /** URL or base64 data URL of the person's face. */
65
+ faceImage: string;
66
+ /** One of the supported haircut values (see HAIRCUTS). */
67
+ haircut: string;
68
+ /** Optional free-text color, e.g. "ash blonde". Omit to keep current color. */
69
+ hairColor?: string;
70
+ }
71
+ interface TryOnTattooParams {
72
+ /** URL or base64 data URL — a close-up of bare skin. */
73
+ bodyImage: string;
74
+ /** URL or base64 data URL of the tattoo design on its own. */
75
+ designImage: string;
76
+ /** Optional free-text placement/size, max 140 chars. */
77
+ placement?: string;
78
+ }
79
+ interface WaitOptions {
80
+ /** How often to poll, in ms. Default 2000. */
81
+ pollIntervalMs?: number;
82
+ /** Give up after this many ms. Default 120000. */
83
+ timeoutMs?: number;
84
+ /** Abort the wait. */
85
+ signal?: AbortSignal;
86
+ }
87
+ /** The supported `haircut` values for hairstyle try-on. */
88
+ declare const HAIRCUTS: readonly ["Afro", "BobCut", "BowlCut", "BoxBraids", "BuzzCut", "Chignon", "CombOver", "CornrowBraids", "CurlyBob", "CurlyShag", "DoubleBun", "Dreadlocks", "FauxHawk", "FishtailBraid", "LongCurly", "LongHairTiedUp", "LongHimeCut", "LongStraight", "LongTwintails", "LongWavy", "LongWavyCurtainBangs", "ManBun", "MessyTousled", "PixieCut", "Pompadour", "Ponytail", "ShortCurlyPixie", "ShortTwintails", "ShoulderLengthHair", "Spiky", "TexturedFringe", "TwinBraids", "Updo", "WavyShag"];
89
+ /**
90
+ * Raised for any API-level error (bad request, auth, rate limit, out of credits,
91
+ * server error) and for runtime failures surfaced while polling a job.
92
+ */
93
+ declare class TryItOnError extends Error {
94
+ /** HTTP status code, or null for a runtime (job) failure. */
95
+ readonly status: number | null;
96
+ /** The API error name, e.g. "OutOfCredits" or "ProcessingError". */
97
+ readonly errorName: string | null;
98
+ constructor(message: string, opts?: {
99
+ status?: number | null;
100
+ errorName?: string | null;
101
+ });
102
+ }
103
+ declare class TryItOn {
104
+ private readonly apiKey;
105
+ private readonly baseUrl;
106
+ private readonly timeoutMs;
107
+ private readonly fetchImpl;
108
+ constructor(options: TryItOnOptions);
109
+ /** Put a garment or accessory on a person. Returns the job id. */
110
+ tryOnClothes(params: TryOnClothesParams): Promise<string>;
111
+ /** Restyle a person's hair. Returns the job id. */
112
+ tryOnHairstyle(params: TryOnHairstyleParams): Promise<string>;
113
+ /** Ink a design onto skin. Returns the job id. */
114
+ tryOnTattoo(params: TryOnTattooParams): Promise<string>;
115
+ /** Fetch the current status of a job. */
116
+ getStatus(jobId: string): Promise<StatusResponse>;
117
+ /** Fetch your current credit balance. */
118
+ getCredits(): Promise<Credits>;
119
+ /**
120
+ * Poll a job until it completes, then resolve with the output image URLs.
121
+ * Throws a TryItOnError if the job fails or the timeout is reached.
122
+ */
123
+ waitForResult(jobId: string, opts?: WaitOptions): Promise<string[]>;
124
+ private request;
125
+ }
126
+
127
+ export { type Category, type Credits, type FetchLike, HAIRCUTS, type JobError, type JobStatus, type Mode, type StatusResponse, TryItOn, TryItOnError, type TryItOnOptions, type TryOnClothesParams, type TryOnHairstyleParams, type TryOnTattooParams, type WaitOptions, TryItOn as default };
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ // src/index.ts
2
+ var DEFAULT_BASE_URL = "https://tryiton.now/api/v1";
3
+ var HAIRCUTS = [
4
+ "Afro",
5
+ "BobCut",
6
+ "BowlCut",
7
+ "BoxBraids",
8
+ "BuzzCut",
9
+ "Chignon",
10
+ "CombOver",
11
+ "CornrowBraids",
12
+ "CurlyBob",
13
+ "CurlyShag",
14
+ "DoubleBun",
15
+ "Dreadlocks",
16
+ "FauxHawk",
17
+ "FishtailBraid",
18
+ "LongCurly",
19
+ "LongHairTiedUp",
20
+ "LongHimeCut",
21
+ "LongStraight",
22
+ "LongTwintails",
23
+ "LongWavy",
24
+ "LongWavyCurtainBangs",
25
+ "ManBun",
26
+ "MessyTousled",
27
+ "PixieCut",
28
+ "Pompadour",
29
+ "Ponytail",
30
+ "ShortCurlyPixie",
31
+ "ShortTwintails",
32
+ "ShoulderLengthHair",
33
+ "Spiky",
34
+ "TexturedFringe",
35
+ "TwinBraids",
36
+ "Updo",
37
+ "WavyShag"
38
+ ];
39
+ var TryItOnError = class _TryItOnError extends Error {
40
+ constructor(message, opts = {}) {
41
+ super(message);
42
+ this.name = "TryItOnError";
43
+ this.status = opts.status ?? null;
44
+ this.errorName = opts.errorName ?? null;
45
+ Object.setPrototypeOf(this, _TryItOnError.prototype);
46
+ }
47
+ };
48
+ function sleep(ms, signal) {
49
+ return new Promise((resolve, reject) => {
50
+ if (signal?.aborted) return reject(new TryItOnError("Aborted.", { errorName: "Aborted" }));
51
+ const t = setTimeout(resolve, ms);
52
+ signal?.addEventListener(
53
+ "abort",
54
+ () => {
55
+ clearTimeout(t);
56
+ reject(new TryItOnError("Aborted.", { errorName: "Aborted" }));
57
+ },
58
+ { once: true }
59
+ );
60
+ });
61
+ }
62
+ var TryItOn = class {
63
+ constructor(options) {
64
+ if (!options || !options.apiKey) throw new TryItOnError("An apiKey is required.", { errorName: "ConfigError" });
65
+ this.apiKey = options.apiKey;
66
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
67
+ this.timeoutMs = options.timeoutMs ?? 6e4;
68
+ const f = options.fetch ?? globalThis.fetch;
69
+ if (!f) {
70
+ throw new TryItOnError(
71
+ "No fetch implementation found. Use Node 18+, or pass `fetch` in options.",
72
+ { errorName: "ConfigError" }
73
+ );
74
+ }
75
+ this.fetchImpl = f;
76
+ }
77
+ /** Put a garment or accessory on a person. Returns the job id. */
78
+ async tryOnClothes(params) {
79
+ const body = {
80
+ model_image: params.modelImage,
81
+ garment_image: params.garmentImage,
82
+ category: params.category,
83
+ subcategory: params.subcategory,
84
+ mode: params.mode,
85
+ num_samples: params.numSamples,
86
+ output_format: params.outputFormat,
87
+ seed: params.seed,
88
+ segmentation_free: params.segmentationFree,
89
+ garment_photo_type: params.garmentPhotoType,
90
+ moderation_level: params.moderationLevel
91
+ };
92
+ const res = await this.request("POST", "/tryon/clothes", body);
93
+ return res.jobId;
94
+ }
95
+ /** Restyle a person's hair. Returns the job id. */
96
+ async tryOnHairstyle(params) {
97
+ const body = { face_image: params.faceImage, haircut: params.haircut, hair_color: params.hairColor };
98
+ const res = await this.request("POST", "/tryon/hairstyle", body);
99
+ return res.jobId;
100
+ }
101
+ /** Ink a design onto skin. Returns the job id. */
102
+ async tryOnTattoo(params) {
103
+ const body = { body_image: params.bodyImage, design_image: params.designImage, placement: params.placement };
104
+ const res = await this.request("POST", "/tryon/tattoo", body);
105
+ return res.jobId;
106
+ }
107
+ /** Fetch the current status of a job. */
108
+ async getStatus(jobId) {
109
+ return this.request("GET", `/status/${encodeURIComponent(jobId)}`);
110
+ }
111
+ /** Fetch your current credit balance. */
112
+ async getCredits() {
113
+ const res = await this.request("GET", "/credits");
114
+ return res.credits;
115
+ }
116
+ /**
117
+ * Poll a job until it completes, then resolve with the output image URLs.
118
+ * Throws a TryItOnError if the job fails or the timeout is reached.
119
+ */
120
+ async waitForResult(jobId, opts = {}) {
121
+ const pollInterval = opts.pollIntervalMs ?? 2e3;
122
+ const timeout = opts.timeoutMs ?? 12e4;
123
+ const start = Date.now();
124
+ for (; ; ) {
125
+ const res = await this.getStatus(jobId);
126
+ if (res.status === "completed") return res.output ?? [];
127
+ if (res.status === "failed") {
128
+ throw new TryItOnError(res.error?.message ?? "Try-on failed.", {
129
+ errorName: res.error?.name ?? "ProcessingError"
130
+ });
131
+ }
132
+ if (Date.now() - start > timeout) {
133
+ throw new TryItOnError(`Timed out waiting for job ${jobId} after ${timeout}ms.`, { errorName: "Timeout" });
134
+ }
135
+ await sleep(pollInterval, opts.signal);
136
+ }
137
+ }
138
+ async request(method, path, body) {
139
+ const url = `${this.baseUrl}${path}`;
140
+ const controller = new AbortController();
141
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
142
+ let res;
143
+ try {
144
+ res = await this.fetchImpl(url, {
145
+ method,
146
+ headers: {
147
+ Authorization: `Bearer ${this.apiKey}`,
148
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
149
+ },
150
+ body: body !== void 0 ? JSON.stringify(stripUndefined(body)) : void 0,
151
+ signal: controller.signal
152
+ });
153
+ } catch (err) {
154
+ clearTimeout(timer);
155
+ if (err?.name === "AbortError") {
156
+ throw new TryItOnError(`Request timed out after ${this.timeoutMs}ms.`, { errorName: "Timeout" });
157
+ }
158
+ throw new TryItOnError(`Network error: ${err?.message ?? String(err)}`, { errorName: "NetworkError" });
159
+ } finally {
160
+ clearTimeout(timer);
161
+ }
162
+ const text = await res.text();
163
+ let data = {};
164
+ if (text) {
165
+ try {
166
+ data = JSON.parse(text);
167
+ } catch {
168
+ if (!res.ok) throw new TryItOnError(text || `HTTP ${res.status}`, { status: res.status });
169
+ }
170
+ }
171
+ if (!res.ok) {
172
+ throw new TryItOnError(data?.message ?? `HTTP ${res.status}`, {
173
+ status: res.status,
174
+ errorName: data?.error ?? null
175
+ });
176
+ }
177
+ return data;
178
+ }
179
+ };
180
+ function stripUndefined(obj) {
181
+ if (obj === null || typeof obj !== "object") return obj;
182
+ const out = {};
183
+ for (const [k, v] of Object.entries(obj)) {
184
+ if (v !== void 0) out[k] = v;
185
+ }
186
+ return out;
187
+ }
188
+ var index_default = TryItOn;
189
+ export {
190
+ HAIRCUTS,
191
+ TryItOn,
192
+ TryItOnError,
193
+ index_default as default
194
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "tryiton",
3
+ "version": "0.1.0",
4
+ "description": "Official JavaScript/TypeScript SDK for the TryItOn virtual try-on API.",
5
+ "keywords": [
6
+ "tryiton",
7
+ "virtual-try-on",
8
+ "tryon",
9
+ "fashion",
10
+ "ai",
11
+ "sdk"
12
+ ],
13
+ "homepage": "https://docs.tryiton.now",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/tryiton-now/tryiton-node.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/tryiton-now/tryiton-node/issues"
20
+ },
21
+ "license": "MIT",
22
+ "author": "TryItOn",
23
+ "type": "module",
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "README.md"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "devDependencies": {
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.4.0"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,298 @@
1
+ /**
2
+ * TryItOn — official JavaScript / TypeScript SDK.
3
+ *
4
+ * Wraps the TryItOn virtual try-on REST API (https://docs.tryiton.now).
5
+ * Requires a runtime with a global `fetch` (Node 18+, Bun, Deno, browsers) or a
6
+ * `fetch` implementation passed via options.
7
+ */
8
+
9
+ const DEFAULT_BASE_URL = "https://tryiton.now/api/v1";
10
+
11
+ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
12
+
13
+ export interface TryItOnOptions {
14
+ /** Your API key from https://tryiton.now/app/developer */
15
+ apiKey: string;
16
+ /** Override the API base URL. Defaults to https://tryiton.now/api/v1 */
17
+ baseUrl?: string;
18
+ /** Per-request timeout in milliseconds. Defaults to 60000. */
19
+ timeoutMs?: number;
20
+ /** Custom fetch implementation (e.g. node-fetch on older Node). */
21
+ fetch?: FetchLike;
22
+ }
23
+
24
+ export type JobStatus = "processing" | "completed" | "failed";
25
+
26
+ export interface JobError {
27
+ name: string;
28
+ message: string;
29
+ }
30
+
31
+ export interface StatusResponse {
32
+ ok: boolean;
33
+ status: JobStatus;
34
+ /** CDN URLs of the result image(s); present when completed. Expire after 72h. */
35
+ output?: string[];
36
+ error?: JobError | null;
37
+ }
38
+
39
+ export interface Credits {
40
+ on_demand: number;
41
+ subscription: number;
42
+ purchased: number;
43
+ reserved: number;
44
+ }
45
+
46
+ export type Category =
47
+ | "auto"
48
+ | "clothing"
49
+ | "eyewear"
50
+ | "footwear"
51
+ | "headwear"
52
+ | "jewelry"
53
+ | "accessories"
54
+ | "others";
55
+
56
+ export type Mode = "performance" | "balanced" | "quality";
57
+
58
+ export interface TryOnClothesParams {
59
+ /** URL or base64 data URL of the person. */
60
+ modelImage: string;
61
+ /** URL or base64 data URL of the garment/accessory. */
62
+ garmentImage: string;
63
+ /** What the item is. `auto` detects it for you. */
64
+ category?: Category;
65
+ /** Required for `clothing`, `jewelry`, `accessories` (e.g. "tops"). */
66
+ subcategory?: string;
67
+ /** Quality/speed trade-off (clothing only). */
68
+ mode?: Mode;
69
+ /** Number of output images, 1–4 (clothing only). Charged per image. */
70
+ numSamples?: number;
71
+ /** "png" or "jpeg" (clothing only). */
72
+ outputFormat?: "png" | "jpeg";
73
+ /** Fixes randomness (clothing only). */
74
+ seed?: number;
75
+ /** Advanced (clothing only). */
76
+ segmentationFree?: boolean;
77
+ /** Advanced hint: "auto" | "model" | "flat-lay" (clothing only). */
78
+ garmentPhotoType?: "auto" | "model" | "flat-lay";
79
+ /** Advanced (clothing only). */
80
+ moderationLevel?: string;
81
+ }
82
+
83
+ export interface TryOnHairstyleParams {
84
+ /** URL or base64 data URL of the person's face. */
85
+ faceImage: string;
86
+ /** One of the supported haircut values (see HAIRCUTS). */
87
+ haircut: string;
88
+ /** Optional free-text color, e.g. "ash blonde". Omit to keep current color. */
89
+ hairColor?: string;
90
+ }
91
+
92
+ export interface TryOnTattooParams {
93
+ /** URL or base64 data URL — a close-up of bare skin. */
94
+ bodyImage: string;
95
+ /** URL or base64 data URL of the tattoo design on its own. */
96
+ designImage: string;
97
+ /** Optional free-text placement/size, max 140 chars. */
98
+ placement?: string;
99
+ }
100
+
101
+ export interface WaitOptions {
102
+ /** How often to poll, in ms. Default 2000. */
103
+ pollIntervalMs?: number;
104
+ /** Give up after this many ms. Default 120000. */
105
+ timeoutMs?: number;
106
+ /** Abort the wait. */
107
+ signal?: AbortSignal;
108
+ }
109
+
110
+ /** The supported `haircut` values for hairstyle try-on. */
111
+ export const HAIRCUTS = [
112
+ "Afro", "BobCut", "BowlCut", "BoxBraids", "BuzzCut", "Chignon", "CombOver",
113
+ "CornrowBraids", "CurlyBob", "CurlyShag", "DoubleBun", "Dreadlocks", "FauxHawk",
114
+ "FishtailBraid", "LongCurly", "LongHairTiedUp", "LongHimeCut", "LongStraight",
115
+ "LongTwintails", "LongWavy", "LongWavyCurtainBangs", "ManBun", "MessyTousled",
116
+ "PixieCut", "Pompadour", "Ponytail", "ShortCurlyPixie", "ShortTwintails",
117
+ "ShoulderLengthHair", "Spiky", "TexturedFringe", "TwinBraids", "Updo", "WavyShag",
118
+ ] as const;
119
+
120
+ /**
121
+ * Raised for any API-level error (bad request, auth, rate limit, out of credits,
122
+ * server error) and for runtime failures surfaced while polling a job.
123
+ */
124
+ export class TryItOnError extends Error {
125
+ /** HTTP status code, or null for a runtime (job) failure. */
126
+ readonly status: number | null;
127
+ /** The API error name, e.g. "OutOfCredits" or "ProcessingError". */
128
+ readonly errorName: string | null;
129
+
130
+ constructor(message: string, opts: { status?: number | null; errorName?: string | null } = {}) {
131
+ super(message);
132
+ this.name = "TryItOnError";
133
+ this.status = opts.status ?? null;
134
+ this.errorName = opts.errorName ?? null;
135
+ Object.setPrototypeOf(this, TryItOnError.prototype);
136
+ }
137
+ }
138
+
139
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
140
+ return new Promise((resolve, reject) => {
141
+ if (signal?.aborted) return reject(new TryItOnError("Aborted.", { errorName: "Aborted" }));
142
+ const t = setTimeout(resolve, ms);
143
+ signal?.addEventListener(
144
+ "abort",
145
+ () => {
146
+ clearTimeout(t);
147
+ reject(new TryItOnError("Aborted.", { errorName: "Aborted" }));
148
+ },
149
+ { once: true },
150
+ );
151
+ });
152
+ }
153
+
154
+ export class TryItOn {
155
+ private readonly apiKey: string;
156
+ private readonly baseUrl: string;
157
+ private readonly timeoutMs: number;
158
+ private readonly fetchImpl: FetchLike;
159
+
160
+ constructor(options: TryItOnOptions) {
161
+ if (!options || !options.apiKey) throw new TryItOnError("An apiKey is required.", { errorName: "ConfigError" });
162
+ this.apiKey = options.apiKey;
163
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
164
+ this.timeoutMs = options.timeoutMs ?? 60000;
165
+ const f = options.fetch ?? (globalThis.fetch as FetchLike | undefined);
166
+ if (!f) {
167
+ throw new TryItOnError(
168
+ "No fetch implementation found. Use Node 18+, or pass `fetch` in options.",
169
+ { errorName: "ConfigError" },
170
+ );
171
+ }
172
+ this.fetchImpl = f;
173
+ }
174
+
175
+ /** Put a garment or accessory on a person. Returns the job id. */
176
+ async tryOnClothes(params: TryOnClothesParams): Promise<string> {
177
+ const body = {
178
+ model_image: params.modelImage,
179
+ garment_image: params.garmentImage,
180
+ category: params.category,
181
+ subcategory: params.subcategory,
182
+ mode: params.mode,
183
+ num_samples: params.numSamples,
184
+ output_format: params.outputFormat,
185
+ seed: params.seed,
186
+ segmentation_free: params.segmentationFree,
187
+ garment_photo_type: params.garmentPhotoType,
188
+ moderation_level: params.moderationLevel,
189
+ };
190
+ const res = await this.request<{ jobId: string }>("POST", "/tryon/clothes", body);
191
+ return res.jobId;
192
+ }
193
+
194
+ /** Restyle a person's hair. Returns the job id. */
195
+ async tryOnHairstyle(params: TryOnHairstyleParams): Promise<string> {
196
+ const body = { face_image: params.faceImage, haircut: params.haircut, hair_color: params.hairColor };
197
+ const res = await this.request<{ jobId: string }>("POST", "/tryon/hairstyle", body);
198
+ return res.jobId;
199
+ }
200
+
201
+ /** Ink a design onto skin. Returns the job id. */
202
+ async tryOnTattoo(params: TryOnTattooParams): Promise<string> {
203
+ const body = { body_image: params.bodyImage, design_image: params.designImage, placement: params.placement };
204
+ const res = await this.request<{ jobId: string }>("POST", "/tryon/tattoo", body);
205
+ return res.jobId;
206
+ }
207
+
208
+ /** Fetch the current status of a job. */
209
+ async getStatus(jobId: string): Promise<StatusResponse> {
210
+ return this.request<StatusResponse>("GET", `/status/${encodeURIComponent(jobId)}`);
211
+ }
212
+
213
+ /** Fetch your current credit balance. */
214
+ async getCredits(): Promise<Credits> {
215
+ const res = await this.request<{ credits: Credits }>("GET", "/credits");
216
+ return res.credits;
217
+ }
218
+
219
+ /**
220
+ * Poll a job until it completes, then resolve with the output image URLs.
221
+ * Throws a TryItOnError if the job fails or the timeout is reached.
222
+ */
223
+ async waitForResult(jobId: string, opts: WaitOptions = {}): Promise<string[]> {
224
+ const pollInterval = opts.pollIntervalMs ?? 2000;
225
+ const timeout = opts.timeoutMs ?? 120000;
226
+ const start = Date.now();
227
+ for (;;) {
228
+ const res = await this.getStatus(jobId);
229
+ if (res.status === "completed") return res.output ?? [];
230
+ if (res.status === "failed") {
231
+ throw new TryItOnError(res.error?.message ?? "Try-on failed.", {
232
+ errorName: res.error?.name ?? "ProcessingError",
233
+ });
234
+ }
235
+ if (Date.now() - start > timeout) {
236
+ throw new TryItOnError(`Timed out waiting for job ${jobId} after ${timeout}ms.`, { errorName: "Timeout" });
237
+ }
238
+ await sleep(pollInterval, opts.signal);
239
+ }
240
+ }
241
+
242
+ private async request<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
243
+ const url = `${this.baseUrl}${path}`;
244
+ const controller = new AbortController();
245
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
246
+
247
+ let res: Response;
248
+ try {
249
+ res = await this.fetchImpl(url, {
250
+ method,
251
+ headers: {
252
+ Authorization: `Bearer ${this.apiKey}`,
253
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
254
+ },
255
+ body: body !== undefined ? JSON.stringify(stripUndefined(body)) : undefined,
256
+ signal: controller.signal,
257
+ });
258
+ } catch (err) {
259
+ clearTimeout(timer);
260
+ if ((err as Error)?.name === "AbortError") {
261
+ throw new TryItOnError(`Request timed out after ${this.timeoutMs}ms.`, { errorName: "Timeout" });
262
+ }
263
+ throw new TryItOnError(`Network error: ${(err as Error)?.message ?? String(err)}`, { errorName: "NetworkError" });
264
+ } finally {
265
+ clearTimeout(timer);
266
+ }
267
+
268
+ const text = await res.text();
269
+ let data: any = {};
270
+ if (text) {
271
+ try {
272
+ data = JSON.parse(text);
273
+ } catch {
274
+ if (!res.ok) throw new TryItOnError(text || `HTTP ${res.status}`, { status: res.status });
275
+ }
276
+ }
277
+
278
+ if (!res.ok) {
279
+ throw new TryItOnError(data?.message ?? `HTTP ${res.status}`, {
280
+ status: res.status,
281
+ errorName: data?.error ?? null,
282
+ });
283
+ }
284
+
285
+ return data as T;
286
+ }
287
+ }
288
+
289
+ function stripUndefined(obj: unknown): unknown {
290
+ if (obj === null || typeof obj !== "object") return obj;
291
+ const out: Record<string, unknown> = {};
292
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
293
+ if (v !== undefined) out[k] = v;
294
+ }
295
+ return out;
296
+ }
297
+
298
+ export default TryItOn;