← Files Velox TriFit API DocsARCHIVED FILE
src/client.mjs
5.06 KB · Nov 2, 2026 · 13:06 UTC
// Demo adapter around the same public SDK shipped in the downloadable ZIP.
import { VeloxDressMe } from '../sdks/javascript/index.mjs';
export class ApiError extends Error {
constructor(message, { status, code, requestId, retryAfter } = {}) {
super(message);
this.name = 'ApiError';
Object.assign(this, { status, code, requestId, retryAfter });
}
}
export class DressMeClient {
constructor({ baseUrl, clientId, applicationKey, subject, fetch: fetchImpl = globalThis.fetch }) {
if (typeof window !== 'undefined') throw new Error('Use this client on your backend, never in a browser.');
const url = new URL(baseUrl);
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))) throw new Error('Use HTTPS or HTTP loopback.');
if (url.username || url.password || url.search || url.hash) throw new Error('Invalid API base URL.');
if (!clientId || !applicationKey?.startsWith('va_')) throw new Error('An assigned client ID and application credential are required.');
if (!/^[A-Za-z0-9_-]{1,128}$/.test(subject || '')) throw new Error('Use an opaque subject from your authenticated server session.');
this.baseUrl = baseUrl.replace(/\/$/, '');
this.fetch = fetchImpl.bind(globalThis);
this.headers = { 'X-Velox-Client-Id': clientId, 'X-Velox-Integration-Key': applicationKey, 'X-Velox-Subject': subject };
this.sdk = new VeloxDressMe({ baseUrl, clientId, integrationKey: applicationKey, subject, fetch: (...args) => this.fetch(...args), autoRefresh: false });
}
async request(path, { method = 'GET', body, headers = {}, signal } = {}) {
try { return await this.sdk.request(path, { method, body, headers, signal }); }
catch (error) { throw new ApiError(error.message, error); }
}
async verifyEnvironment(expected) {
const info = await this.request('/me');
if (info.kind !== 'application' || info.environment !== expected) throw new Error('Application/environment mismatch. Stop; never fall back to another environment.');
return info;
}
async upload(file, { role, fileName, onReserved }) {
if (!['personal', 'product'].includes(role)) throw new Error('Use personal or product role.');
if (!['image/png', 'image/jpeg', 'image/webp'].includes(file.type) || !file.size || file.size > 5 * 1024 * 1024) throw new Error('Use a PNG, JPEG or WebP image up to 5 MiB.');
if (typeof onReserved !== 'function') throw new Error('Persist the provisional asset ID with onReserved.');
const target = await this.request('/assets/upload-url', { method: 'POST', body: { role, fileName, contentType: file.type, sizeBytes: file.size } });
await onReserved(target.assetId);
const uploadUrl = new URL(target.uploadUrl);
const localApi = new URL(this.baseUrl);
if (uploadUrl.username || uploadUrl.password || (uploadUrl.protocol !== 'https:' && !(localApi.protocol === 'http:' && uploadUrl.origin === localApi.origin))) throw new Error('Unsafe upload URL.');
// The signed capability authorizes this upload. Never send API credentials to storage.
const transferred = await this.fetch(uploadUrl, { method: 'PUT', headers: target.uploadHeaders, body: file, redirect: 'error', signal: AbortSignal.timeout(30000) });
if (!transferred.ok) throw new ApiError('Upload failed; reconcile the saved asset ID before retrying.', { status: transferred.status, code: 'upload_failed' });
return this.completeUpload(target.assetId);
}
completeUpload(id) { return this.request(`/assets/${encodeURIComponent(id)}/complete`, { method: 'POST', body: {} }); }
getAsset(id) { return this.request(`/assets/${encodeURIComponent(id)}`); }
createJob({ personalAssetId, productAssetId }, idempotencyKey) {
if (!idempotencyKey) throw new Error('Save an idempotency key before submitting.');
return this.request('/dressme/jobs', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey }, body: { personalAssetId, productAssetId } });
}
async waitForJob(id, { timeoutMs = 120000, intervalMs = 1500, onStatus = () => {} } = {}) {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isFinite(intervalMs) || intervalMs < 10) throw new Error('Use positive timeoutMs and intervalMs >= 10.');
const signal = AbortSignal.timeout(timeoutMs);
while (true) {
signal.throwIfAborted();
const job = await this.request(`/dressme/jobs/${encodeURIComponent(id)}`, { signal });
await onStatus(job.status);
if (job.status === 'succeeded') return job;
if (job.status === 'failed') throw new ApiError(job.error?.message || 'Generation failed.', { code: job.error?.code || 'generation_failed' });
if (!['queued', 'running'].includes(job.status)) throw new Error('Unexpected job state. Stop and investigate.');
await new Promise((resolve, reject) => {
const abort = () => { clearTimeout(timer); reject(signal.reason); };
const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, intervalMs);
signal.addEventListener('abort', abort, { once: true });
if (signal.aborted) abort();
});
}
}
}
SHA-256: 8928f43ed21faf3b47bb53f07ea34432f551084749188d438f544cb1155f7702