@mappa-ai/mappa-node 1.1.1 → 1.2.1
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/dist/index.cjs +1732 -1
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -22
- package/dist/index.d.mts +128 -22
- package/dist/index.mjs +1717 -1
- package/dist/index.mjs.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1 +1,1732 @@
|
|
|
1
|
-
let e=require(`@paralleldrive/cuid2`);var t=class extends Error{name=`MappaError`;requestId;code;constructor(e,t){super(e),this.requestId=t?.requestId,this.code=t?.code,this.cause=t?.cause}},n=class extends t{name=`ApiError`;status;details;constructor(e,t){super(e,{requestId:t.requestId,code:t.code}),this.status=t.status,this.details=t.details}},r=class extends n{name=`RateLimitError`;retryAfterMs},i=class extends n{name=`AuthError`},a=class extends n{name=`ValidationError`},o=class extends t{name=`JobFailedError`;jobId;constructor(e,t,n){super(t,n),this.jobId=e}},s=class extends t{name=`JobCanceledError`;jobId;constructor(e,t,n){super(t,n),this.jobId=e}},c=class{constructor(e){this.transport=e}async getBalance(e){return(await this.transport.request({method:`GET`,path:`/v1/credits/balance`,requestId:e?.requestId,signal:e?.signal,retryable:!0})).data}async listTransactions(e){let t={};return e?.limit!==void 0&&(t.limit=String(e.limit)),e?.offset!==void 0&&(t.offset=String(e.offset)),(await this.transport.request({method:`GET`,path:`/v1/credits/transactions`,query:t,requestId:e?.requestId,signal:e?.signal,retryable:!0})).data}async*listAllTransactions(e){let t=0,n=e?.limit??50;for(;;){let r=await this.listTransactions({...e,limit:n,offset:t});for(let e of r.transactions)yield e;if(t+=r.transactions.length,t>=r.pagination.total)break}}async getJobUsage(e,n){if(!e)throw new t(`jobId is required`);return(await this.transport.request({method:`GET`,path:`/v1/credits/usage/${encodeURIComponent(e)}`,requestId:n?.requestId,signal:n?.signal,retryable:!0})).data}async hasEnough(e,t){return(await this.getBalance(t)).available>=e}async getAvailable(e){return(await this.getBalance(e)).available}};const l=/^[a-zA-Z0-9_-]{1,64}$/,u=10;function d(e){if(typeof e!=`string`)throw new t(`Tags must be strings`);if(!l.test(e))throw new t(`Invalid tag "${e}": must be 1-64 characters, alphanumeric with underscores and hyphens only`)}function f(e){if(!Array.isArray(e))throw new t(`tags must be an array`);if(e.length>10)throw new t(`Too many tags: maximum 10 per request`);for(let t of e)d(t)}var p=class{constructor(e){this.transport=e}async get(e,n){if(!e||typeof e!=`string`)throw new t(`entityId must be a non-empty string`);return(await this.transport.request({method:`GET`,path:`/v1/entities/${encodeURIComponent(e)}`,requestId:n?.requestId,signal:n?.signal,retryable:!0})).data}async list(e){let t={};return e?.tags&&(f(e.tags),t.tags=e.tags.join(`,`)),e?.cursor&&(t.cursor=e.cursor),e?.limit!==void 0&&(t.limit=String(e.limit)),(await this.transport.request({method:`GET`,path:`/v1/entities`,query:t,requestId:e?.requestId,signal:e?.signal,retryable:!0})).data}async*listAll(e){let t,n=!0;for(;n;){let r=await this.list({...e,cursor:t});for(let e of r.entities)yield e;t=r.cursor,n=r.hasMore}}async getByTag(e,t){return d(e),this.list({...t,tags:[e]})}async addTags(e,n,r){if(!e||typeof e!=`string`)throw new t(`entityId must be a non-empty string`);if(n.length===0)throw new t(`At least one tag is required`);return f(n),(await this.transport.request({method:`POST`,path:`/v1/entities/${encodeURIComponent(e)}/tags`,body:{tags:n},requestId:r?.requestId,signal:r?.signal,retryable:!0})).data}async removeTags(e,n,r){if(!e||typeof e!=`string`)throw new t(`entityId must be a non-empty string`);if(n.length===0)throw new t(`At least one tag is required`);return f(n),(await this.transport.request({method:`DELETE`,path:`/v1/entities/${encodeURIComponent(e)}/tags`,body:{tags:n},requestId:r?.requestId,signal:r?.signal,retryable:!0})).data}async setTags(e,n,r){if(!e||typeof e!=`string`)throw new t(`entityId must be a non-empty string`);return f(n),(await this.transport.request({method:`PUT`,path:`/v1/entities/${encodeURIComponent(e)}/tags`,body:{tags:n},requestId:r?.requestId,signal:r?.signal,retryable:!0})).data}},m=class{constructor(e){this.transport=e}async create(e){if(!!e.reportId==!!e.jobId)throw new t(`Provide exactly one of reportId or jobId`);return(await this.transport.request({method:`POST`,path:`/v1/feedback`,body:e,idempotencyKey:e.idempotencyKey,requestId:e.requestId,signal:e.signal,retryable:!0})).data}},h=class{constructor(e){this.transport=e}async upload(e){if(typeof FormData>`u`)throw new t(`FormData is not available in this runtime; cannot perform multipart upload`);let n=g(e.file,e.filename),r=e.contentType??n;if(!r)throw new t(`contentType is required when it cannot be inferred from file.type or filename`);let i=e.filename??_(e.file)??`upload`,a=await y(e.file,r),o=new FormData;return o.append(`file`,a,i),o.append(`contentType`,r),e.filename&&o.append(`filename`,e.filename),(await this.transport.request({method:`POST`,path:`/v1/files`,body:o,idempotencyKey:e.idempotencyKey,requestId:e.requestId,signal:e.signal,retryable:!0})).data}async get(e,n){if(!e)throw new t(`mediaId is required`);return(await this.transport.request({method:`GET`,path:`/v1/files/${encodeURIComponent(e)}`,requestId:n?.requestId,signal:n?.signal,retryable:!0})).data}async list(e){let t={};return e?.limit!==void 0&&(t.limit=String(e.limit)),e?.cursor&&(t.cursor=e.cursor),e?.includeDeleted!==void 0&&(t.includeDeleted=String(e.includeDeleted)),(await this.transport.request({method:`GET`,path:`/v1/files`,query:t,requestId:e?.requestId,signal:e?.signal,retryable:!0})).data}async*listAll(e){let t,n=!0;for(;n;){let r=await this.list({...e,cursor:t});for(let e of r.files)yield e;t=r.cursor,n=r.hasMore}}async setRetentionLock(e,n,r){if(!e)throw new t(`mediaId is required`);return(await this.transport.request({method:`PATCH`,path:`/v1/files/${encodeURIComponent(e)}/retention`,body:{lock:n},requestId:r?.requestId,signal:r?.signal,retryable:!0})).data}async delete(e,n){if(!e)throw new t(`mediaId is required`);return(await this.transport.request({method:`DELETE`,path:`/v1/files/${encodeURIComponent(e)}`,idempotencyKey:n?.idempotencyKey,requestId:n?.requestId,signal:n?.signal,retryable:!0})).data}};function g(e,t){if(typeof Blob<`u`&&e instanceof Blob&&e.type)return e.type;if(t)return v(t)}function _(e){if(typeof Blob<`u`&&e instanceof Blob){let t=e;if(typeof t.name==`string`&&t.name)return t.name}}function v(e){let t=e.lastIndexOf(`.`);if(!(t<0))switch(e.slice(t+1).toLowerCase()){case`mp4`:return`video/mp4`;case`mov`:return`video/quicktime`;case`webm`:return`video/webm`;case`mp3`:return`audio/mpeg`;case`wav`:return`audio/wav`;case`m4a`:return`audio/mp4`;case`png`:return`image/png`;case`jpg`:case`jpeg`:return`image/jpeg`;case`gif`:return`image/gif`;case`webp`:return`image/webp`;case`pdf`:return`application/pdf`;case`json`:return`application/json`;case`txt`:return`text/plain`;default:return}}async function y(e,n){if(typeof Blob<`u`&&e instanceof Blob){if(e.type===n)return e;let t=e;return typeof t.slice==`function`?t.slice(0,e.size,n):e}if(e instanceof ArrayBuffer||e instanceof Uint8Array)return new Blob([e],{type:n});if(typeof ReadableStream<`u`&&e instanceof ReadableStream){if(typeof Response>`u`)throw new t(`ReadableStream upload requires Response to convert stream to Blob`);let r=await new Response(e).blob();return r.slice(0,r.size,n)}throw new t(`Unsupported file type for upload()`)}var b=class{constructor(e){this.transport=e}async ping(){return(await this.transport.request({method:`GET`,path:`/v1/health/ping`,retryable:!0})).data}};const x=(0,e.init)({length:32});function S(e,t){let n=e.get(t);return n===null?void 0:n}function C(e){let t=.8+Math.random()*.4;return Math.floor(e*t)}function w(e,t,n){let r=t*2**Math.max(0,e-1);return Math.min(r,n)}function T(){return Date.now()}function E(e){return!!e&&typeof e.aborted==`boolean`}function D(){let e=Error(`The operation was aborted`);return e.name=`AbortError`,e}function O(e=`req`){return`${e}_${x()}`}var k=class{constructor(e){this.transport=e}async get(e,t){return(await this.transport.request({method:`GET`,path:`/v1/jobs/${encodeURIComponent(e)}`,requestId:t?.requestId,signal:t?.signal,retryable:!0})).data}async cancel(e,t){return(await this.transport.request({method:`POST`,path:`/v1/jobs/${encodeURIComponent(e)}/cancel`,idempotencyKey:t?.idempotencyKey,requestId:t?.requestId,signal:t?.signal,retryable:!0})).data}async wait(e,t){let n=t?.timeoutMs??5*6e4,r=t?.pollIntervalMs??1e3,i=t?.maxPollIntervalMs??1e4,a=T(),c=0,l,u;for(;;){if(t?.signal?.aborted)throw D();let d=await this.get(e,{signal:t?.signal});if(d.status!==u&&(u=d.status,t?.onEvent?.({type:`status`,job:d})),d.stage&&d.stage!==l&&(l=d.stage,t?.onEvent?.({type:`stage`,stage:d.stage,progress:d.progress,job:d})),d.status===`succeeded`)return t?.onEvent?.({type:`terminal`,job:d}),d;if(d.status===`failed`)throw t?.onEvent?.({type:`terminal`,job:d}),new o(e,d.error?.message??`Job failed`,{requestId:d.requestId,code:d.error?.code,cause:d.error});if(d.status===`canceled`)throw t?.onEvent?.({type:`terminal`,job:d}),new s(e,`Job canceled`,{requestId:d.requestId,cause:d.error});if(T()-a>n)throw new o(e,`Timed out waiting for job ${e} after ${n}ms`,{cause:{jobId:e,timeoutMs:n}});c+=1;let f=C(w(c,r,i));await new Promise(e=>setTimeout(e,f))}}async*stream(e,t){let n,r;for(;;){if(t?.signal?.aborted)return;let i=await this.get(e,{signal:t?.signal});if(i.status!==r){r=i.status;let e={type:`status`,job:i};t?.onEvent?.(e),yield e}if(i.stage&&i.stage!==n){n=i.stage;let e={type:`stage`,stage:i.stage,progress:i.progress,job:i};t?.onEvent?.(e),yield e}if(i.status===`succeeded`||i.status===`failed`||i.status===`canceled`){let e={type:`terminal`,job:i};t?.onEvent?.(e),yield e;return}await new Promise(e=>setTimeout(e,1e3))}}};function A(e){let n=e;if(!(e=>typeof e==`object`&&!!e)(n))throw new t(`media must be an object`);if(n.url!==void 0)throw new t(`media.url is not supported; pass { mediaId } or use createJobFromUrl()`);let r=n.mediaId;if(typeof r!=`string`||!r)throw new t(`media.mediaId must be a non-empty string`)}var j=class{constructor(e,t,n,r){this.transport=e,this.jobs=t,this.files=n,this.fetchImpl=r}async createJob(e){A(e.media);let t=e.idempotencyKey??this.defaultIdempotencyKey(e),n=await this.transport.request({method:`POST`,path:`/v1/reports/jobs`,body:this.normalizeJobRequest(e),idempotencyKey:t,requestId:e.requestId,retryable:!0}),r={...n.data,requestId:n.requestId??n.data.requestId};return r.handle=this.makeHandle(r.jobId),r}async createJobFromFile(e){let{file:t,contentType:n,filename:r,idempotencyKey:i,requestId:a,signal:o,...s}=e,c=await this.files.upload({file:t,contentType:n,filename:r,idempotencyKey:i,requestId:a,signal:o});return this.createJob({...s,media:{mediaId:c.mediaId},idempotencyKey:i,requestId:a})}async createJobFromUrl(e){let{url:n,contentType:r,filename:i,idempotencyKey:a,requestId:o,signal:s,...c}=e,l;try{l=new URL(n)}catch{throw new t(`url must be a valid URL`)}if(l.protocol!==`http:`&&l.protocol!==`https:`)throw new t(`url must use http: or https:`);let u=await this.fetchImpl(l.toString(),{signal:s});if(!u.ok)throw new t(`Failed to download url (status ${u.status})`);let d=u.headers.get(`content-type`)??void 0,f=r??d;if(!f)throw new t(`contentType is required when it cannot be inferred from the download response`);if(typeof Blob>`u`)throw new t(`Blob is not available in this runtime; cannot download and upload from url`);let p=await u.blob(),m=await this.files.upload({file:p,contentType:f,filename:i,idempotencyKey:a,requestId:o,signal:s});return this.createJob({...c,media:{mediaId:m.mediaId},idempotencyKey:a,requestId:o})}async get(e,t){return(await this.transport.request({method:`GET`,path:`/v1/reports/${encodeURIComponent(e)}`,requestId:t?.requestId,signal:t?.signal,retryable:!0})).data}async getByJob(e,t){return(await this.transport.request({method:`GET`,path:`/v1/reports/by-job/${encodeURIComponent(e)}`,requestId:t?.requestId,signal:t?.signal,retryable:!0})).data}async generate(e,n){let r=await this.createJob(e);if(!r.handle)throw new t(`Job receipt is missing handle`);return r.handle.wait(n?.wait)}async generateFromFile(e,n){let r=await this.createJobFromFile(e);if(!r.handle)throw new t(`Job receipt is missing handle`);return r.handle.wait(n?.wait)}async generateFromUrl(e,n){let r=await this.createJobFromUrl(e);if(!r.handle)throw new t(`Job receipt is missing handle`);return r.handle.wait(n?.wait)}makeHandle(e){let n=this;return{jobId:e,stream:t=>n.jobs.stream(e,t),async wait(r){let i=await n.jobs.wait(e,r);if(!i.reportId)throw new t(`Job ${e} succeeded but no reportId was returned`);return n.get(i.reportId)},cancel:()=>n.jobs.cancel(e),job:()=>n.jobs.get(e),report:()=>n.getByJob(e)}}defaultIdempotencyKey(e){return O(`idem`)}normalizeJobRequest(e){let t=e.target;if(!t)return e;let n={strategy:t.strategy};switch(t.onMiss&&(n.on_miss=t.onMiss),t.tags&&t.tags.length>0&&(n.tags=t.tags),t.excludeTags&&t.excludeTags.length>0&&(n.exclude_tags=t.excludeTags),t.strategy){case`dominant`:return{...e,target:n};case`timerange`:{let r=t.timeRange??{};return{...e,target:{...n,timerange:{start_seconds:r.startSeconds??null,end_seconds:r.endSeconds??null}}}}case`entity_id`:return{...e,target:{...n,entity_id:t.entityId}};case`magic_hint`:return{...e,target:{...n,hint:t.hint}};default:return e}}};function M(e,t,n){let r=new URL(t.replace(/^\//,``),e.endsWith(`/`)?e:`${e}/`);if(n)for(let[e,t]of Object.entries(n))t!==void 0&&r.searchParams.set(e,String(t));return r.toString()}async function N(e){let t=await e.text();if(!t)return{parsed:null,text:``};try{return{parsed:JSON.parse(t),text:t}}catch{return{parsed:t,text:t}}}function P(e,t){let o=e.headers.get(`x-request-id`)??void 0,s,c=`Request failed with status ${e.status}`,l=t;if(typeof t==`string`)c=t;else if(t&&typeof t==`object`){let e=t,n=e.error??e;if(n&&typeof n==`object`){let e=n;typeof e.message==`string`&&(c=e.message),typeof e.code==`string`&&(s=e.code),`details`in e&&(l=e.details)}}if(e.status===401||e.status===403)return new i(c,{status:e.status,requestId:o,code:s,details:l});if(e.status===422)return new a(c,{status:e.status,requestId:o,code:s,details:l});if(e.status===429){let t=new r(c,{status:e.status,requestId:o,code:s,details:l}),n=e.headers.get(`retry-after`);if(n){let e=Number(n);Number.isFinite(e)&&e>=0&&(t.retryAfterMs=e*1e3)}return t}return new n(c,{status:e.status,requestId:o,code:s,details:l})}function F(e,t){return e.retryable?t instanceof r?{retry:!0,retryAfterMs:t.retryAfterMs}:t instanceof n?{retry:t.status>=500&&t.status<=599}:t instanceof TypeError?{retry:!0}:{retry:!1}:{retry:!1}}var I=class{fetchImpl;constructor(e){this.opts=e,this.fetchImpl=e.fetch??fetch}async request(e){let t=M(this.opts.baseUrl,e.path,e.query),n=e.requestId??O(`req`),r={"Mappa-Api-Key":this.opts.apiKey,"X-Request-Id":n,...this.opts.userAgent?{"User-Agent":this.opts.userAgent}:{},...this.opts.defaultHeaders??{}};if(e.idempotencyKey&&(r[`Idempotency-Key`]=e.idempotencyKey),e.headers)for(let[t,n]of Object.entries(e.headers))n!==void 0&&(r[t]=n);let i=typeof FormData<`u`&&e.body instanceof FormData;e.body!==void 0&&!i&&(r[`Content-Type`]=`application/json`);let a=e.body===void 0?void 0:i?e.body:JSON.stringify(e.body),o=Math.max(0,this.opts.maxRetries),s=Date.now();for(let i=1;i<=1+o;i++){let c=new AbortController,l=setTimeout(()=>c.abort(D()),this.opts.timeoutMs);if(E(e.signal)){let t=e.signal;if(!t)throw clearTimeout(l),Error(`Unexpected: abort signal missing`);if(t.aborted)throw clearTimeout(l),D();t.addEventListener(`abort`,()=>c.abort(D()),{once:!0})}this.opts.telemetry?.onRequest?.({method:e.method,url:t,requestId:n});try{let l=await this.fetchImpl(t,{method:e.method,headers:r,body:a,signal:c.signal}),u=Date.now()-s,d=S(l.headers,`x-request-id`)??n;if(!l.ok){let{parsed:n}=await N(l),r=P(l,n);this.opts.telemetry?.onError?.({url:t,requestId:d,error:r});let a=F(e,r);if(i<=o+1&&a.retry&&i<=o){let e=a.retryAfterMs??C(w(i,500,4e3));await new Promise(t=>setTimeout(t,e));continue}throw r}let f=l.headers.get(`content-type`)??``,p;return p=f.includes(`application/json`)?await l.json():await l.text(),this.opts.telemetry?.onResponse?.({status:l.status,url:t,requestId:d,durationMs:u}),{data:p,status:l.status,requestId:d,headers:l.headers}}catch(r){this.opts.telemetry?.onError?.({url:t,requestId:n,error:r});let a=F(e,r);if(i<=o&&a.retry){let e=a.retryAfterMs??C(w(i,500,4e3));await new Promise(t=>setTimeout(t,e));continue}throw r}finally{clearTimeout(l)}}throw Error(`Unexpected transport exit`)}};function L(e){return typeof e==`object`&&!!e}var R=class{async verifySignature(e){let t=e.toleranceSec??300,n=z(e.headers,`mappa-signature`);if(!n)throw Error(`Missing mappa-signature header`);let r=B(n),i=Number(r.t);if(!Number.isFinite(i))throw Error(`Invalid signature timestamp`);let a=Math.floor(Date.now()/1e3);if(Math.abs(a-i)>t)throw Error(`Signature timestamp outside tolerance`);let o=`${r.t}.${e.payload}`;if(!U(await V(e.secret,o),r.v1))throw Error(`Invalid signature`);return{ok:!0}}parseEvent(e){let t=JSON.parse(e);if(!L(t))throw Error(`Invalid webhook payload: not an object`);let n=t,r=n.id,i=n.type,a=n.createdAt;if(typeof r!=`string`)throw Error(`Invalid webhook payload: id must be a string`);if(typeof i!=`string`)throw Error(`Invalid webhook payload: type must be a string`);if(typeof a!=`string`)throw Error(`Invalid webhook payload: createdAt must be a string`);return{id:r,type:i,createdAt:a,data:`data`in n?n.data:void 0}}};function z(e,t){let n=Object.keys(e).find(e=>e.toLowerCase()===t.toLowerCase()),r=n?e[n]:void 0;if(r)return Array.isArray(r)?r[0]:r}function B(e){let t={};for(let n of e.split(`,`)){let[e,r]=n.split(`=`);e&&r&&(t[e.trim()]=r.trim())}if(!t.t||!t.v1)throw Error(`Invalid signature format`);return{t:t.t,v1:t.v1}}async function V(e,t){let n=new TextEncoder,r=await crypto.subtle.importKey(`raw`,n.encode(e),{name:`HMAC`,hash:`SHA-256`},!1,[`sign`]);return H(await crypto.subtle.sign(`HMAC`,r,n.encode(t)))}function H(e){let t=new Uint8Array(e),n=``;for(let e of t)n+=e.toString(16).padStart(2,`0`);return n}function U(e,t){if(e.length!==t.length)return!1;let n=0;for(let r=0;r<e.length;r++)n|=e.charCodeAt(r)^t.charCodeAt(r);return n===0}var W=class e{files;jobs;reports;feedback;credits;entities;webhooks;health;transport;opts;constructor(e){if(!e.apiKey)throw new t(`apiKey is required`);let n=e.baseUrl??`https://api.mappa.ai`,r=e.timeoutMs??3e4,i=e.maxRetries??2;this.opts={...e,apiKey:e.apiKey,baseUrl:n,timeoutMs:r,maxRetries:i},this.transport=new I({apiKey:e.apiKey,baseUrl:n,timeoutMs:r,maxRetries:i,defaultHeaders:e.defaultHeaders,fetch:e.fetch,telemetry:e.telemetry,userAgent:e.userAgent}),this.files=new h(this.transport),this.jobs=new k(this.transport),this.reports=new j(this.transport,this.jobs,this.files,this.opts.fetch??fetch),this.feedback=new m(this.transport),this.credits=new c(this.transport),this.entities=new p(this.transport),this.webhooks=new R,this.health=new b(this.transport)}withOptions(t){return new e({...this.opts,...t,apiKey:t.apiKey??this.opts.apiKey})}close(){}};function G(e){return e.output.type===`markdown`}function K(e){return e.output.type===`json`}function q(e){return e.output.type===`pdf`}function J(e){return e.output.type===`url`}function Y(e){return e.entity!==void 0&&e.entity!==null}function X(e){return e instanceof t}exports.ApiError=n,exports.AuthError=i,exports.JobCanceledError=s,exports.JobFailedError=o,exports.Mappa=W,exports.MappaError=t,exports.RateLimitError=r,exports.ValidationError=a,exports.hasEntity=Y,exports.isJsonReport=K,exports.isMappaError=X,exports.isMarkdownReport=G,exports.isPdfReport=q,exports.isUrlReport=J;
|
|
1
|
+
let _paralleldrive_cuid2 = require("@paralleldrive/cuid2");
|
|
2
|
+
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* Symbol for custom Node.js inspect formatting.
|
|
6
|
+
* Ensures errors display nicely in console.log, REPL, and debuggers.
|
|
7
|
+
*/
|
|
8
|
+
const customInspect = Symbol.for("nodejs.util.inspect.custom");
|
|
9
|
+
/**
|
|
10
|
+
* Formats error details for display.
|
|
11
|
+
*/
|
|
12
|
+
function formatDetails(details, indent = " ") {
|
|
13
|
+
if (details === void 0 || details === null) return "";
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(details, null, 2).split("\n").join(`\n${indent}`);
|
|
16
|
+
} catch {
|
|
17
|
+
return String(details);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Base error type for all SDK-raised errors.
|
|
22
|
+
*
|
|
23
|
+
* When available, {@link MappaError.requestId} can be used to correlate a failure
|
|
24
|
+
* with server logs/support.
|
|
25
|
+
*/
|
|
26
|
+
var MappaError = class extends Error {
|
|
27
|
+
name = "MappaError";
|
|
28
|
+
requestId;
|
|
29
|
+
code;
|
|
30
|
+
constructor(message, opts) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.requestId = opts?.requestId;
|
|
33
|
+
this.code = opts?.code;
|
|
34
|
+
this.cause = opts?.cause;
|
|
35
|
+
}
|
|
36
|
+
toString() {
|
|
37
|
+
const lines = [`${this.name}: ${this.message}`];
|
|
38
|
+
if (this.code) lines.push(` Code: ${this.code}`);
|
|
39
|
+
if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
[customInspect]() {
|
|
43
|
+
return this.toString();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Error returned when the API responds with a non-2xx status.
|
|
48
|
+
*/
|
|
49
|
+
var ApiError = class extends MappaError {
|
|
50
|
+
name = "ApiError";
|
|
51
|
+
status;
|
|
52
|
+
details;
|
|
53
|
+
constructor(message, opts) {
|
|
54
|
+
super(message, {
|
|
55
|
+
requestId: opts.requestId,
|
|
56
|
+
code: opts.code
|
|
57
|
+
});
|
|
58
|
+
this.status = opts.status;
|
|
59
|
+
this.details = opts.details;
|
|
60
|
+
}
|
|
61
|
+
toString() {
|
|
62
|
+
const lines = [`${this.name}: ${this.message}`];
|
|
63
|
+
lines.push(` Status: ${this.status}`);
|
|
64
|
+
if (this.code) lines.push(` Code: ${this.code}`);
|
|
65
|
+
if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
|
|
66
|
+
if (this.details !== void 0 && this.details !== null) lines.push(` Details: ${formatDetails(this.details)}`);
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Error returned for HTTP 429 responses.
|
|
72
|
+
*
|
|
73
|
+
* If provided by the server, {@link RateLimitError.retryAfterMs} indicates when
|
|
74
|
+
* it is safe to retry.
|
|
75
|
+
*/
|
|
76
|
+
var RateLimitError = class extends ApiError {
|
|
77
|
+
name = "RateLimitError";
|
|
78
|
+
retryAfterMs;
|
|
79
|
+
toString() {
|
|
80
|
+
const lines = [`${this.name}: ${this.message}`];
|
|
81
|
+
lines.push(` Status: ${this.status}`);
|
|
82
|
+
if (this.retryAfterMs !== void 0) lines.push(` Retry After: ${this.retryAfterMs}ms`);
|
|
83
|
+
if (this.code) lines.push(` Code: ${this.code}`);
|
|
84
|
+
if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Error returned for authentication/authorization failures (typically 401/403).
|
|
90
|
+
*/
|
|
91
|
+
var AuthError = class extends ApiError {
|
|
92
|
+
name = "AuthError";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Error returned when the server rejects a request as invalid (typically 422).
|
|
96
|
+
*/
|
|
97
|
+
var ValidationError = class extends ApiError {
|
|
98
|
+
name = "ValidationError";
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Error returned when the account lacks sufficient credits (HTTP 402).
|
|
102
|
+
*
|
|
103
|
+
* Use {@link InsufficientCreditsError.required} and {@link InsufficientCreditsError.available}
|
|
104
|
+
* to inform users how many credits are needed.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* try {
|
|
109
|
+
* await mappa.reports.createJob({ ... });
|
|
110
|
+
* } catch (err) {
|
|
111
|
+
* if (err instanceof InsufficientCreditsError) {
|
|
112
|
+
* console.log(`Need ${err.required} credits, have ${err.available}`);
|
|
113
|
+
* }
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
var InsufficientCreditsError = class extends ApiError {
|
|
118
|
+
name = "InsufficientCreditsError";
|
|
119
|
+
/** Credits required for the operation */
|
|
120
|
+
required;
|
|
121
|
+
/** Credits currently available */
|
|
122
|
+
available;
|
|
123
|
+
constructor(message, opts) {
|
|
124
|
+
super(message, opts);
|
|
125
|
+
this.required = opts.details?.required ?? 0;
|
|
126
|
+
this.available = opts.details?.available ?? 0;
|
|
127
|
+
}
|
|
128
|
+
toString() {
|
|
129
|
+
const lines = [`${this.name}: ${this.message}`];
|
|
130
|
+
lines.push(` Status: ${this.status}`);
|
|
131
|
+
lines.push(` Required: ${this.required} credits`);
|
|
132
|
+
lines.push(` Available: ${this.available} credits`);
|
|
133
|
+
if (this.code) lines.push(` Code: ${this.code}`);
|
|
134
|
+
if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Error thrown by polling helpers when a job reaches the "failed" terminal state.
|
|
140
|
+
*/
|
|
141
|
+
var JobFailedError = class extends MappaError {
|
|
142
|
+
name = "JobFailedError";
|
|
143
|
+
jobId;
|
|
144
|
+
constructor(jobId, message, opts) {
|
|
145
|
+
super(message, opts);
|
|
146
|
+
this.jobId = jobId;
|
|
147
|
+
}
|
|
148
|
+
toString() {
|
|
149
|
+
const lines = [`${this.name}: ${this.message}`];
|
|
150
|
+
lines.push(` Job ID: ${this.jobId}`);
|
|
151
|
+
if (this.code) lines.push(` Code: ${this.code}`);
|
|
152
|
+
if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Error thrown by polling helpers when a job reaches the "canceled" terminal state.
|
|
158
|
+
*/
|
|
159
|
+
var JobCanceledError = class extends MappaError {
|
|
160
|
+
name = "JobCanceledError";
|
|
161
|
+
jobId;
|
|
162
|
+
constructor(jobId, message, opts) {
|
|
163
|
+
super(message, opts);
|
|
164
|
+
this.jobId = jobId;
|
|
165
|
+
}
|
|
166
|
+
toString() {
|
|
167
|
+
const lines = [`${this.name}: ${this.message}`];
|
|
168
|
+
lines.push(` Job ID: ${this.jobId}`);
|
|
169
|
+
if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/resources/credits.ts
|
|
176
|
+
/**
|
|
177
|
+
* Credits API resource.
|
|
178
|
+
*
|
|
179
|
+
* Provides methods to manage and query credit balances, transaction history,
|
|
180
|
+
* and job-specific credit usage.
|
|
181
|
+
*/
|
|
182
|
+
var CreditsResource = class {
|
|
183
|
+
constructor(transport) {
|
|
184
|
+
this.transport = transport;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get the current credit balance for your team.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const balance = await mappa.credits.getBalance();
|
|
191
|
+
* console.log(`Available: ${balance.available} credits`);
|
|
192
|
+
*/
|
|
193
|
+
async getBalance(opts) {
|
|
194
|
+
return (await this.transport.request({
|
|
195
|
+
method: "GET",
|
|
196
|
+
path: "/v1/credits/balance",
|
|
197
|
+
requestId: opts?.requestId,
|
|
198
|
+
signal: opts?.signal,
|
|
199
|
+
retryable: true
|
|
200
|
+
})).data;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* List credit transactions with offset pagination.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const { transactions, pagination } = await mappa.credits.listTransactions({ limit: 25 });
|
|
207
|
+
* console.log(`Showing ${transactions.length} of ${pagination.total}`);
|
|
208
|
+
*/
|
|
209
|
+
async listTransactions(opts) {
|
|
210
|
+
const query = {};
|
|
211
|
+
if (opts?.limit !== void 0) query.limit = String(opts.limit);
|
|
212
|
+
if (opts?.offset !== void 0) query.offset = String(opts.offset);
|
|
213
|
+
return (await this.transport.request({
|
|
214
|
+
method: "GET",
|
|
215
|
+
path: "/v1/credits/transactions",
|
|
216
|
+
query,
|
|
217
|
+
requestId: opts?.requestId,
|
|
218
|
+
signal: opts?.signal,
|
|
219
|
+
retryable: true
|
|
220
|
+
})).data;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Iterate over all transactions, automatically handling pagination.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* for await (const tx of mappa.credits.listAllTransactions()) {
|
|
227
|
+
* console.log(`${tx.type}: ${tx.amount}`);
|
|
228
|
+
* }
|
|
229
|
+
*/
|
|
230
|
+
async *listAllTransactions(opts) {
|
|
231
|
+
let offset = 0;
|
|
232
|
+
const limit = opts?.limit ?? 50;
|
|
233
|
+
while (true) {
|
|
234
|
+
const page = await this.listTransactions({
|
|
235
|
+
...opts,
|
|
236
|
+
limit,
|
|
237
|
+
offset
|
|
238
|
+
});
|
|
239
|
+
for (const tx of page.transactions) yield tx;
|
|
240
|
+
offset += page.transactions.length;
|
|
241
|
+
if (offset >= page.pagination.total) break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get credit usage details for a completed job.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* const usage = await mappa.credits.getJobUsage("job_xyz");
|
|
249
|
+
* console.log(`Net credits used: ${usage.creditsNetUsed}`);
|
|
250
|
+
*/
|
|
251
|
+
async getJobUsage(jobId, opts) {
|
|
252
|
+
if (!jobId) throw new MappaError("jobId is required");
|
|
253
|
+
return (await this.transport.request({
|
|
254
|
+
method: "GET",
|
|
255
|
+
path: `/v1/credits/usage/${encodeURIComponent(jobId)}`,
|
|
256
|
+
requestId: opts?.requestId,
|
|
257
|
+
signal: opts?.signal,
|
|
258
|
+
retryable: true
|
|
259
|
+
})).data;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Check if the team has enough available credits for an operation.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* if (await mappa.credits.hasEnough(100)) {
|
|
266
|
+
* await mappa.reports.createJob(...);
|
|
267
|
+
* }
|
|
268
|
+
*/
|
|
269
|
+
async hasEnough(credits, opts) {
|
|
270
|
+
return (await this.getBalance(opts)).available >= credits;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get the number of available credits (balance - reserved).
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* const available = await mappa.credits.getAvailable();
|
|
277
|
+
* console.log(`You can spend ${available} credits`);
|
|
278
|
+
*/
|
|
279
|
+
async getAvailable(opts) {
|
|
280
|
+
return (await this.getBalance(opts)).available;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/resources/entities.ts
|
|
286
|
+
/**
|
|
287
|
+
* Tag validation regex: 1-64 chars, alphanumeric with underscores and hyphens.
|
|
288
|
+
*/
|
|
289
|
+
const TAG_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
290
|
+
/**
|
|
291
|
+
* Maximum number of tags per request.
|
|
292
|
+
*/
|
|
293
|
+
const MAX_TAGS_PER_REQUEST = 10;
|
|
294
|
+
/**
|
|
295
|
+
* Validates a single tag.
|
|
296
|
+
* @throws {MappaError} if validation fails
|
|
297
|
+
*/
|
|
298
|
+
function validateTag(tag) {
|
|
299
|
+
if (typeof tag !== "string") throw new MappaError("Tags must be strings");
|
|
300
|
+
if (!TAG_REGEX.test(tag)) throw new MappaError(`Invalid tag "${tag}": must be 1-64 characters, alphanumeric with underscores and hyphens only`);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Validates an array of tags.
|
|
304
|
+
* @throws {MappaError} if validation fails
|
|
305
|
+
*/
|
|
306
|
+
function validateTags(tags) {
|
|
307
|
+
if (!Array.isArray(tags)) throw new MappaError("tags must be an array");
|
|
308
|
+
if (tags.length > MAX_TAGS_PER_REQUEST) throw new MappaError(`Too many tags: maximum ${MAX_TAGS_PER_REQUEST} per request`);
|
|
309
|
+
for (const tag of tags) validateTag(tag);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Entities API resource.
|
|
313
|
+
*
|
|
314
|
+
* Responsibilities:
|
|
315
|
+
* - List entities with optional tag filtering (`GET /v1/entities`)
|
|
316
|
+
* - Get single entity details (`GET /v1/entities/:entityId`)
|
|
317
|
+
* - Add tags to entities (`POST /v1/entities/:entityId/tags`)
|
|
318
|
+
* - Remove tags from entities (`DELETE /v1/entities/:entityId/tags`)
|
|
319
|
+
* - Replace all entity tags (`PUT /v1/entities/:entityId/tags`)
|
|
320
|
+
*
|
|
321
|
+
* Entities represent analyzed speakers identified by voice fingerprints.
|
|
322
|
+
* Tags allow you to label entities (e.g., "interviewer", "sales-rep") for easier
|
|
323
|
+
* filtering and identification across multiple reports.
|
|
324
|
+
*/
|
|
325
|
+
var EntitiesResource = class {
|
|
326
|
+
constructor(transport) {
|
|
327
|
+
this.transport = transport;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get a single entity by ID.
|
|
331
|
+
*
|
|
332
|
+
* Returns entity metadata including tags, creation time, and usage statistics.
|
|
333
|
+
*
|
|
334
|
+
* @param entityId - The entity ID to retrieve
|
|
335
|
+
* @param opts - Optional request options (requestId, signal)
|
|
336
|
+
* @returns Entity details with tags and metadata
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* const entity = await mappa.entities.get("entity_abc123");
|
|
341
|
+
* console.log(entity.tags); // ["interviewer", "john"]
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
async get(entityId, opts) {
|
|
345
|
+
if (!entityId || typeof entityId !== "string") throw new MappaError("entityId must be a non-empty string");
|
|
346
|
+
return (await this.transport.request({
|
|
347
|
+
method: "GET",
|
|
348
|
+
path: `/v1/entities/${encodeURIComponent(entityId)}`,
|
|
349
|
+
requestId: opts?.requestId,
|
|
350
|
+
signal: opts?.signal,
|
|
351
|
+
retryable: true
|
|
352
|
+
})).data;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* List entities with optional tag filtering.
|
|
356
|
+
*
|
|
357
|
+
* Supports cursor-based pagination. Use the returned `cursor` for fetching
|
|
358
|
+
* subsequent pages, or use {@link listAll} for automatic pagination.
|
|
359
|
+
*
|
|
360
|
+
* When `tags` is provided, only entities with ALL specified tags are returned (AND logic).
|
|
361
|
+
*
|
|
362
|
+
* @param opts - List options: tags filter, cursor, limit
|
|
363
|
+
* @returns Paginated list of entities
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* // List all entities
|
|
368
|
+
* const page1 = await mappa.entities.list({ limit: 20 });
|
|
369
|
+
*
|
|
370
|
+
* // Filter by tags (must have both "interviewer" AND "sales")
|
|
371
|
+
* const filtered = await mappa.entities.list({
|
|
372
|
+
* tags: ["interviewer", "sales"],
|
|
373
|
+
* limit: 50
|
|
374
|
+
* });
|
|
375
|
+
*
|
|
376
|
+
* // Pagination
|
|
377
|
+
* const page2 = await mappa.entities.list({
|
|
378
|
+
* cursor: page1.cursor,
|
|
379
|
+
* limit: 20
|
|
380
|
+
* });
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
async list(opts) {
|
|
384
|
+
const query = {};
|
|
385
|
+
if (opts?.tags) {
|
|
386
|
+
validateTags(opts.tags);
|
|
387
|
+
query.tags = opts.tags.join(",");
|
|
388
|
+
}
|
|
389
|
+
if (opts?.cursor) query.cursor = opts.cursor;
|
|
390
|
+
if (opts?.limit !== void 0) query.limit = String(opts.limit);
|
|
391
|
+
return (await this.transport.request({
|
|
392
|
+
method: "GET",
|
|
393
|
+
path: "/v1/entities",
|
|
394
|
+
query,
|
|
395
|
+
requestId: opts?.requestId,
|
|
396
|
+
signal: opts?.signal,
|
|
397
|
+
retryable: true
|
|
398
|
+
})).data;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Async iterator that automatically paginates through all entities.
|
|
402
|
+
*
|
|
403
|
+
* Useful for processing large entity sets without manual pagination management.
|
|
404
|
+
*
|
|
405
|
+
* @param opts - List options: tags filter, limit per page
|
|
406
|
+
* @yields Individual entities
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```typescript
|
|
410
|
+
* // Process all entities with "interviewer" tag
|
|
411
|
+
* for await (const entity of mappa.entities.listAll({ tags: ["interviewer"] })) {
|
|
412
|
+
* console.log(`${entity.id}: ${entity.tags.join(", ")}`);
|
|
413
|
+
* }
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
async *listAll(opts) {
|
|
417
|
+
let cursor;
|
|
418
|
+
let hasMore = true;
|
|
419
|
+
while (hasMore) {
|
|
420
|
+
const page = await this.list({
|
|
421
|
+
...opts,
|
|
422
|
+
cursor
|
|
423
|
+
});
|
|
424
|
+
for (const entity of page.entities) yield entity;
|
|
425
|
+
cursor = page.cursor;
|
|
426
|
+
hasMore = page.hasMore;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get all entities with a specific tag.
|
|
431
|
+
*
|
|
432
|
+
* Convenience wrapper around {@link list} for single-tag filtering.
|
|
433
|
+
*
|
|
434
|
+
* @param tag - The tag to filter by
|
|
435
|
+
* @param opts - Optional pagination and request options
|
|
436
|
+
* @returns Paginated list of entities with the specified tag
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* ```typescript
|
|
440
|
+
* const interviewers = await mappa.entities.getByTag("interviewer");
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
async getByTag(tag, opts) {
|
|
444
|
+
validateTag(tag);
|
|
445
|
+
return this.list({
|
|
446
|
+
...opts,
|
|
447
|
+
tags: [tag]
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Add tags to an entity.
|
|
452
|
+
*
|
|
453
|
+
* Idempotent: existing tags are preserved, duplicates are ignored.
|
|
454
|
+
*
|
|
455
|
+
* @param entityId - The entity ID to tag
|
|
456
|
+
* @param tags - Array of tags to add (1-10 tags, each 1-64 chars)
|
|
457
|
+
* @param opts - Optional request options
|
|
458
|
+
* @returns Updated tags for the entity
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```typescript
|
|
462
|
+
* await mappa.entities.addTags("entity_abc123", ["interviewer", "john"]);
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
async addTags(entityId, tags, opts) {
|
|
466
|
+
if (!entityId || typeof entityId !== "string") throw new MappaError("entityId must be a non-empty string");
|
|
467
|
+
if (tags.length === 0) throw new MappaError("At least one tag is required");
|
|
468
|
+
validateTags(tags);
|
|
469
|
+
return (await this.transport.request({
|
|
470
|
+
method: "POST",
|
|
471
|
+
path: `/v1/entities/${encodeURIComponent(entityId)}/tags`,
|
|
472
|
+
body: { tags },
|
|
473
|
+
requestId: opts?.requestId,
|
|
474
|
+
signal: opts?.signal,
|
|
475
|
+
retryable: true
|
|
476
|
+
})).data;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Remove tags from an entity.
|
|
480
|
+
*
|
|
481
|
+
* Idempotent: missing tags are silently ignored.
|
|
482
|
+
*
|
|
483
|
+
* @param entityId - The entity ID to update
|
|
484
|
+
* @param tags - Array of tags to remove
|
|
485
|
+
* @param opts - Optional request options
|
|
486
|
+
* @returns Updated tags for the entity
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* ```typescript
|
|
490
|
+
* await mappa.entities.removeTags("entity_abc123", ["interviewer"]);
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
async removeTags(entityId, tags, opts) {
|
|
494
|
+
if (!entityId || typeof entityId !== "string") throw new MappaError("entityId must be a non-empty string");
|
|
495
|
+
if (tags.length === 0) throw new MappaError("At least one tag is required");
|
|
496
|
+
validateTags(tags);
|
|
497
|
+
return (await this.transport.request({
|
|
498
|
+
method: "DELETE",
|
|
499
|
+
path: `/v1/entities/${encodeURIComponent(entityId)}/tags`,
|
|
500
|
+
body: { tags },
|
|
501
|
+
requestId: opts?.requestId,
|
|
502
|
+
signal: opts?.signal,
|
|
503
|
+
retryable: true
|
|
504
|
+
})).data;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Replace all tags on an entity.
|
|
508
|
+
*
|
|
509
|
+
* Sets the complete tag list, removing any tags not in the provided array.
|
|
510
|
+
* Pass an empty array to remove all tags.
|
|
511
|
+
*
|
|
512
|
+
* @param entityId - The entity ID to update
|
|
513
|
+
* @param tags - New complete tag list (0-10 tags)
|
|
514
|
+
* @param opts - Optional request options
|
|
515
|
+
* @returns Updated tags for the entity
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```typescript
|
|
519
|
+
* // Replace all tags
|
|
520
|
+
* await mappa.entities.setTags("entity_abc123", ["sales-rep", "john"]);
|
|
521
|
+
*
|
|
522
|
+
* // Remove all tags
|
|
523
|
+
* await mappa.entities.setTags("entity_abc123", []);
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
async setTags(entityId, tags, opts) {
|
|
527
|
+
if (!entityId || typeof entityId !== "string") throw new MappaError("entityId must be a non-empty string");
|
|
528
|
+
validateTags(tags);
|
|
529
|
+
return (await this.transport.request({
|
|
530
|
+
method: "PUT",
|
|
531
|
+
path: `/v1/entities/${encodeURIComponent(entityId)}/tags`,
|
|
532
|
+
body: { tags },
|
|
533
|
+
requestId: opts?.requestId,
|
|
534
|
+
signal: opts?.signal,
|
|
535
|
+
retryable: true
|
|
536
|
+
})).data;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/resources/feedback.ts
|
|
542
|
+
var FeedbackResource = class {
|
|
543
|
+
constructor(transport) {
|
|
544
|
+
this.transport = transport;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Create feedback for a report or job. Provide exactly one of `reportId` or `jobId`.
|
|
548
|
+
*/
|
|
549
|
+
async create(req) {
|
|
550
|
+
if (!!req.reportId === !!req.jobId) throw new MappaError("Provide exactly one of reportId or jobId");
|
|
551
|
+
return (await this.transport.request({
|
|
552
|
+
method: "POST",
|
|
553
|
+
path: "/v1/feedback",
|
|
554
|
+
body: req,
|
|
555
|
+
idempotencyKey: req.idempotencyKey,
|
|
556
|
+
requestId: req.requestId,
|
|
557
|
+
signal: req.signal,
|
|
558
|
+
retryable: true
|
|
559
|
+
})).data;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/resources/files.ts
|
|
565
|
+
/**
|
|
566
|
+
* Uses multipart/form-data for uploads.
|
|
567
|
+
*
|
|
568
|
+
* If you need resumable uploads, add a dedicated resumable protocol.
|
|
569
|
+
*/
|
|
570
|
+
var FilesResource = class {
|
|
571
|
+
constructor(transport) {
|
|
572
|
+
this.transport = transport;
|
|
573
|
+
}
|
|
574
|
+
async upload(req) {
|
|
575
|
+
if (typeof FormData === "undefined") throw new MappaError("FormData is not available in this runtime; cannot perform multipart upload");
|
|
576
|
+
const derivedContentType = inferContentType(req.file, req.filename);
|
|
577
|
+
const contentType = req.contentType ?? derivedContentType;
|
|
578
|
+
if (!contentType) throw new MappaError("contentType is required when it cannot be inferred from file.type or filename");
|
|
579
|
+
const filename = req.filename ?? inferFilename(req.file) ?? "upload";
|
|
580
|
+
const filePart = await toFormDataPart(req.file, contentType);
|
|
581
|
+
const form = new FormData();
|
|
582
|
+
form.append("file", filePart, filename);
|
|
583
|
+
form.append("contentType", contentType);
|
|
584
|
+
if (req.filename) form.append("filename", req.filename);
|
|
585
|
+
return (await this.transport.request({
|
|
586
|
+
method: "POST",
|
|
587
|
+
path: "/v1/files",
|
|
588
|
+
body: form,
|
|
589
|
+
idempotencyKey: req.idempotencyKey,
|
|
590
|
+
requestId: req.requestId,
|
|
591
|
+
signal: req.signal,
|
|
592
|
+
retryable: true
|
|
593
|
+
})).data;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Retrieve metadata for a single uploaded file.
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* const file = await mappa.files.get("media_abc123");
|
|
600
|
+
* console.log(file.processingStatus); // "COMPLETED"
|
|
601
|
+
*/
|
|
602
|
+
async get(mediaId, opts) {
|
|
603
|
+
if (!mediaId) throw new MappaError("mediaId is required");
|
|
604
|
+
return (await this.transport.request({
|
|
605
|
+
method: "GET",
|
|
606
|
+
path: `/v1/files/${encodeURIComponent(mediaId)}`,
|
|
607
|
+
requestId: opts?.requestId,
|
|
608
|
+
signal: opts?.signal,
|
|
609
|
+
retryable: true
|
|
610
|
+
})).data;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* List uploaded files with cursor pagination.
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* const page1 = await mappa.files.list({ limit: 10 });
|
|
617
|
+
* if (page1.hasMore) {
|
|
618
|
+
* const page2 = await mappa.files.list({ limit: 10, cursor: page1.cursor });
|
|
619
|
+
* }
|
|
620
|
+
*/
|
|
621
|
+
async list(opts) {
|
|
622
|
+
const query = {};
|
|
623
|
+
if (opts?.limit !== void 0) query.limit = String(opts.limit);
|
|
624
|
+
if (opts?.cursor) query.cursor = opts.cursor;
|
|
625
|
+
if (opts?.includeDeleted !== void 0) query.includeDeleted = String(opts.includeDeleted);
|
|
626
|
+
return (await this.transport.request({
|
|
627
|
+
method: "GET",
|
|
628
|
+
path: "/v1/files",
|
|
629
|
+
query,
|
|
630
|
+
requestId: opts?.requestId,
|
|
631
|
+
signal: opts?.signal,
|
|
632
|
+
retryable: true
|
|
633
|
+
})).data;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Iterate over all files, automatically handling pagination.
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* for await (const file of mappa.files.listAll()) {
|
|
640
|
+
* console.log(file.mediaId);
|
|
641
|
+
* }
|
|
642
|
+
*
|
|
643
|
+
* // Or collect all
|
|
644
|
+
* const allFiles = [];
|
|
645
|
+
* for await (const file of mappa.files.listAll({ limit: 50 })) {
|
|
646
|
+
* allFiles.push(file);
|
|
647
|
+
* }
|
|
648
|
+
*/
|
|
649
|
+
async *listAll(opts) {
|
|
650
|
+
let cursor;
|
|
651
|
+
let hasMore = true;
|
|
652
|
+
while (hasMore) {
|
|
653
|
+
const page = await this.list({
|
|
654
|
+
...opts,
|
|
655
|
+
cursor
|
|
656
|
+
});
|
|
657
|
+
for (const file of page.files) yield file;
|
|
658
|
+
cursor = page.cursor;
|
|
659
|
+
hasMore = page.hasMore;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Lock or unlock a file's retention to prevent/allow automatic deletion.
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* // Prevent automatic deletion
|
|
667
|
+
* await mappa.files.setRetentionLock("media_abc", true);
|
|
668
|
+
*
|
|
669
|
+
* // Allow automatic deletion
|
|
670
|
+
* await mappa.files.setRetentionLock("media_abc", false);
|
|
671
|
+
*/
|
|
672
|
+
async setRetentionLock(mediaId, locked, opts) {
|
|
673
|
+
if (!mediaId) throw new MappaError("mediaId is required");
|
|
674
|
+
return (await this.transport.request({
|
|
675
|
+
method: "PATCH",
|
|
676
|
+
path: `/v1/files/${encodeURIComponent(mediaId)}/retention`,
|
|
677
|
+
body: { lock: locked },
|
|
678
|
+
requestId: opts?.requestId,
|
|
679
|
+
signal: opts?.signal,
|
|
680
|
+
retryable: true
|
|
681
|
+
})).data;
|
|
682
|
+
}
|
|
683
|
+
async delete(mediaId, opts) {
|
|
684
|
+
if (!mediaId) throw new MappaError("mediaId is required");
|
|
685
|
+
return (await this.transport.request({
|
|
686
|
+
method: "DELETE",
|
|
687
|
+
path: `/v1/files/${encodeURIComponent(mediaId)}`,
|
|
688
|
+
idempotencyKey: opts?.idempotencyKey,
|
|
689
|
+
requestId: opts?.requestId,
|
|
690
|
+
signal: opts?.signal,
|
|
691
|
+
retryable: true
|
|
692
|
+
})).data;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
function inferContentType(file, filename) {
|
|
696
|
+
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
|
697
|
+
if (file.type) return file.type;
|
|
698
|
+
}
|
|
699
|
+
if (filename) return contentTypeFromFilename(filename);
|
|
700
|
+
}
|
|
701
|
+
function inferFilename(file) {
|
|
702
|
+
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
|
703
|
+
const anyBlob = file;
|
|
704
|
+
if (typeof anyBlob.name === "string" && anyBlob.name) return anyBlob.name;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function contentTypeFromFilename(filename) {
|
|
708
|
+
const i = filename.lastIndexOf(".");
|
|
709
|
+
if (i < 0) return void 0;
|
|
710
|
+
switch (filename.slice(i + 1).toLowerCase()) {
|
|
711
|
+
case "mp4": return "video/mp4";
|
|
712
|
+
case "mov": return "video/quicktime";
|
|
713
|
+
case "webm": return "video/webm";
|
|
714
|
+
case "mp3": return "audio/mpeg";
|
|
715
|
+
case "wav": return "audio/wav";
|
|
716
|
+
case "m4a": return "audio/mp4";
|
|
717
|
+
case "png": return "image/png";
|
|
718
|
+
case "jpg":
|
|
719
|
+
case "jpeg": return "image/jpeg";
|
|
720
|
+
case "gif": return "image/gif";
|
|
721
|
+
case "webp": return "image/webp";
|
|
722
|
+
case "pdf": return "application/pdf";
|
|
723
|
+
case "json": return "application/json";
|
|
724
|
+
case "txt": return "text/plain";
|
|
725
|
+
default: return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function toFormDataPart(file, contentType) {
|
|
729
|
+
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
|
730
|
+
if (file.type === contentType) return file;
|
|
731
|
+
const slicer = file;
|
|
732
|
+
if (typeof slicer.slice === "function") return slicer.slice(0, file.size, contentType);
|
|
733
|
+
return file;
|
|
734
|
+
}
|
|
735
|
+
if (file instanceof ArrayBuffer) return new Blob([file], { type: contentType });
|
|
736
|
+
if (file instanceof Uint8Array) return new Blob([file], { type: contentType });
|
|
737
|
+
if (typeof ReadableStream !== "undefined" && file instanceof ReadableStream) {
|
|
738
|
+
if (typeof Response === "undefined") throw new MappaError("ReadableStream upload requires Response to convert stream to Blob");
|
|
739
|
+
const blob = await new Response(file).blob();
|
|
740
|
+
return blob.slice(0, blob.size, contentType);
|
|
741
|
+
}
|
|
742
|
+
throw new MappaError("Unsupported file type for upload()");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/resources/health.ts
|
|
747
|
+
var HealthResource = class {
|
|
748
|
+
constructor(transport) {
|
|
749
|
+
this.transport = transport;
|
|
750
|
+
}
|
|
751
|
+
async ping() {
|
|
752
|
+
return (await this.transport.request({
|
|
753
|
+
method: "GET",
|
|
754
|
+
path: "/v1/health/ping",
|
|
755
|
+
retryable: true
|
|
756
|
+
})).data;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
//#endregion
|
|
761
|
+
//#region src/utils/index.ts
|
|
762
|
+
const createId = (0, _paralleldrive_cuid2.init)({ length: 32 });
|
|
763
|
+
function getHeader(headers, name) {
|
|
764
|
+
const v = headers.get(name);
|
|
765
|
+
return v === null ? void 0 : v;
|
|
766
|
+
}
|
|
767
|
+
function jitter(ms) {
|
|
768
|
+
const r = .8 + Math.random() * .4;
|
|
769
|
+
return Math.floor(ms * r);
|
|
770
|
+
}
|
|
771
|
+
function backoffMs(attempt, baseMs, maxMs) {
|
|
772
|
+
const ms = baseMs * 2 ** Math.max(0, attempt - 1);
|
|
773
|
+
return Math.min(ms, maxMs);
|
|
774
|
+
}
|
|
775
|
+
function hasAbortSignal(signal) {
|
|
776
|
+
return !!signal && typeof signal.aborted === "boolean";
|
|
777
|
+
}
|
|
778
|
+
function makeAbortError() {
|
|
779
|
+
const e = /* @__PURE__ */ new Error("The operation was aborted");
|
|
780
|
+
e.name = "AbortError";
|
|
781
|
+
return e;
|
|
782
|
+
}
|
|
783
|
+
function randomId(prefix = "req") {
|
|
784
|
+
return `${prefix}_${createId()}`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
//#endregion
|
|
788
|
+
//#region src/resources/jobs.ts
|
|
789
|
+
var JobsResource = class {
|
|
790
|
+
constructor(transport) {
|
|
791
|
+
this.transport = transport;
|
|
792
|
+
}
|
|
793
|
+
async get(jobId, opts) {
|
|
794
|
+
return (await this.transport.request({
|
|
795
|
+
method: "GET",
|
|
796
|
+
path: `/v1/jobs/${encodeURIComponent(jobId)}`,
|
|
797
|
+
requestId: opts?.requestId,
|
|
798
|
+
signal: opts?.signal,
|
|
799
|
+
retryable: true
|
|
800
|
+
})).data;
|
|
801
|
+
}
|
|
802
|
+
async cancel(jobId, opts) {
|
|
803
|
+
return (await this.transport.request({
|
|
804
|
+
method: "POST",
|
|
805
|
+
path: `/v1/jobs/${encodeURIComponent(jobId)}/cancel`,
|
|
806
|
+
idempotencyKey: opts?.idempotencyKey,
|
|
807
|
+
requestId: opts?.requestId,
|
|
808
|
+
signal: opts?.signal,
|
|
809
|
+
retryable: true
|
|
810
|
+
})).data;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Wait for a job to reach a terminal state.
|
|
814
|
+
*
|
|
815
|
+
* Uses SSE streaming internally for efficient real-time updates.
|
|
816
|
+
*/
|
|
817
|
+
async wait(jobId, opts) {
|
|
818
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
|
|
819
|
+
const controller = new AbortController();
|
|
820
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
821
|
+
if (opts?.signal) {
|
|
822
|
+
if (opts.signal.aborted) {
|
|
823
|
+
clearTimeout(timeoutId);
|
|
824
|
+
throw makeAbortError();
|
|
825
|
+
}
|
|
826
|
+
opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
for await (const event of this.stream(jobId, {
|
|
830
|
+
signal: controller.signal,
|
|
831
|
+
onEvent: opts?.onEvent
|
|
832
|
+
})) if (event.type === "terminal") {
|
|
833
|
+
const job = event.job;
|
|
834
|
+
if (job.status === "succeeded") return job;
|
|
835
|
+
if (job.status === "failed") throw new JobFailedError(jobId, job.error?.message ?? "Job failed", {
|
|
836
|
+
requestId: job.requestId,
|
|
837
|
+
code: job.error?.code,
|
|
838
|
+
cause: job.error
|
|
839
|
+
});
|
|
840
|
+
if (job.status === "canceled") throw new JobCanceledError(jobId, "Job canceled", {
|
|
841
|
+
requestId: job.requestId,
|
|
842
|
+
cause: job.error
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
throw new JobFailedError(jobId, `Timed out waiting for job ${jobId} after ${timeoutMs}ms`, { cause: {
|
|
846
|
+
jobId,
|
|
847
|
+
timeoutMs
|
|
848
|
+
} });
|
|
849
|
+
} finally {
|
|
850
|
+
clearTimeout(timeoutId);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Stream job events via SSE.
|
|
855
|
+
*
|
|
856
|
+
* Yields events as they arrive from the server. Use `AbortSignal` to cancel streaming.
|
|
857
|
+
* Automatically handles reconnection with `Last-Event-ID` for up to 3 retries.
|
|
858
|
+
*/
|
|
859
|
+
async *stream(jobId, opts) {
|
|
860
|
+
const maxRetries = 3;
|
|
861
|
+
let lastEventId;
|
|
862
|
+
let retries = 0;
|
|
863
|
+
while (retries < maxRetries) try {
|
|
864
|
+
const sseStream = this.transport.streamSSE(`/v1/jobs/${encodeURIComponent(jobId)}/stream`, {
|
|
865
|
+
signal: opts?.signal,
|
|
866
|
+
lastEventId
|
|
867
|
+
});
|
|
868
|
+
for await (const sseEvent of sseStream) {
|
|
869
|
+
lastEventId = sseEvent.id;
|
|
870
|
+
if (sseEvent.event === "error") {
|
|
871
|
+
const errorData = sseEvent.data;
|
|
872
|
+
throw new MappaError(errorData.error?.message ?? "Unknown SSE error", { code: errorData.error?.code });
|
|
873
|
+
}
|
|
874
|
+
if (sseEvent.event === "heartbeat") continue;
|
|
875
|
+
const jobEvent = this.mapSSEToJobEvent(sseEvent);
|
|
876
|
+
if (jobEvent) {
|
|
877
|
+
opts?.onEvent?.(jobEvent);
|
|
878
|
+
yield jobEvent;
|
|
879
|
+
if (sseEvent.event === "terminal") return;
|
|
880
|
+
}
|
|
881
|
+
retries = 0;
|
|
882
|
+
}
|
|
883
|
+
retries++;
|
|
884
|
+
if (retries < maxRetries) await this.backoff(retries);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if (opts?.signal?.aborted) throw error;
|
|
887
|
+
retries++;
|
|
888
|
+
if (retries >= maxRetries) throw error;
|
|
889
|
+
await this.backoff(retries);
|
|
890
|
+
}
|
|
891
|
+
throw new MappaError(`Failed to get status for job ${jobId} after ${maxRetries} retries`);
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Map an SSE event to a JobEvent.
|
|
895
|
+
*/
|
|
896
|
+
mapSSEToJobEvent(sseEvent) {
|
|
897
|
+
const data = sseEvent.data;
|
|
898
|
+
switch (sseEvent.event) {
|
|
899
|
+
case "status": return {
|
|
900
|
+
type: "status",
|
|
901
|
+
job: data.job
|
|
902
|
+
};
|
|
903
|
+
case "stage": return {
|
|
904
|
+
type: "stage",
|
|
905
|
+
stage: data.stage,
|
|
906
|
+
progress: data.progress,
|
|
907
|
+
job: data.job
|
|
908
|
+
};
|
|
909
|
+
case "terminal": return {
|
|
910
|
+
type: "terminal",
|
|
911
|
+
job: data.job
|
|
912
|
+
};
|
|
913
|
+
default: return {
|
|
914
|
+
type: "status",
|
|
915
|
+
job: data.job
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Exponential backoff with jitter for reconnection.
|
|
921
|
+
*/
|
|
922
|
+
async backoff(attempt) {
|
|
923
|
+
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
924
|
+
const jitter$1 = delay * .5 * Math.random();
|
|
925
|
+
await new Promise((r) => setTimeout(r, delay + jitter$1));
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/resources/reports.ts
|
|
931
|
+
/**
|
|
932
|
+
* Runtime validation for the internal `MediaIdRef` requirement.
|
|
933
|
+
*
|
|
934
|
+
* The public API server expects a `mediaId` when creating a report job.
|
|
935
|
+
* Use helpers like `createJobFromFile` / `createJobFromUrl` to start from bytes or a URL.
|
|
936
|
+
*/
|
|
937
|
+
function validateMedia(media) {
|
|
938
|
+
const m = media;
|
|
939
|
+
const isObj = (v) => v !== null && typeof v === "object";
|
|
940
|
+
if (!isObj(m)) throw new MappaError("media must be an object");
|
|
941
|
+
if (m.url !== void 0) throw new MappaError("media.url is not supported; pass { mediaId } or use createJobFromUrl()");
|
|
942
|
+
const mediaId = m.mediaId;
|
|
943
|
+
if (typeof mediaId !== "string" || !mediaId) throw new MappaError("media.mediaId must be a non-empty string");
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Reports API resource.
|
|
947
|
+
*
|
|
948
|
+
* Responsibilities:
|
|
949
|
+
* - Create report jobs (`POST /v1/reports/jobs`).
|
|
950
|
+
* - Fetch reports by report ID (`GET /v1/reports/:reportId`).
|
|
951
|
+
* - Fetch reports by job ID (`GET /v1/reports/by-job/:jobId`).
|
|
952
|
+
*
|
|
953
|
+
* Convenience helpers:
|
|
954
|
+
* - {@link ReportsResource.createJobFromFile} orchestrates `files.upload()` + {@link ReportsResource.createJob}.
|
|
955
|
+
* - {@link ReportsResource.createJobFromUrl} downloads a remote URL, uploads it, then calls {@link ReportsResource.createJob}.
|
|
956
|
+
* - {@link ReportsResource.generate} / {@link ReportsResource.generateFromFile} are script-friendly wrappers that
|
|
957
|
+
* create a job, wait for completion, and then fetch the final report.
|
|
958
|
+
*
|
|
959
|
+
* For production systems, prefer `createJob*()` plus webhooks or streaming job events rather than blocking waits.
|
|
960
|
+
*/
|
|
961
|
+
var ReportsResource = class {
|
|
962
|
+
constructor(transport, jobs, files, fetchImpl) {
|
|
963
|
+
this.transport = transport;
|
|
964
|
+
this.jobs = jobs;
|
|
965
|
+
this.files = files;
|
|
966
|
+
this.fetchImpl = fetchImpl;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Create a new report job.
|
|
970
|
+
*
|
|
971
|
+
* Behavior:
|
|
972
|
+
* - Validates {@link MediaIdRef} at runtime (must provide `{ mediaId }`).
|
|
973
|
+
* - Defaults to `{ strategy: "dominant" }` when `target` is omitted.
|
|
974
|
+
* - Applies an idempotency key: uses `req.idempotencyKey` when provided; otherwise generates a best-effort default.
|
|
975
|
+
* - Forwards `req.requestId` to the transport for end-to-end correlation.
|
|
976
|
+
*
|
|
977
|
+
* The returned receipt includes a {@link ReportRunHandle} (`receipt.handle`) which can be used to:
|
|
978
|
+
* - stream job events
|
|
979
|
+
* - wait for completion and fetch the final report
|
|
980
|
+
* - cancel the job, or fetch job/report metadata
|
|
981
|
+
*/
|
|
982
|
+
async createJob(req) {
|
|
983
|
+
validateMedia(req.media);
|
|
984
|
+
const idempotencyKey = req.idempotencyKey ?? this.defaultIdempotencyKey(req);
|
|
985
|
+
const res = await this.transport.request({
|
|
986
|
+
method: "POST",
|
|
987
|
+
path: "/v1/reports/jobs",
|
|
988
|
+
body: this.normalizeJobRequest(req),
|
|
989
|
+
idempotencyKey,
|
|
990
|
+
requestId: req.requestId,
|
|
991
|
+
retryable: true
|
|
992
|
+
});
|
|
993
|
+
const receipt = {
|
|
994
|
+
...res.data,
|
|
995
|
+
requestId: res.requestId ?? res.data.requestId
|
|
996
|
+
};
|
|
997
|
+
receipt.handle = this.makeHandle(receipt.jobId);
|
|
998
|
+
return receipt;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Upload a file and create a report job in one call.
|
|
1002
|
+
*
|
|
1003
|
+
* Keeps `createJob()` strict about `media: { mediaId }` while offering a
|
|
1004
|
+
* convenient helper when you start from raw bytes.
|
|
1005
|
+
*/
|
|
1006
|
+
async createJobFromFile(req) {
|
|
1007
|
+
const { file, contentType, filename, idempotencyKey, requestId, signal, ...rest } = req;
|
|
1008
|
+
const upload = await this.files.upload({
|
|
1009
|
+
file,
|
|
1010
|
+
contentType,
|
|
1011
|
+
filename,
|
|
1012
|
+
idempotencyKey,
|
|
1013
|
+
requestId,
|
|
1014
|
+
signal
|
|
1015
|
+
});
|
|
1016
|
+
return this.createJob({
|
|
1017
|
+
...rest,
|
|
1018
|
+
media: { mediaId: upload.mediaId },
|
|
1019
|
+
idempotencyKey,
|
|
1020
|
+
requestId
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Download a file from a URL, upload it, and create a report job.
|
|
1025
|
+
*
|
|
1026
|
+
* Recommended when starting from a remote URL because report job creation
|
|
1027
|
+
* only accepts `media: { mediaId }`.
|
|
1028
|
+
*
|
|
1029
|
+
* Workflow:
|
|
1030
|
+
* 1) `fetch(url)`
|
|
1031
|
+
* 2) Validate the response (2xx) and derive `contentType`
|
|
1032
|
+
* 3) `files.upload({ file: Blob, ... })`
|
|
1033
|
+
* 4) `createJob({ media: { mediaId }, ... })`
|
|
1034
|
+
*
|
|
1035
|
+
* Verification / safety:
|
|
1036
|
+
* - Only allows `http:` and `https:` URLs.
|
|
1037
|
+
* - Requires a resolvable `contentType` (from `req.contentType` or response header).
|
|
1038
|
+
*/
|
|
1039
|
+
async createJobFromUrl(req) {
|
|
1040
|
+
const { url, contentType: contentTypeOverride, filename, idempotencyKey, requestId, signal, ...rest } = req;
|
|
1041
|
+
let parsed;
|
|
1042
|
+
try {
|
|
1043
|
+
parsed = new URL(url);
|
|
1044
|
+
} catch {
|
|
1045
|
+
throw new MappaError("url must be a valid URL");
|
|
1046
|
+
}
|
|
1047
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new MappaError("url must use http: or https:");
|
|
1048
|
+
const res = await this.fetchImpl(parsed.toString(), { signal });
|
|
1049
|
+
if (!res.ok) throw new MappaError(`Failed to download url (status ${res.status})`);
|
|
1050
|
+
const derivedContentType = res.headers.get("content-type") ?? void 0;
|
|
1051
|
+
const contentType = contentTypeOverride ?? derivedContentType;
|
|
1052
|
+
if (!contentType) throw new MappaError("contentType is required when it cannot be inferred from the download response");
|
|
1053
|
+
if (typeof Blob === "undefined") throw new MappaError("Blob is not available in this runtime; cannot download and upload from url");
|
|
1054
|
+
const blob = await res.blob();
|
|
1055
|
+
const upload = await this.files.upload({
|
|
1056
|
+
file: blob,
|
|
1057
|
+
contentType,
|
|
1058
|
+
filename,
|
|
1059
|
+
idempotencyKey,
|
|
1060
|
+
requestId,
|
|
1061
|
+
signal
|
|
1062
|
+
});
|
|
1063
|
+
return this.createJob({
|
|
1064
|
+
...rest,
|
|
1065
|
+
media: { mediaId: upload.mediaId },
|
|
1066
|
+
idempotencyKey,
|
|
1067
|
+
requestId
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
async get(reportId, opts) {
|
|
1071
|
+
return (await this.transport.request({
|
|
1072
|
+
method: "GET",
|
|
1073
|
+
path: `/v1/reports/${encodeURIComponent(reportId)}`,
|
|
1074
|
+
requestId: opts?.requestId,
|
|
1075
|
+
signal: opts?.signal,
|
|
1076
|
+
retryable: true
|
|
1077
|
+
})).data;
|
|
1078
|
+
}
|
|
1079
|
+
async getByJob(jobId, opts) {
|
|
1080
|
+
return (await this.transport.request({
|
|
1081
|
+
method: "GET",
|
|
1082
|
+
path: `/v1/reports/by-job/${encodeURIComponent(jobId)}`,
|
|
1083
|
+
requestId: opts?.requestId,
|
|
1084
|
+
signal: opts?.signal,
|
|
1085
|
+
retryable: true
|
|
1086
|
+
})).data;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Convenience wrapper: createJob + wait + get
|
|
1090
|
+
* Use for scripts; for production prefer createJob + webhooks/stream.
|
|
1091
|
+
*/
|
|
1092
|
+
async generate(req, opts) {
|
|
1093
|
+
const receipt = await this.createJob(req);
|
|
1094
|
+
if (!receipt.handle) throw new MappaError("Job receipt is missing handle");
|
|
1095
|
+
return receipt.handle.wait(opts?.wait);
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Convenience wrapper: createJobFromFile + wait + get.
|
|
1099
|
+
* Use for scripts; for production prefer createJobFromFile + webhooks/stream.
|
|
1100
|
+
*/
|
|
1101
|
+
async generateFromFile(req, opts) {
|
|
1102
|
+
const receipt = await this.createJobFromFile(req);
|
|
1103
|
+
if (!receipt.handle) throw new MappaError("Job receipt is missing handle");
|
|
1104
|
+
return receipt.handle.wait(opts?.wait);
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Convenience wrapper: createJobFromUrl + wait + get.
|
|
1108
|
+
* Use for scripts; for production prefer createJobFromUrl + webhooks/stream.
|
|
1109
|
+
*/
|
|
1110
|
+
async generateFromUrl(req, opts) {
|
|
1111
|
+
const receipt = await this.createJobFromUrl(req);
|
|
1112
|
+
if (!receipt.handle) throw new MappaError("Job receipt is missing handle");
|
|
1113
|
+
return receipt.handle.wait(opts?.wait);
|
|
1114
|
+
}
|
|
1115
|
+
makeHandle(jobId) {
|
|
1116
|
+
const self = this;
|
|
1117
|
+
return {
|
|
1118
|
+
jobId,
|
|
1119
|
+
stream: (opts) => self.jobs.stream(jobId, opts),
|
|
1120
|
+
async wait(opts) {
|
|
1121
|
+
const terminal = await self.jobs.wait(jobId, opts);
|
|
1122
|
+
if (!terminal.reportId) throw new MappaError(`Job ${jobId} succeeded but no reportId was returned`);
|
|
1123
|
+
return self.get(terminal.reportId);
|
|
1124
|
+
},
|
|
1125
|
+
cancel: () => self.jobs.cancel(jobId),
|
|
1126
|
+
job: () => self.jobs.get(jobId),
|
|
1127
|
+
report: () => self.getByJob(jobId)
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
defaultIdempotencyKey(_req) {
|
|
1131
|
+
return randomId("idem");
|
|
1132
|
+
}
|
|
1133
|
+
normalizeJobRequest(req) {
|
|
1134
|
+
const target = req.target;
|
|
1135
|
+
if (!target) return {
|
|
1136
|
+
...req,
|
|
1137
|
+
target: { strategy: "dominant" }
|
|
1138
|
+
};
|
|
1139
|
+
const baseTarget = { strategy: target.strategy };
|
|
1140
|
+
if (target.onMiss) baseTarget.on_miss = target.onMiss;
|
|
1141
|
+
if (target.tags && target.tags.length > 0) baseTarget.tags = target.tags;
|
|
1142
|
+
if (target.excludeTags && target.excludeTags.length > 0) baseTarget.exclude_tags = target.excludeTags;
|
|
1143
|
+
switch (target.strategy) {
|
|
1144
|
+
case "dominant": return {
|
|
1145
|
+
...req,
|
|
1146
|
+
target: baseTarget
|
|
1147
|
+
};
|
|
1148
|
+
case "timerange": {
|
|
1149
|
+
const timeRange = target.timeRange ?? {};
|
|
1150
|
+
return {
|
|
1151
|
+
...req,
|
|
1152
|
+
target: {
|
|
1153
|
+
...baseTarget,
|
|
1154
|
+
timerange: {
|
|
1155
|
+
start_seconds: timeRange.startSeconds ?? null,
|
|
1156
|
+
end_seconds: timeRange.endSeconds ?? null
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
case "entity_id": return {
|
|
1162
|
+
...req,
|
|
1163
|
+
target: {
|
|
1164
|
+
...baseTarget,
|
|
1165
|
+
entity_id: target.entityId
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
case "magic_hint": return {
|
|
1169
|
+
...req,
|
|
1170
|
+
target: {
|
|
1171
|
+
...baseTarget,
|
|
1172
|
+
hint: target.hint
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
default: return req;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
//#endregion
|
|
1181
|
+
//#region src/resources/transport.ts
|
|
1182
|
+
function buildUrl(baseUrl, path, query) {
|
|
1183
|
+
const u = new URL(path.replace(/^\//, ""), baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
1184
|
+
if (query) for (const [k, v] of Object.entries(query)) {
|
|
1185
|
+
if (v === void 0) continue;
|
|
1186
|
+
u.searchParams.set(k, String(v));
|
|
1187
|
+
}
|
|
1188
|
+
return u.toString();
|
|
1189
|
+
}
|
|
1190
|
+
async function readBody(res) {
|
|
1191
|
+
const text = await res.text();
|
|
1192
|
+
if (!text) return {
|
|
1193
|
+
parsed: null,
|
|
1194
|
+
text: ""
|
|
1195
|
+
};
|
|
1196
|
+
try {
|
|
1197
|
+
return {
|
|
1198
|
+
parsed: JSON.parse(text),
|
|
1199
|
+
text
|
|
1200
|
+
};
|
|
1201
|
+
} catch {
|
|
1202
|
+
return {
|
|
1203
|
+
parsed: text,
|
|
1204
|
+
text
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function coerceApiError(res, parsed) {
|
|
1209
|
+
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
1210
|
+
let code;
|
|
1211
|
+
let message = `Request failed with status ${res.status}`;
|
|
1212
|
+
let details = parsed;
|
|
1213
|
+
if (typeof parsed === "string") message = parsed;
|
|
1214
|
+
else if (parsed && typeof parsed === "object") {
|
|
1215
|
+
const p = parsed;
|
|
1216
|
+
const err = p.error ?? p;
|
|
1217
|
+
if (err && typeof err === "object") {
|
|
1218
|
+
const e = err;
|
|
1219
|
+
if (typeof e.message === "string") message = e.message;
|
|
1220
|
+
if (typeof e.code === "string") code = e.code;
|
|
1221
|
+
if ("details" in e) details = e.details;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (res.status === 401 || res.status === 403) return new AuthError(message, {
|
|
1225
|
+
status: res.status,
|
|
1226
|
+
requestId,
|
|
1227
|
+
code,
|
|
1228
|
+
details
|
|
1229
|
+
});
|
|
1230
|
+
if (res.status === 422) return new ValidationError(message, {
|
|
1231
|
+
status: res.status,
|
|
1232
|
+
requestId,
|
|
1233
|
+
code,
|
|
1234
|
+
details
|
|
1235
|
+
});
|
|
1236
|
+
if (res.status === 402 && code === "insufficient_credits") return new InsufficientCreditsError(message, {
|
|
1237
|
+
status: res.status,
|
|
1238
|
+
requestId,
|
|
1239
|
+
code,
|
|
1240
|
+
details
|
|
1241
|
+
});
|
|
1242
|
+
if (res.status === 429) {
|
|
1243
|
+
const e = new RateLimitError(message, {
|
|
1244
|
+
status: res.status,
|
|
1245
|
+
requestId,
|
|
1246
|
+
code,
|
|
1247
|
+
details
|
|
1248
|
+
});
|
|
1249
|
+
const ra = res.headers.get("retry-after");
|
|
1250
|
+
if (ra) {
|
|
1251
|
+
const sec = Number(ra);
|
|
1252
|
+
if (Number.isFinite(sec) && sec >= 0) e.retryAfterMs = sec * 1e3;
|
|
1253
|
+
}
|
|
1254
|
+
return e;
|
|
1255
|
+
}
|
|
1256
|
+
return new ApiError(message, {
|
|
1257
|
+
status: res.status,
|
|
1258
|
+
requestId,
|
|
1259
|
+
code,
|
|
1260
|
+
details
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
function shouldRetry(opts, err) {
|
|
1264
|
+
if (!opts.retryable) return { retry: false };
|
|
1265
|
+
if (err instanceof RateLimitError) return {
|
|
1266
|
+
retry: true,
|
|
1267
|
+
retryAfterMs: err.retryAfterMs
|
|
1268
|
+
};
|
|
1269
|
+
if (err instanceof ApiError) return { retry: err.status >= 500 && err.status <= 599 };
|
|
1270
|
+
if (err instanceof TypeError) return { retry: true };
|
|
1271
|
+
return { retry: false };
|
|
1272
|
+
}
|
|
1273
|
+
var Transport = class {
|
|
1274
|
+
fetchImpl;
|
|
1275
|
+
constructor(opts) {
|
|
1276
|
+
this.opts = opts;
|
|
1277
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Stream SSE events from a given path.
|
|
1281
|
+
*
|
|
1282
|
+
* Uses native `fetch` with streaming response body (not browser-only `EventSource`).
|
|
1283
|
+
* Parses SSE format manually from the `ReadableStream`.
|
|
1284
|
+
*/
|
|
1285
|
+
async *streamSSE(path, opts) {
|
|
1286
|
+
const url = buildUrl(this.opts.baseUrl, path);
|
|
1287
|
+
const requestId = randomId("req");
|
|
1288
|
+
const headers = {
|
|
1289
|
+
Accept: "text/event-stream",
|
|
1290
|
+
"Cache-Control": "no-cache",
|
|
1291
|
+
"Mappa-Api-Key": this.opts.apiKey,
|
|
1292
|
+
"X-Request-Id": requestId,
|
|
1293
|
+
...this.opts.userAgent ? { "User-Agent": this.opts.userAgent } : {},
|
|
1294
|
+
...this.opts.defaultHeaders ?? {}
|
|
1295
|
+
};
|
|
1296
|
+
if (opts?.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
|
|
1297
|
+
const controller = new AbortController();
|
|
1298
|
+
const timeout = setTimeout(() => controller.abort(makeAbortError()), this.opts.timeoutMs);
|
|
1299
|
+
if (hasAbortSignal(opts?.signal)) {
|
|
1300
|
+
const signal = opts?.signal;
|
|
1301
|
+
if (signal?.aborted) {
|
|
1302
|
+
clearTimeout(timeout);
|
|
1303
|
+
throw makeAbortError();
|
|
1304
|
+
}
|
|
1305
|
+
signal?.addEventListener("abort", () => controller.abort(makeAbortError()), { once: true });
|
|
1306
|
+
}
|
|
1307
|
+
this.opts.telemetry?.onRequest?.({
|
|
1308
|
+
method: "GET",
|
|
1309
|
+
url,
|
|
1310
|
+
requestId
|
|
1311
|
+
});
|
|
1312
|
+
let res;
|
|
1313
|
+
try {
|
|
1314
|
+
res = await this.fetchImpl(url, {
|
|
1315
|
+
method: "GET",
|
|
1316
|
+
headers,
|
|
1317
|
+
signal: controller.signal
|
|
1318
|
+
});
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
clearTimeout(timeout);
|
|
1321
|
+
this.opts.telemetry?.onError?.({
|
|
1322
|
+
url,
|
|
1323
|
+
requestId,
|
|
1324
|
+
error: err
|
|
1325
|
+
});
|
|
1326
|
+
throw err;
|
|
1327
|
+
}
|
|
1328
|
+
if (!res.ok) {
|
|
1329
|
+
clearTimeout(timeout);
|
|
1330
|
+
const { parsed } = await readBody(res);
|
|
1331
|
+
const apiErr = coerceApiError(res, parsed);
|
|
1332
|
+
this.opts.telemetry?.onError?.({
|
|
1333
|
+
url,
|
|
1334
|
+
requestId,
|
|
1335
|
+
error: apiErr
|
|
1336
|
+
});
|
|
1337
|
+
throw apiErr;
|
|
1338
|
+
}
|
|
1339
|
+
if (!res.body) {
|
|
1340
|
+
clearTimeout(timeout);
|
|
1341
|
+
throw new MappaError("SSE response has no body");
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
yield* this.parseSSEStream(res.body);
|
|
1345
|
+
} finally {
|
|
1346
|
+
clearTimeout(timeout);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Parse SSE events from a ReadableStream.
|
|
1351
|
+
*
|
|
1352
|
+
* SSE format:
|
|
1353
|
+
* ```
|
|
1354
|
+
* id: <id>
|
|
1355
|
+
* event: <type>
|
|
1356
|
+
* data: <json>
|
|
1357
|
+
*
|
|
1358
|
+
* ```
|
|
1359
|
+
* Each event is terminated by an empty line.
|
|
1360
|
+
*/
|
|
1361
|
+
async *parseSSEStream(body) {
|
|
1362
|
+
const decoder = new TextDecoder();
|
|
1363
|
+
const reader = body.getReader();
|
|
1364
|
+
let buffer = "";
|
|
1365
|
+
try {
|
|
1366
|
+
while (true) {
|
|
1367
|
+
const { done, value } = await reader.read();
|
|
1368
|
+
if (done) break;
|
|
1369
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1370
|
+
const events = buffer.split("\n\n");
|
|
1371
|
+
buffer = events.pop() ?? "";
|
|
1372
|
+
for (const eventText of events) {
|
|
1373
|
+
if (!eventText.trim()) continue;
|
|
1374
|
+
const event = this.parseSSEEvent(eventText);
|
|
1375
|
+
if (event) yield event;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (buffer.trim()) {
|
|
1379
|
+
const event = this.parseSSEEvent(buffer);
|
|
1380
|
+
if (event) yield event;
|
|
1381
|
+
}
|
|
1382
|
+
} finally {
|
|
1383
|
+
reader.releaseLock();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Parse a single SSE event from text.
|
|
1388
|
+
*/
|
|
1389
|
+
parseSSEEvent(text) {
|
|
1390
|
+
const lines = text.split("\n");
|
|
1391
|
+
let id;
|
|
1392
|
+
let event = "message";
|
|
1393
|
+
let data = "";
|
|
1394
|
+
for (const line of lines) if (line.startsWith("id:")) id = line.slice(3).trim();
|
|
1395
|
+
else if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
1396
|
+
else if (line.startsWith("data:")) {
|
|
1397
|
+
if (data) data += "\n";
|
|
1398
|
+
data += line.slice(5).trim();
|
|
1399
|
+
}
|
|
1400
|
+
if (!data) return null;
|
|
1401
|
+
let parsedData;
|
|
1402
|
+
try {
|
|
1403
|
+
parsedData = JSON.parse(data);
|
|
1404
|
+
} catch {
|
|
1405
|
+
parsedData = data;
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
id,
|
|
1409
|
+
event,
|
|
1410
|
+
data: parsedData
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
async request(req) {
|
|
1414
|
+
const url = buildUrl(this.opts.baseUrl, req.path, req.query);
|
|
1415
|
+
const requestId = req.requestId ?? randomId("req");
|
|
1416
|
+
const headers = {
|
|
1417
|
+
"Mappa-Api-Key": this.opts.apiKey,
|
|
1418
|
+
"X-Request-Id": requestId,
|
|
1419
|
+
...this.opts.userAgent ? { "User-Agent": this.opts.userAgent } : {},
|
|
1420
|
+
...this.opts.defaultHeaders ?? {}
|
|
1421
|
+
};
|
|
1422
|
+
if (req.idempotencyKey) headers["Idempotency-Key"] = req.idempotencyKey;
|
|
1423
|
+
if (req.headers) {
|
|
1424
|
+
for (const [k, v] of Object.entries(req.headers)) if (v !== void 0) headers[k] = v;
|
|
1425
|
+
}
|
|
1426
|
+
const isFormData = typeof FormData !== "undefined" && req.body instanceof FormData;
|
|
1427
|
+
if (req.body !== void 0 && !isFormData) headers["Content-Type"] = "application/json";
|
|
1428
|
+
const body = req.body === void 0 ? void 0 : isFormData ? req.body : JSON.stringify(req.body);
|
|
1429
|
+
const maxRetries = Math.max(0, this.opts.maxRetries);
|
|
1430
|
+
const startedAt = Date.now();
|
|
1431
|
+
for (let attempt = 1; attempt <= 1 + maxRetries; attempt++) {
|
|
1432
|
+
const controller = new AbortController();
|
|
1433
|
+
const timeout = setTimeout(() => controller.abort(makeAbortError()), this.opts.timeoutMs);
|
|
1434
|
+
if (hasAbortSignal(req.signal)) {
|
|
1435
|
+
const signal = req.signal;
|
|
1436
|
+
if (!signal) {
|
|
1437
|
+
clearTimeout(timeout);
|
|
1438
|
+
throw new Error("Unexpected: abort signal missing");
|
|
1439
|
+
}
|
|
1440
|
+
if (signal.aborted) {
|
|
1441
|
+
clearTimeout(timeout);
|
|
1442
|
+
throw makeAbortError();
|
|
1443
|
+
}
|
|
1444
|
+
signal.addEventListener("abort", () => controller.abort(makeAbortError()), { once: true });
|
|
1445
|
+
}
|
|
1446
|
+
this.opts.telemetry?.onRequest?.({
|
|
1447
|
+
method: req.method,
|
|
1448
|
+
url,
|
|
1449
|
+
requestId
|
|
1450
|
+
});
|
|
1451
|
+
try {
|
|
1452
|
+
const res = await this.fetchImpl(url, {
|
|
1453
|
+
method: req.method,
|
|
1454
|
+
headers,
|
|
1455
|
+
body,
|
|
1456
|
+
signal: controller.signal
|
|
1457
|
+
});
|
|
1458
|
+
const durationMs = Date.now() - startedAt;
|
|
1459
|
+
const serverRequestId = getHeader(res.headers, "x-request-id") ?? requestId;
|
|
1460
|
+
if (!res.ok) {
|
|
1461
|
+
const { parsed } = await readBody(res);
|
|
1462
|
+
const apiErr = coerceApiError(res, parsed);
|
|
1463
|
+
this.opts.telemetry?.onError?.({
|
|
1464
|
+
url,
|
|
1465
|
+
requestId: serverRequestId,
|
|
1466
|
+
error: apiErr
|
|
1467
|
+
});
|
|
1468
|
+
const decision = shouldRetry(req, apiErr);
|
|
1469
|
+
if (attempt <= maxRetries + 1 && decision.retry && attempt <= maxRetries) {
|
|
1470
|
+
const base = decision.retryAfterMs ?? jitter(backoffMs(attempt, 500, 4e3));
|
|
1471
|
+
await new Promise((r) => setTimeout(r, base));
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
throw apiErr;
|
|
1475
|
+
}
|
|
1476
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
1477
|
+
let data;
|
|
1478
|
+
if (ct.includes("application/json")) data = await res.json();
|
|
1479
|
+
else data = await res.text();
|
|
1480
|
+
this.opts.telemetry?.onResponse?.({
|
|
1481
|
+
status: res.status,
|
|
1482
|
+
url,
|
|
1483
|
+
requestId: serverRequestId,
|
|
1484
|
+
durationMs
|
|
1485
|
+
});
|
|
1486
|
+
return {
|
|
1487
|
+
data,
|
|
1488
|
+
status: res.status,
|
|
1489
|
+
requestId: serverRequestId,
|
|
1490
|
+
headers: res.headers
|
|
1491
|
+
};
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
this.opts.telemetry?.onError?.({
|
|
1494
|
+
url,
|
|
1495
|
+
requestId,
|
|
1496
|
+
error: err
|
|
1497
|
+
});
|
|
1498
|
+
const decision = shouldRetry(req, err);
|
|
1499
|
+
if (attempt <= maxRetries && decision.retry) {
|
|
1500
|
+
const sleep = decision.retryAfterMs ?? jitter(backoffMs(attempt, 500, 4e3));
|
|
1501
|
+
await new Promise((r) => setTimeout(r, sleep));
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
throw err;
|
|
1505
|
+
} finally {
|
|
1506
|
+
clearTimeout(timeout);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
throw new Error("Unexpected transport exit");
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region src/resources/webhooks.ts
|
|
1515
|
+
function isObject(v) {
|
|
1516
|
+
return v !== null && typeof v === "object";
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Async signature verification using WebCrypto (works in modern Node and browsers).
|
|
1520
|
+
* Signature scheme placeholder:
|
|
1521
|
+
* headers["mappa-signature"] = "t=1700000000,v1=<hex_hmac_sha256>"
|
|
1522
|
+
* Signed payload: `${t}.${rawBody}`
|
|
1523
|
+
*/
|
|
1524
|
+
var WebhooksResource = class {
|
|
1525
|
+
async verifySignature(params) {
|
|
1526
|
+
const tolerance = params.toleranceSec ?? 300;
|
|
1527
|
+
const sigHeader = headerValue(params.headers, "mappa-signature");
|
|
1528
|
+
if (!sigHeader) throw new Error("Missing mappa-signature header");
|
|
1529
|
+
const parts = parseSig(sigHeader);
|
|
1530
|
+
const ts = Number(parts.t);
|
|
1531
|
+
if (!Number.isFinite(ts)) throw new Error("Invalid signature timestamp");
|
|
1532
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
1533
|
+
if (Math.abs(nowSec - ts) > tolerance) throw new Error("Signature timestamp outside tolerance");
|
|
1534
|
+
const signed = `${parts.t}.${params.payload}`;
|
|
1535
|
+
if (!timingSafeEqualHex(await hmacHex(params.secret, signed), parts.v1)) throw new Error("Invalid signature");
|
|
1536
|
+
return { ok: true };
|
|
1537
|
+
}
|
|
1538
|
+
parseEvent(payload) {
|
|
1539
|
+
const raw = JSON.parse(payload);
|
|
1540
|
+
if (!isObject(raw)) throw new Error("Invalid webhook payload: not an object");
|
|
1541
|
+
const obj = raw;
|
|
1542
|
+
const id = obj.id;
|
|
1543
|
+
const type = obj.type;
|
|
1544
|
+
const createdAt = obj.createdAt;
|
|
1545
|
+
if (typeof id !== "string") throw new Error("Invalid webhook payload: id must be a string");
|
|
1546
|
+
if (typeof type !== "string") throw new Error("Invalid webhook payload: type must be a string");
|
|
1547
|
+
if (typeof createdAt !== "string") throw new Error("Invalid webhook payload: createdAt must be a string");
|
|
1548
|
+
return {
|
|
1549
|
+
id,
|
|
1550
|
+
type,
|
|
1551
|
+
createdAt,
|
|
1552
|
+
data: "data" in obj ? obj.data : void 0
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
function headerValue(headers, name) {
|
|
1557
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
1558
|
+
const v = key ? headers[key] : void 0;
|
|
1559
|
+
if (!v) return void 0;
|
|
1560
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1561
|
+
}
|
|
1562
|
+
function parseSig(h) {
|
|
1563
|
+
const out = {};
|
|
1564
|
+
for (const part of h.split(",")) {
|
|
1565
|
+
const [k, v] = part.split("=");
|
|
1566
|
+
if (k && v) out[k.trim()] = v.trim();
|
|
1567
|
+
}
|
|
1568
|
+
if (!out.t || !out.v1) throw new Error("Invalid signature format");
|
|
1569
|
+
return {
|
|
1570
|
+
t: out.t,
|
|
1571
|
+
v1: out.v1
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
async function hmacHex(secret, message) {
|
|
1575
|
+
const enc = new TextEncoder();
|
|
1576
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
1577
|
+
name: "HMAC",
|
|
1578
|
+
hash: "SHA-256"
|
|
1579
|
+
}, false, ["sign"]);
|
|
1580
|
+
return bufToHex(await crypto.subtle.sign("HMAC", key, enc.encode(message)));
|
|
1581
|
+
}
|
|
1582
|
+
function bufToHex(buf) {
|
|
1583
|
+
const b = new Uint8Array(buf);
|
|
1584
|
+
let s = "";
|
|
1585
|
+
for (const x of b) s += x.toString(16).padStart(2, "0");
|
|
1586
|
+
return s;
|
|
1587
|
+
}
|
|
1588
|
+
function timingSafeEqualHex(a, b) {
|
|
1589
|
+
if (a.length !== b.length) return false;
|
|
1590
|
+
let r = 0;
|
|
1591
|
+
for (let i = 0; i < a.length; i++) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1592
|
+
return r === 0;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
//#endregion
|
|
1596
|
+
//#region src/Mappa.ts
|
|
1597
|
+
/**
|
|
1598
|
+
* Main SDK client.
|
|
1599
|
+
*
|
|
1600
|
+
* Exposes resource namespaces ({@link Mappa.files}, {@link Mappa.reports}, etc.)
|
|
1601
|
+
* and configures a shared HTTP transport.
|
|
1602
|
+
*/
|
|
1603
|
+
var Mappa = class Mappa {
|
|
1604
|
+
files;
|
|
1605
|
+
jobs;
|
|
1606
|
+
reports;
|
|
1607
|
+
feedback;
|
|
1608
|
+
credits;
|
|
1609
|
+
entities;
|
|
1610
|
+
webhooks;
|
|
1611
|
+
health;
|
|
1612
|
+
transport;
|
|
1613
|
+
opts;
|
|
1614
|
+
constructor(options) {
|
|
1615
|
+
if (!options.apiKey) throw new MappaError("apiKey is required");
|
|
1616
|
+
const baseUrl = options.baseUrl ?? "https://api.mappa.ai";
|
|
1617
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1618
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
1619
|
+
this.opts = {
|
|
1620
|
+
...options,
|
|
1621
|
+
apiKey: options.apiKey,
|
|
1622
|
+
baseUrl,
|
|
1623
|
+
timeoutMs,
|
|
1624
|
+
maxRetries
|
|
1625
|
+
};
|
|
1626
|
+
this.transport = new Transport({
|
|
1627
|
+
apiKey: options.apiKey,
|
|
1628
|
+
baseUrl,
|
|
1629
|
+
timeoutMs,
|
|
1630
|
+
maxRetries,
|
|
1631
|
+
defaultHeaders: options.defaultHeaders,
|
|
1632
|
+
fetch: options.fetch,
|
|
1633
|
+
telemetry: options.telemetry,
|
|
1634
|
+
userAgent: options.userAgent
|
|
1635
|
+
});
|
|
1636
|
+
this.files = new FilesResource(this.transport);
|
|
1637
|
+
this.jobs = new JobsResource(this.transport);
|
|
1638
|
+
this.reports = new ReportsResource(this.transport, this.jobs, this.files, this.opts.fetch ?? fetch);
|
|
1639
|
+
this.feedback = new FeedbackResource(this.transport);
|
|
1640
|
+
this.credits = new CreditsResource(this.transport);
|
|
1641
|
+
this.entities = new EntitiesResource(this.transport);
|
|
1642
|
+
this.webhooks = new WebhooksResource();
|
|
1643
|
+
this.health = new HealthResource(this.transport);
|
|
1644
|
+
}
|
|
1645
|
+
withOptions(overrides) {
|
|
1646
|
+
return new Mappa({
|
|
1647
|
+
...this.opts,
|
|
1648
|
+
...overrides,
|
|
1649
|
+
apiKey: overrides.apiKey ?? this.opts.apiKey
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
close() {}
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
//#endregion
|
|
1656
|
+
//#region src/types.ts
|
|
1657
|
+
/**
|
|
1658
|
+
* Type guard for MarkdownReport.
|
|
1659
|
+
*/
|
|
1660
|
+
function isMarkdownReport(report) {
|
|
1661
|
+
return report.output.type === "markdown";
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Type guard for JsonReport.
|
|
1665
|
+
*/
|
|
1666
|
+
function isJsonReport(report) {
|
|
1667
|
+
return report.output.type === "json";
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Type guard for PdfReport.
|
|
1671
|
+
*/
|
|
1672
|
+
function isPdfReport(report) {
|
|
1673
|
+
return report.output.type === "pdf";
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Type guard for UrlReport.
|
|
1677
|
+
*/
|
|
1678
|
+
function isUrlReport(report) {
|
|
1679
|
+
return report.output.type === "url";
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Type guard to check if a report has entity information.
|
|
1683
|
+
* Always returns true since entity is always present in reports.
|
|
1684
|
+
*/
|
|
1685
|
+
function hasEntity(report) {
|
|
1686
|
+
return report.entity !== void 0 && report.entity !== null;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
//#endregion
|
|
1690
|
+
//#region src/index.ts
|
|
1691
|
+
/**
|
|
1692
|
+
* Type guard for catching SDK errors.
|
|
1693
|
+
*/
|
|
1694
|
+
function isMappaError(err) {
|
|
1695
|
+
return err instanceof MappaError;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Type guard for insufficient credits errors.
|
|
1699
|
+
*
|
|
1700
|
+
* @example
|
|
1701
|
+
* ```typescript
|
|
1702
|
+
* try {
|
|
1703
|
+
* await mappa.reports.createJob({ ... });
|
|
1704
|
+
* } catch (err) {
|
|
1705
|
+
* if (isInsufficientCreditsError(err)) {
|
|
1706
|
+
* console.log(`Need ${err.required} credits, have ${err.available}`);
|
|
1707
|
+
* }
|
|
1708
|
+
* }
|
|
1709
|
+
* ```
|
|
1710
|
+
*/
|
|
1711
|
+
function isInsufficientCreditsError(err) {
|
|
1712
|
+
return err instanceof InsufficientCreditsError;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
//#endregion
|
|
1716
|
+
exports.ApiError = ApiError;
|
|
1717
|
+
exports.AuthError = AuthError;
|
|
1718
|
+
exports.InsufficientCreditsError = InsufficientCreditsError;
|
|
1719
|
+
exports.JobCanceledError = JobCanceledError;
|
|
1720
|
+
exports.JobFailedError = JobFailedError;
|
|
1721
|
+
exports.Mappa = Mappa;
|
|
1722
|
+
exports.MappaError = MappaError;
|
|
1723
|
+
exports.RateLimitError = RateLimitError;
|
|
1724
|
+
exports.ValidationError = ValidationError;
|
|
1725
|
+
exports.hasEntity = hasEntity;
|
|
1726
|
+
exports.isInsufficientCreditsError = isInsufficientCreditsError;
|
|
1727
|
+
exports.isJsonReport = isJsonReport;
|
|
1728
|
+
exports.isMappaError = isMappaError;
|
|
1729
|
+
exports.isMarkdownReport = isMarkdownReport;
|
|
1730
|
+
exports.isPdfReport = isPdfReport;
|
|
1731
|
+
exports.isUrlReport = isUrlReport;
|
|
1732
|
+
//# sourceMappingURL=index.cjs.map
|