@meterapp/car-image-sdk 1.4.0 → 1.6.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/README.md +87 -16
- package/dist/client.d.ts +62 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +181 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +439 -131
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +393 -42
- package/dist/mcp/index.js.map +1 -1
- package/dist/params.d.ts +24 -2
- package/dist/params.d.ts.map +1 -1
- package/dist/params.js +92 -25
- package/dist/params.js.map +1 -1
- package/dist/types.d.ts +354 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @meterapp/car-image-sdk
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for the **Car Image API** by Meter — studio-quality, transparent-background renders of any vehicle in the open [`@meterapp/vehicle-db`](https://github.com/MeterApp/vehicle-db) catalog (1,599 makes, 44,254 models, model years 1990–2027).
|
|
3
|
+
TypeScript SDK for the **Car Image API** by Meter — studio-quality, transparent-background renders of any vehicle in the open [`@meterapp/vehicle-db`](https://github.com/MeterApp/vehicle-db) catalog (1,599 makes, 44,254 models, model years 1990–2027), in any paint color; plus stable vehicle ids, free VIN decoding and textured 3D models.
|
|
4
4
|
|
|
5
5
|
- Zero runtime dependencies. Works in Node 20+, Vercel/Cloudflare edge runtimes, Deno, Bun and browsers.
|
|
6
6
|
- Typed parameters and responses for every endpoint.
|
|
7
7
|
- `CarImageError` carries `status`, `title`, `detail`, `requestId` and the raw RFC 9457 problem.
|
|
8
8
|
- Automatic retries with full jitter on `429`/`503` (honoring `Retry-After`); never retries `402`.
|
|
9
|
-
- Signed-URL creation
|
|
9
|
+
- Signed-URL and 3D-model creation carry an `Idempotency-Key`, so a retried `POST` replays instead of billing twice.
|
|
10
10
|
- Shared MCP tool definitions (`@meterapp/car-image-sdk/mcp`) used by both the stdio and the remote MCP servers.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
@@ -15,7 +15,7 @@ npm install @meterapp/car-image-sdk
|
|
|
15
15
|
|
|
16
16
|
## Pricing in one line
|
|
17
17
|
|
|
18
|
-
Every delivered image costs **1 credit**, cold or CDN hit. **$1 = 1,000 credits**, every account starts with **100 free credits**, there is no subscription. Signed delivery URLs cost 1 credit when created; loading them is free.
|
|
18
|
+
Every delivered image costs **1 credit**, cold or CDN hit. **$1 = 1,000 credits**, every account starts with **100 free credits**, there is no subscription. Signed delivery URLs cost 1 credit when created; loading them is free. A 3D model costs **1,000 credits** ($1.00) when requested; VIN decoding, catalog lookups, polling and downloads are free.
|
|
19
19
|
|
|
20
20
|
Get a key at <https://carimage.dev/dashboard?ref=npm-sdk> or run `npx @meterapp/car-image login`.
|
|
21
21
|
|
|
@@ -32,7 +32,7 @@ const image = await client.getImage({
|
|
|
32
32
|
model: "911",
|
|
33
33
|
year: 2024,
|
|
34
34
|
view: "front-3-4", // front | front-3-4 | side | side-right | rear | rear-3-4
|
|
35
|
-
color: "red", // 15 presets
|
|
35
|
+
color: "red", // 15 presets (white black gray silver blue red green brown beige tan orange yellow gold burgundy purple) or any hex: "#1a2b3c"
|
|
36
36
|
size: "medium", // thumb=256 small=512 medium=768 large=1024 — or width/height, 1–1024 px each
|
|
37
37
|
format: "png", // png | webp | jpg | auto (auto negotiates WebP or PNG from the Accept header)
|
|
38
38
|
});
|
|
@@ -43,9 +43,75 @@ console.log(image.source, image.creditsCharged, image.creditsRemaining, image.re
|
|
|
43
43
|
|
|
44
44
|
`apiKey` and `baseUrl` fall back to `CAR_IMAGE_API_KEY` / `CAR_IMAGE_API_URL` when running on a server.
|
|
45
45
|
|
|
46
|
+
## Any paint color
|
|
47
|
+
|
|
48
|
+
`color` is one of the fifteen presets or any hex paint — `#1a2b3c`, `1a2b3c`, `#abc` or `abc`. Every image call takes it (`getImage`, `getImageUrl`, `createImageUrls`, `feedback`) and so does `create3dModel`. The SDK spells a hex bare in query strings (`color=1a2b3c`) and sends it as written in JSON bodies; responses echo it as `#1a2b3c`. A hex that matches a preset's own swatch is that preset. The `Color` type keeps the preset names in autocomplete while accepting any string; `isColor(value)` tells the two apart from anything else, and `COLORS` is still the list of presets.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await client.getImage({ make: "Porsche", model: "911", year: 2024, color: "#1a2b3c" });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Stable vehicle ids
|
|
55
|
+
|
|
56
|
+
Every catalog vehicle (one make, model and year) has a stable id such as `veh_395yw8tn73ff8`. It never changes across catalog releases, so it is what to store instead of three strings. Every catalog response carries them (`searchVehicles` hits get `id` for the year the query named and `ids` for every year; `vehicles({ year, makeId })` models get `id`; `resolve()` params get `vehicle_id`; `decodeVin()` names one), and every image or 3D call accepts `{ vehicle }` in place of `{ make, model, year }`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const { data } = await client.searchVehicles("2024 porsche 911");
|
|
60
|
+
const id = data.results[0].id; // "veh_395yw8tn73ff8"
|
|
61
|
+
await client.getImage({ vehicle: id, view: "side" }); // same image as make/model/year
|
|
62
|
+
await client.vehicle(id); // { make, model, year, years, image_path, images } — public, no key
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`ImageParams` is `{ make, model, year } | { vehicle }` plus the options, so TypeScript refuses both at once; at runtime whichever you pass is sent and the server answers `400` to both. Responses echo the id as `vehicle.vehicle_id` (null when the catalog cannot name one).
|
|
66
|
+
|
|
67
|
+
## VIN decoding
|
|
68
|
+
|
|
69
|
+
`decodeVin(vin, { year? })` decodes a full 17-character VIN, or a partial one of at least 5 characters with `*` for unknown positions, with NHTSA vPIC data — free, key required. `data.vehicle` is the catalog vehicle it maps to, with the id that renders it, or null when the catalog does not carry the car.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const { data } = await client.decodeVin("1HGCM82633A004352");
|
|
73
|
+
data.year, data.make, data.model, data.trim; // 2003, "Honda", "Accord", "EX-V6"
|
|
74
|
+
data.engine, data.transmission, data.plant; // typed sub-objects; unknown values are null
|
|
75
|
+
data.attributes; // every decoded vPIC variable, verbatim
|
|
76
|
+
if (data.vehicle) await client.getImage({ vehicle: data.vehicle.id });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`valid`, `errors` and `suggested_vin` carry the decoder's diagnostics (a bad check digit, a model-year mismatch with the `year` hint). A VIN NHTSA has no record of is a `404`; when neither decoder answers it is a `502`.
|
|
80
|
+
|
|
81
|
+
## 3D models
|
|
82
|
+
|
|
83
|
+
`create3dModel` builds a textured 3D model — GLB, USDZ, FBX, a PNG thumbnail and `glb_web`, a browser build a tenth the size — of any catalog vehicle in any paint color, from the same renders the images use. It costs 1,000 credits at creation whether the model is cached, in progress or new, and nothing for a vehicle and color your account already owns (`billing.already_owned`; a failed model is refunded); polling, downloads and publishing are free. A model takes one to seven minutes.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const { data: model, billing } = await client.create3dModel(
|
|
87
|
+
{ make: "Toyota", model: "Camry", year: 2025 }, // or { vehicle: "veh_…" }
|
|
88
|
+
{ color: "#1a2b3c", webhookUrl: "https://example.com/hooks/3d", webhookSecret: process.env.HOOK_SECRET, publish: true }
|
|
89
|
+
);
|
|
90
|
+
model.status; // "queued" | "processing" | "ready" (cached) | "failed"
|
|
91
|
+
billing.already_owned; // true (and credits_charged 0) when you already owned this vehicle in this color
|
|
92
|
+
model.public?.embed.html; // '<script src="https://carimage.dev/embed/3d.js" async></script>\n<car-3d model="m3d_…"></car-3d>'
|
|
93
|
+
|
|
94
|
+
const ready = await client.wait3dModel(model.id, { // polls GET /api/v1/3d/{id}
|
|
95
|
+
intervalMs: 5_000, // default 5 s
|
|
96
|
+
timeoutMs: 600_000, // default 10 min; throws a CarImageError with status 408
|
|
97
|
+
onProgress: (state) => console.log(state.status, state.progress, state.stage, state.estimated_seconds_remaining),
|
|
98
|
+
});
|
|
99
|
+
if (ready.status === "ready") {
|
|
100
|
+
const glb = await client.download3dModel(ready.id, "glb"); // { bytes, contentType } — also "usdz" | "fbx" | "thumbnail"
|
|
101
|
+
await writeFile("camry.glb", glb.bytes);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await client.get3dModel(model.id); // one poll
|
|
105
|
+
await client.list3dModels({ limit: 20 }); // your recent requests + pricing.credits_per_model
|
|
106
|
+
await client.publish3dModel(model.id); // data.public: key-free URLs (glb, usdz, poster) and the embed; works before ready
|
|
107
|
+
await client.unpublish3dModel(model.id); // the links stop working within the hour
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`ready.files.<kind>.url` are API URLs that answer `302` to a signed download valid for an hour; `download3dModel` follows that redirect itself and never sends the API key to the storage host. A published model's `data.public.files` need no key at all: `model.glb` (the browser build), `model.usdz` and `poster.png` redirect to immutable copies on the CDN, and `data.public.embed.html` is a two-line `<car-3d>` embed that shows the poster at once, loads Google's `<model-viewer>` when it scrolls into view and takes `view`, `spin`, `backdrop`, `ar`, `static`, `no-zoom` and `alt` (`fetch` only attaches it to the client's own origin). With `webhookUrl` (https, public host) the server POSTs `{ event: "3d_model.ready" | "3d_model.failed", data, sent_at }` once the model settles, signed as `X-CarImage-Signature: sha256=<hex HMAC of the raw body>` when `webhookSecret` is set. `create3dModel` sends an `Idempotency-Key` like `createImageUrls` (pass `idempotencyKey` to make retries across processes safe). A `503` means 3D models are switched off or at the day's capacity; nothing is charged and `retryAfterSeconds` says when to try again.
|
|
111
|
+
|
|
46
112
|
## Sizing, backgrounds and trimming
|
|
47
113
|
|
|
48
|
-
Six views,
|
|
114
|
+
Six views, any paint color, PNG/WebP/JPG up to 1024 px — delivered at whatever box your layout needs.
|
|
49
115
|
|
|
50
116
|
```ts
|
|
51
117
|
await client.getImage({
|
|
@@ -61,6 +127,7 @@ await client.getImage({
|
|
|
61
127
|
|
|
62
128
|
| Parameter | Values | What it does |
|
|
63
129
|
| --- | --- | --- |
|
|
130
|
+
| `color` | one of `COLORS` · hex as `#rrggbb`, `rrggbb`, `#rgb` or `rgb` | The paint. Defaults to `silver`; echoed back as the preset name or `#rrggbb`. |
|
|
64
131
|
| `size` | `thumb` 256 · `small` 512 · `medium` 768 · `large` 1024 | Square presets; shorthand for `width`. |
|
|
65
132
|
| `width`, `height` | 1–1024 px each | One dimension keeps the aspect ratio. Both together give exactly `width`×`height` (see `fit`). Nothing is upscaled past 1024. |
|
|
66
133
|
| `fit` | `contain` (default) · `cover` · `inside` | Only matters with both dimensions. `contain` returns exactly the box with the whole car and transparent (or `background`) padding; `cover` fills the box and centre-crops; `inside` keeps the car within the box and may return a smaller image — the old behaviour. |
|
|
@@ -69,7 +136,7 @@ await client.getImage({
|
|
|
69
136
|
| `padding` | 0–50 | Margin around a trimmed car, as a percentage of its longer side. Only with `trim`. |
|
|
70
137
|
| `format` | `png` (default) · `webp` · `jpg` · `auto` | `auto` picks WebP for clients whose `Accept` header lists it and PNG otherwise (`Vary: Accept`); a signed URL created with `auto` negotiates on every load. |
|
|
71
138
|
|
|
72
|
-
The same parameters apply to `getImageUrl` and to every entry of `createImageUrls`; the returned `vehicle` echoes `fit`, `background`, `trim` and `padding` back. `assertImageParams` (and therefore every call) rejects a `padding` without `trim
|
|
139
|
+
The same parameters apply to `getImageUrl` and to every entry of `createImageUrls`; the returned `vehicle` echoes `vehicle_id`, `color`, `fit`, `background`, `trim` and `padding` back. `assertImageParams` (and therefore every call) rejects a `padding` without `trim`, dimensions outside 1–1024, a color that is neither a preset nor a hex, and a request that names neither a vehicle id nor all of make, model and year, before anything is sent.
|
|
73
140
|
|
|
74
141
|
## Next.js route handler
|
|
75
142
|
|
|
@@ -122,13 +189,17 @@ await client.createImageUrls(images, { ttlSeconds: 604_800, idempotencyKey: `ord
|
|
|
122
189
|
|
|
123
190
|
```ts
|
|
124
191
|
await client.getImageUrl(params); // 1 credit, returns a signed URL instead of bytes
|
|
125
|
-
await client.options(); // views, colors, sizes, fits, backgrounds, trim limits, formats, pricing, catalog coverage
|
|
126
|
-
await client.resolve("red 2024 porsche 911 side view"); // free text -> { make, model, year, view, color } + candidates + confidence high|medium|low
|
|
192
|
+
await client.options(); // views, colors (and how hex paint is spelled), sizes, fits, backgrounds, trim limits, formats, pricing, catalog coverage, vehicle ids
|
|
193
|
+
await client.resolve("red 2024 porsche 911 side view"); // free text -> { vehicle_id, make, model, year, view, color } + candidates + confidence high|medium|low
|
|
127
194
|
await client.vehicles(); // years
|
|
128
195
|
await client.vehicles({ year: 2024 }); // makes for a year
|
|
129
|
-
await client.vehicles({ year: 2024, makeId: 7 }); // models for a year + make
|
|
130
|
-
await client.
|
|
131
|
-
await client.
|
|
196
|
+
await client.vehicles({ year: 2024, makeId: 7 }); // models for a year + make, each with its vehicle id
|
|
197
|
+
await client.vehicles({ make: "porsche", model: "911" }); // a model's years, each with its vehicle id
|
|
198
|
+
await client.vehicle("veh_395yw8tn73ff8"); // one vehicle by stable id (public)
|
|
199
|
+
await client.searchVehicles("porshe 911", { limit: 5 }); // fuzzy catalog search; model hits carry ids
|
|
200
|
+
await client.decodeVin("1HGCM82633A004352"); // free VIN decode + the catalog vehicle it maps to
|
|
201
|
+
await client.create3dModel({ vehicle: id }, { color }); // 1,000 credits; then wait3dModel, get3dModel, list3dModels, download3dModel
|
|
202
|
+
await client.feedback({ requestId, verdict: "good" }); // or { make, model, year, rating: 4, reason } or { vehicle, verdict }
|
|
132
203
|
await client.listRequests({ sort: "top" }); // vehicle and feature requests; see "Requests & feedback"
|
|
133
204
|
await client.account(); // credits, auto-reload, usage_30d, key scopes
|
|
134
205
|
await client.createCheckout(5000); // hosted Stripe Checkout URL (scope billing:write)
|
|
@@ -173,12 +244,12 @@ try {
|
|
|
173
244
|
await client.getImage({ make: "Porsche", model: "911", year: 2024 });
|
|
174
245
|
} catch (error) {
|
|
175
246
|
if (error instanceof CarImageError) {
|
|
176
|
-
error.status; // 400 | 401 | 402 | 404 | 409 | 422 | 429 | 502 | 0 (network)
|
|
247
|
+
error.status; // 400 | 401 | 402 | 404 | 409 | 422 | 429 | 502 | 503 | 408 (wait3dModel timeout) | 0 (network)
|
|
177
248
|
error.title; // "Insufficient credits"
|
|
178
249
|
error.detail; // "This request needs 1 credit; balance is 0"
|
|
179
250
|
error.requestId; // "0d1e…" — quote this when contacting support
|
|
180
251
|
error.problem; // the raw application/problem+json body (402 includes balance, required_credits)
|
|
181
|
-
error.retryAfterSeconds; // set on 429,
|
|
252
|
+
error.retryAfterSeconds; // set on 429, on a 409 (Idempotency-Key conflict, or a 3D file requested before the model is ready) and on a 503
|
|
182
253
|
if (error.isInsufficientCredits) {
|
|
183
254
|
// Send a human to https://carimage.dev/dashboard?ref=npm-sdk#billing — never buy credits automatically.
|
|
184
255
|
}
|
|
@@ -199,13 +270,13 @@ import {
|
|
|
199
270
|
} from "@meterapp/car-image-sdk/mcp";
|
|
200
271
|
```
|
|
201
272
|
|
|
202
|
-
Tools come in two sets. `MCP_TOOLSETS.core` (`DEFAULT_MCP_TOOLSET`, `"core"`) is the
|
|
273
|
+
Tools come in two sets. `MCP_TOOLSETS.core` (`DEFAULT_MCP_TOOLSET`, `"core"`) is the eleven an agent needs to find, render, embed and model a car: `get_car_image`, `create_car_image_urls`, `search_vehicles`, `resolve_vehicle`, `decode_vin`, `list_image_options`, `get_account`, `rate_image`, `create_3d_model`, `get_3d_model`, `describe_api`. `MCP_TOOLSETS.all` adds the request board and the two "about you" tools: `list_requests`, `request_vehicle`, `request_feature`, `get_request`, `upvote_request`, `comment_on_request`, `share_building`, `share_referral`. The hosted server exposes `core` at `https://carimage.dev/api/mcp` and `all` at `https://carimage.dev/api/mcp?toolset=all`; the stdio server takes `car-image mcp --toolset all`. `mcpInstructions(toolset)` returns the matching server instructions (`MCP_INSTRUCTIONS` is the `all` text) and `isMcpToolset(value)` validates a name from a URL or a flag.
|
|
203
274
|
|
|
204
|
-
`get_car_image
|
|
275
|
+
`get_car_image`, each `create_car_image_urls` entry, `rate_image` and `create_3d_model` name the vehicle by make, model and year or by `vehicle` (a stable id) and take `color` as a preset or a hex; the image tools also take `fit`, `background`, `trim` and `padding` alongside view, size and format (`auto` included; `inlineImageFormat` is what both servers deliver an inline image as for it: png), and `create_car_image_urls` honors `renew` and `renew_days` and takes `idempotency_key` (`IDEMPOTENCY_KEY_PATTERN`), which the stdio server forwards as the `Idempotency-Key` header. `create_3d_model` costs 1,000 credits, and its description and the server instructions tell the agent to confirm with the human first. Every tool but `describe_api`, whose result is the reference prose itself, advertises an `outputSchema` and returns `structuredContent` that matches it, so a host can hand the model typed data instead of re-parsing the text block. The schemas allow unknown fields: the payloads mirror the REST API and gain fields as it grows.
|
|
205
276
|
|
|
206
277
|
## Types
|
|
207
278
|
|
|
208
|
-
`View`, `Color
|
|
279
|
+
`View`, `Color` (a `PresetColor` or any hex string), `Size`, `Format`, `RequestFormat`, `Fit`, `VehicleId`, `VehicleSelector`, `ImageParams`, `ImageOptions`, `ImageResult`, `CreateImageUrlsOptions`, `CreateImageUrlsResponse`, `OptionsResponse`, `FitOption`, `ResolveResponse`, `ResolveConfidence` (`"high" | "medium" | "low"`), `VehicleSearchResponse`, `VehicleResponse`, `VinResult`, `VinResponse`, `Model3d`, `Model3dFileKind`, `Model3dStatus`, `CreateModel3dOptions`, `CreateModel3dResponse`, `Model3dListResponse`, `Model3dFileResult`, `WaitModel3dOptions`, `AccountResponse`, `RequestRecord`, `RequestComment`, `CreateRequestInput`, `ReferralSource`, … plus the constant arrays `VIEWS`, `COLORS` (the presets), `SIZES`, `FORMATS`, `REQUEST_FORMATS` (the formats plus `auto`), `FITS`, `MODEL_3D_FILE_KINDS`, `REQUEST_STATUSES`, `REFERRAL_SOURCES`, the patterns `HEX_COLOR_PATTERN` and `VEHICLE_ID_PATTERN`, the helpers `isColor`, `isPresetColor`, `isHexColor`, `colorParam` and `vehicleRequestBody`, and the limits `MAX_DIMENSION` (1024), `MAX_PADDING_PERCENT` (50) and `CREDITS_PER_3D_MODEL` (1000).
|
|
209
280
|
|
|
210
281
|
## License
|
|
211
282
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AccountResponse, type BuildingResponse, type CheckoutResponse, type CreateImageUrlsOptions, type CreateImageUrlsResponse, type CreateRequestInput, type CreateRequestResponse, type DeviceCodeResponse, type DeviceTokenResult, type FeedbackInput, type FeedbackResponse, type FetchLike, type HealthResponse, type ImageParams, type ImageResult, type ImageUrlResponse, type ListRequestsParams, type OpenApiDocument, type OptionsResponse, type PortalResponse, type ReferralResponse, type ReferralSource, type RequestCommentResponse, type RequestCommentsResponse, type RequestDetailResponse, type RequestListResponse, type RequestResponse, type ResolveResponse, type TelemetryEvent, type VehicleMakesResponse, type VehicleModelsResponse, type VehicleSearchResponse, type VehicleYearsResponse } from "./types.js";
|
|
1
|
+
import { type AccountResponse, type BuildingResponse, type CheckoutResponse, type CreateImageUrlsOptions, type CreateImageUrlsResponse, type CreateModel3dOptions, type CreateModel3dResponse, type CreateRequestInput, type CreateRequestResponse, type DecodeVinOptions, type DeviceCodeResponse, type DeviceTokenResult, type FeedbackInput, type FeedbackResponse, type FetchLike, type HealthResponse, type ImageParams, type ImageResult, type ImageUrlResponse, type ListRequestsParams, type Model3d, type Model3dFileKind, type Model3dFileResult, type Model3dListResponse, type Model3dResponse, type OpenApiDocument, type OptionsResponse, type PortalResponse, type ReferralResponse, type ReferralSource, type RequestCommentResponse, type RequestCommentsResponse, type RequestDetailResponse, type RequestListResponse, type RequestResponse, type ResolveResponse, type TelemetryEvent, type VehicleMakeModelsResponse, type VehicleMakesResponse, type VehicleModelLookupResponse, type VehicleModelsResponse, type VehicleResponse, type VehicleSearchResponse, type VehicleSelector, type VehicleYearsResponse, type VinResponse, type WaitModel3dOptions } from "./types.js";
|
|
2
2
|
export interface CarImageClientOptions {
|
|
3
3
|
/** `cimg_…` key. Falls back to `process.env.CAR_IMAGE_API_KEY` when running on a server. */
|
|
4
4
|
apiKey?: string;
|
|
@@ -62,7 +62,11 @@ export declare class CarImageClient {
|
|
|
62
62
|
options(options?: RequestOptions): Promise<OptionsResponse>;
|
|
63
63
|
/** POST /api/v1/images/resolve — free; turns "red 2024 porsche 911 side view" into parameters. */
|
|
64
64
|
resolve(query: string, options?: RequestOptions): Promise<ResolveResponse>;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* GET /api/v1/vehicles — public. No filter: years; `year`: makes; `year`+`makeId`:
|
|
67
|
+
* models (each with its vehicle id); `make` slug: that make's models; `make`+`model`
|
|
68
|
+
* slugs: the model's years, each with its vehicle id.
|
|
69
|
+
*/
|
|
66
70
|
vehicles(filter?: Record<string, never>, options?: RequestOptions): Promise<VehicleYearsResponse>;
|
|
67
71
|
vehicles(filter: {
|
|
68
72
|
year: number;
|
|
@@ -71,12 +75,67 @@ export declare class CarImageClient {
|
|
|
71
75
|
year: number;
|
|
72
76
|
makeId: number;
|
|
73
77
|
}, options?: RequestOptions): Promise<VehicleModelsResponse>;
|
|
78
|
+
vehicles(filter: {
|
|
79
|
+
make: string;
|
|
80
|
+
year?: number;
|
|
81
|
+
}, options?: RequestOptions): Promise<VehicleMakeModelsResponse>;
|
|
82
|
+
vehicles(filter: {
|
|
83
|
+
make: string;
|
|
84
|
+
model: string;
|
|
85
|
+
year?: number;
|
|
86
|
+
}, options?: RequestOptions): Promise<VehicleModelLookupResponse>;
|
|
87
|
+
/** GET /api/v1/vehicles/{id} — public; the vehicle a stable id names, its years and the image URLs that render it. */
|
|
88
|
+
vehicle(id: string, options?: RequestOptions): Promise<VehicleResponse>;
|
|
89
|
+
/**
|
|
90
|
+
* GET /api/v1/vin/{vin} — free; decodes a full 17-character VIN, or a partial
|
|
91
|
+
* one of at least 5 characters with `*` for unknown positions, and names the
|
|
92
|
+
* catalog vehicle it maps to (`data.vehicle`, with the id that renders it).
|
|
93
|
+
*/
|
|
94
|
+
decodeVin(vin: string, options?: DecodeVinOptions & RequestOptions): Promise<VinResponse>;
|
|
95
|
+
/**
|
|
96
|
+
* POST /api/v1/3d — a textured 3D model (GLB, USDZ, FBX and a thumbnail) of one
|
|
97
|
+
* vehicle in one paint color. Costs `credits_per_model` credits (1,000 at
|
|
98
|
+
* launch) at creation, whether the model is cached, in progress or new; a
|
|
99
|
+
* failed model is refunded, and polling and downloads are free. Resolves at
|
|
100
|
+
* once with `status` queued or processing (202), or ready when it was cached
|
|
101
|
+
* (200); `wait3dModel` polls until it settles. Every call carries an
|
|
102
|
+
* `Idempotency-Key`, like `createImageUrls`.
|
|
103
|
+
*/
|
|
104
|
+
create3dModel(vehicle: VehicleSelector, options?: CreateModel3dOptions & RequestOptions): Promise<CreateModel3dResponse>;
|
|
105
|
+
/** GET /api/v1/3d/{id} — free; status, progress and, once ready, the files of one 3D request. */
|
|
106
|
+
get3dModel(id: string, options?: RequestOptions): Promise<Model3dResponse>;
|
|
107
|
+
/**
|
|
108
|
+
* POST /api/v1/3d/{id}/publish — free; gives one of your 3D requests
|
|
109
|
+
* key-free URLs and a two-line embed (`data.public`). The URLs are stable
|
|
110
|
+
* from this moment; the files follow when the model is ready. Idempotent.
|
|
111
|
+
*/
|
|
112
|
+
publish3dModel(id: string, options?: RequestOptions): Promise<Model3dResponse>;
|
|
113
|
+
/** DELETE /api/v1/3d/{id}/publish — free; the public URLs stop working within the hour and `data.public` is null again. Idempotent. */
|
|
114
|
+
unpublish3dModel(id: string, options?: RequestOptions): Promise<Model3dResponse>;
|
|
115
|
+
/** GET /api/v1/3d — free; the caller's recent 3D requests, newest first (`limit` 1–50, default 20). */
|
|
116
|
+
list3dModels(options?: {
|
|
117
|
+
limit?: number;
|
|
118
|
+
} & RequestOptions): Promise<Model3dListResponse>;
|
|
119
|
+
/**
|
|
120
|
+
* GET /api/v1/3d/{id}/files/{kind} — free; the bytes of one file of a ready
|
|
121
|
+
* model. The route answers 302 to a short-lived signed URL on the storage
|
|
122
|
+
* host, which is followed here by hand so the API key never travels there:
|
|
123
|
+
* `fetch` attaches the key only to the client's own origin.
|
|
124
|
+
*/
|
|
125
|
+
download3dModel(id: string, kind: Model3dFileKind, options?: RequestOptions): Promise<Model3dFileResult>;
|
|
126
|
+
/**
|
|
127
|
+
* Polls GET /api/v1/3d/{id} until the model is `ready` or `failed` and
|
|
128
|
+
* returns it either way (check `status`; a failed model carries `error` and
|
|
129
|
+
* was refunded). Throws a `CarImageError` with status 408 when `timeoutMs`
|
|
130
|
+
* (default 10 minutes) passes first; `intervalMs` defaults to 5 seconds.
|
|
131
|
+
*/
|
|
132
|
+
wait3dModel(id: string, options?: WaitModel3dOptions & RequestOptions): Promise<Model3d>;
|
|
74
133
|
/** GET /api/v1/vehicles?q= — public fuzzy search across makes and models. */
|
|
75
134
|
searchVehicles(query: string, options?: {
|
|
76
135
|
limit?: number;
|
|
77
136
|
year?: number;
|
|
78
137
|
} & RequestOptions): Promise<VehicleSearchResponse>;
|
|
79
|
-
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle. */
|
|
138
|
+
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle (make, model and year, or a vehicle id). */
|
|
80
139
|
feedback(input: FeedbackInput, options?: RequestOptions): Promise<FeedbackResponse>;
|
|
81
140
|
/** GET /api/v1/requests — public board of vehicle and feature requests; `viewer.voted` is filled in when a key is sent. */
|
|
82
141
|
listRequests(params?: ListRequestsParams, options?: RequestOptions): Promise<RequestListResponse>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CarImageError } from "./errors.js";
|
|
2
|
-
import { imageRequestBody, imageSearchParams, normalizeYear } from "./params.js";
|
|
2
|
+
import { imageRequestBody, imageSearchParams, isColor, normalizeYear, vehicleRequestBody } from "./params.js";
|
|
3
3
|
import { RETRYABLE_STATUSES, backoffDelayMs, parseRetryAfter } from "./retry.js";
|
|
4
|
-
import { DEFAULT_BASE_URL, REFERRAL_SOURCES, } from "./types.js";
|
|
4
|
+
import { COLORS, DEFAULT_BASE_URL, MODEL_3D_FILE_KINDS, REFERRAL_SOURCES, } from "./types.js";
|
|
5
5
|
import { SDK_VERSION } from "./version.js";
|
|
6
6
|
function readEnv(name) {
|
|
7
7
|
const proc = globalThis.process;
|
|
@@ -173,10 +173,185 @@ export class CarImageClient {
|
|
|
173
173
|
search.set("year", String(normalizeYear(filter.year)));
|
|
174
174
|
if (filter.makeId !== undefined)
|
|
175
175
|
search.set("makeId", String(filter.makeId));
|
|
176
|
+
if (filter.make !== undefined)
|
|
177
|
+
search.set("make", filter.make.trim());
|
|
178
|
+
if (filter.model !== undefined)
|
|
179
|
+
search.set("model", filter.model.trim());
|
|
176
180
|
const query = search.size ? `?${search}` : "";
|
|
177
181
|
const response = await this.send(`/api/v1/vehicles${query}`, { ...options, auth: "optional" });
|
|
178
182
|
return this.json(response);
|
|
179
183
|
}
|
|
184
|
+
/** GET /api/v1/vehicles/{id} — public; the vehicle a stable id names, its years and the image URLs that render it. */
|
|
185
|
+
async vehicle(id, options = {}) {
|
|
186
|
+
if (typeof id !== "string" || !id.trim())
|
|
187
|
+
throw new TypeError("vehicle id is required");
|
|
188
|
+
const response = await this.send(`/api/v1/vehicles/${encodeURIComponent(id.trim())}`, { ...options, auth: "optional" });
|
|
189
|
+
return this.json(response);
|
|
190
|
+
}
|
|
191
|
+
// ------------------------------------------------------------------ VIN
|
|
192
|
+
/**
|
|
193
|
+
* GET /api/v1/vin/{vin} — free; decodes a full 17-character VIN, or a partial
|
|
194
|
+
* one of at least 5 characters with `*` for unknown positions, and names the
|
|
195
|
+
* catalog vehicle it maps to (`data.vehicle`, with the id that renders it).
|
|
196
|
+
*/
|
|
197
|
+
async decodeVin(vin, options = {}) {
|
|
198
|
+
if (typeof vin !== "string" || !vin.trim())
|
|
199
|
+
throw new TypeError("vin is required");
|
|
200
|
+
const { year, ...request } = options;
|
|
201
|
+
const search = new URLSearchParams();
|
|
202
|
+
if (year !== undefined)
|
|
203
|
+
search.set("year", String(normalizeYear(year)));
|
|
204
|
+
const query = search.size ? `?${search}` : "";
|
|
205
|
+
const response = await this.send(`/api/v1/vin/${encodeURIComponent(vin.trim())}${query}`, request);
|
|
206
|
+
return this.json(response);
|
|
207
|
+
}
|
|
208
|
+
// ------------------------------------------------------------ 3D models
|
|
209
|
+
/**
|
|
210
|
+
* POST /api/v1/3d — a textured 3D model (GLB, USDZ, FBX and a thumbnail) of one
|
|
211
|
+
* vehicle in one paint color. Costs `credits_per_model` credits (1,000 at
|
|
212
|
+
* launch) at creation, whether the model is cached, in progress or new; a
|
|
213
|
+
* failed model is refunded, and polling and downloads are free. Resolves at
|
|
214
|
+
* once with `status` queued or processing (202), or ready when it was cached
|
|
215
|
+
* (200); `wait3dModel` polls until it settles. Every call carries an
|
|
216
|
+
* `Idempotency-Key`, like `createImageUrls`.
|
|
217
|
+
*/
|
|
218
|
+
async create3dModel(vehicle, options = {}) {
|
|
219
|
+
const { color, webhookUrl, webhookSecret, publish, idempotencyKey, ...request } = options;
|
|
220
|
+
const body = vehicleRequestBody(vehicle);
|
|
221
|
+
if (color !== undefined) {
|
|
222
|
+
if (!isColor(color))
|
|
223
|
+
throw new TypeError(`Invalid color "${String(color)}": expected one of ${COLORS.join(", ")}, or a hex paint such as #1a2b3c`);
|
|
224
|
+
body.color = String(color).trim();
|
|
225
|
+
}
|
|
226
|
+
if (webhookUrl !== undefined)
|
|
227
|
+
body.webhook_url = webhookUrl;
|
|
228
|
+
if (webhookSecret !== undefined)
|
|
229
|
+
body.webhook_secret = webhookSecret;
|
|
230
|
+
if (publish !== undefined)
|
|
231
|
+
body.publish = publish;
|
|
232
|
+
const key = idempotencyKey ?? generateIdempotencyKey();
|
|
233
|
+
const headers = { ...request.headers };
|
|
234
|
+
if (key)
|
|
235
|
+
headers["Idempotency-Key"] = key;
|
|
236
|
+
const response = await this.send("/api/v1/3d", {
|
|
237
|
+
...request,
|
|
238
|
+
headers,
|
|
239
|
+
method: "POST",
|
|
240
|
+
body: JSON.stringify(body),
|
|
241
|
+
});
|
|
242
|
+
return this.json(response);
|
|
243
|
+
}
|
|
244
|
+
/** GET /api/v1/3d/{id} — free; status, progress and, once ready, the files of one 3D request. */
|
|
245
|
+
async get3dModel(id, options = {}) {
|
|
246
|
+
const response = await this.send(`/api/v1/3d/${requestPath(id)}`, options);
|
|
247
|
+
return this.json(response);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* POST /api/v1/3d/{id}/publish — free; gives one of your 3D requests
|
|
251
|
+
* key-free URLs and a two-line embed (`data.public`). The URLs are stable
|
|
252
|
+
* from this moment; the files follow when the model is ready. Idempotent.
|
|
253
|
+
*/
|
|
254
|
+
async publish3dModel(id, options = {}) {
|
|
255
|
+
const response = await this.send(`/api/v1/3d/${requestPath(id)}/publish`, { ...options, method: "POST" });
|
|
256
|
+
return this.json(response);
|
|
257
|
+
}
|
|
258
|
+
/** DELETE /api/v1/3d/{id}/publish — free; the public URLs stop working within the hour and `data.public` is null again. Idempotent. */
|
|
259
|
+
async unpublish3dModel(id, options = {}) {
|
|
260
|
+
const response = await this.send(`/api/v1/3d/${requestPath(id)}/publish`, { ...options, method: "DELETE" });
|
|
261
|
+
return this.json(response);
|
|
262
|
+
}
|
|
263
|
+
/** GET /api/v1/3d — free; the caller's recent 3D requests, newest first (`limit` 1–50, default 20). */
|
|
264
|
+
async list3dModels(options = {}) {
|
|
265
|
+
const { limit, ...request } = options;
|
|
266
|
+
const search = new URLSearchParams();
|
|
267
|
+
if (limit !== undefined)
|
|
268
|
+
search.set("limit", String(limit));
|
|
269
|
+
const query = search.size ? `?${search}` : "";
|
|
270
|
+
const response = await this.send(`/api/v1/3d${query}`, request);
|
|
271
|
+
return this.json(response);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* GET /api/v1/3d/{id}/files/{kind} — free; the bytes of one file of a ready
|
|
275
|
+
* model. The route answers 302 to a short-lived signed URL on the storage
|
|
276
|
+
* host, which is followed here by hand so the API key never travels there:
|
|
277
|
+
* `fetch` attaches the key only to the client's own origin.
|
|
278
|
+
*/
|
|
279
|
+
async download3dModel(id, kind, options = {}) {
|
|
280
|
+
if (!MODEL_3D_FILE_KINDS.includes(kind)) {
|
|
281
|
+
throw new RangeError(`kind must be one of: ${MODEL_3D_FILE_KINDS.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
const redirect = await this.rawSend(`/api/v1/3d/${requestPath(id)}/files/${kind}`, { ...options, accept: "*/*", redirect: "manual" });
|
|
284
|
+
const requestId = redirect.headers.get("x-request-id");
|
|
285
|
+
const location = redirect.headers.get("location");
|
|
286
|
+
if (redirect.status >= 300 && redirect.status < 400 && location) {
|
|
287
|
+
try {
|
|
288
|
+
await redirect.body?.cancel();
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Nothing to release.
|
|
292
|
+
}
|
|
293
|
+
const target = new URL(location, this.baseUrl).toString();
|
|
294
|
+
const file = await this.fetch(target, { headers: { Accept: "*/*" }, signal: this.signalFor(options) });
|
|
295
|
+
if (!file.ok) {
|
|
296
|
+
throw new CarImageError({
|
|
297
|
+
status: file.status,
|
|
298
|
+
title: "3D file download failed",
|
|
299
|
+
detail: `The storage host answered HTTP ${file.status} for the ${kind} file. Ask for the model again to get a fresh link.`,
|
|
300
|
+
requestId,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
kind,
|
|
305
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
306
|
+
contentType: file.headers.get("content-type") ?? redirect.headers.get("x-file-content-type") ?? "application/octet-stream",
|
|
307
|
+
requestId,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (redirect.ok) {
|
|
311
|
+
return {
|
|
312
|
+
kind,
|
|
313
|
+
bytes: new Uint8Array(await redirect.arrayBuffer()),
|
|
314
|
+
contentType: redirect.headers.get("content-type") ?? "application/octet-stream",
|
|
315
|
+
requestId,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (redirect.status === 0 || redirect.type === "opaqueredirect") {
|
|
319
|
+
throw new CarImageError({
|
|
320
|
+
status: 0,
|
|
321
|
+
title: "Redirect hidden by the runtime",
|
|
322
|
+
detail: `This runtime does not expose redirects; fetch model.files.${kind}.url yourself and follow it without the API key.`,
|
|
323
|
+
requestId,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
throw await CarImageError.fromResponse(redirect);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Polls GET /api/v1/3d/{id} until the model is `ready` or `failed` and
|
|
330
|
+
* returns it either way (check `status`; a failed model carries `error` and
|
|
331
|
+
* was refunded). Throws a `CarImageError` with status 408 when `timeoutMs`
|
|
332
|
+
* (default 10 minutes) passes first; `intervalMs` defaults to 5 seconds.
|
|
333
|
+
*/
|
|
334
|
+
async wait3dModel(id, options = {}) {
|
|
335
|
+
const { intervalMs = 5_000, timeoutMs = 600_000, onProgress, ...request } = options;
|
|
336
|
+
const deadline = Date.now() + timeoutMs;
|
|
337
|
+
for (;;) {
|
|
338
|
+
if (request.signal?.aborted)
|
|
339
|
+
throw request.signal.reason ?? new Error("wait3dModel aborted");
|
|
340
|
+
const { data } = await this.get3dModel(id, request);
|
|
341
|
+
onProgress?.(data);
|
|
342
|
+
if (data.status === "ready" || data.status === "failed")
|
|
343
|
+
return data;
|
|
344
|
+
const remaining = deadline - Date.now();
|
|
345
|
+
if (remaining <= 0) {
|
|
346
|
+
throw new CarImageError({
|
|
347
|
+
status: 408,
|
|
348
|
+
title: "Timed out waiting for the 3D model",
|
|
349
|
+
detail: `Model ${id} is still ${data.status} (${data.progress}%) after ${Math.round(timeoutMs / 1000)} s. Keep polling get3dModel, or pass a webhookUrl when creating one.`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
await this.sleep(Math.min(Math.max(1, intervalMs), remaining));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
180
355
|
/** GET /api/v1/vehicles?q= — public fuzzy search across makes and models. */
|
|
181
356
|
async searchVehicles(query, options = {}) {
|
|
182
357
|
if (typeof query !== "string" || !query.trim())
|
|
@@ -191,7 +366,7 @@ export class CarImageClient {
|
|
|
191
366
|
return this.json(response);
|
|
192
367
|
}
|
|
193
368
|
// ------------------------------------------------------------- feedback
|
|
194
|
-
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle. */
|
|
369
|
+
/** POST /api/v1/feedback — free; rate a delivered image by request id or by vehicle (make, model and year, or a vehicle id). */
|
|
195
370
|
async feedback(input, options = {}) {
|
|
196
371
|
if (input.rating === undefined && input.verdict === undefined) {
|
|
197
372
|
throw new TypeError("feedback requires a rating (1-5) or a verdict (good|bad)");
|
|
@@ -200,14 +375,14 @@ export class CarImageClient {
|
|
|
200
375
|
if ("requestId" in input && input.requestId) {
|
|
201
376
|
body.request_id = input.requestId;
|
|
202
377
|
}
|
|
203
|
-
else if ("make" in input) {
|
|
378
|
+
else if ("make" in input || "vehicle" in input) {
|
|
204
379
|
Object.assign(body, imageRequestBody(input));
|
|
205
380
|
delete body.width;
|
|
206
381
|
delete body.height;
|
|
207
382
|
delete body.format;
|
|
208
383
|
}
|
|
209
384
|
else {
|
|
210
|
-
throw new TypeError("feedback requires requestId or make/model/year");
|
|
385
|
+
throw new TypeError("feedback requires requestId, a vehicle id, or make/model/year");
|
|
211
386
|
}
|
|
212
387
|
if (input.rating !== undefined)
|
|
213
388
|
body.rating = input.rating;
|
|
@@ -505,6 +680,7 @@ export class CarImageClient {
|
|
|
505
680
|
headers,
|
|
506
681
|
body: options.body,
|
|
507
682
|
signal: this.signalFor(options),
|
|
683
|
+
...(options.redirect ? { redirect: options.redirect } : {}),
|
|
508
684
|
});
|
|
509
685
|
}
|
|
510
686
|
catch (error) {
|