songbrain 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -5
- package/dist/index.cjs +109 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -15
- package/dist/index.d.ts +114 -15
- package/dist/index.js +109 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,12 +8,15 @@ var SongbrainError = class extends Error {
|
|
|
8
8
|
code;
|
|
9
9
|
/** The parsed response body, when there was one. */
|
|
10
10
|
body;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** The `Songbrain-Request-Id` of the failed request (`req_…`), or null. */
|
|
12
|
+
requestId;
|
|
13
|
+
constructor(status, code, message, body, requestId = null) {
|
|
14
|
+
super(requestId ? `${message} (request_id: ${requestId})` : message);
|
|
13
15
|
this.name = new.target.name;
|
|
14
16
|
this.status = status;
|
|
15
17
|
this.code = code;
|
|
16
18
|
this.body = body;
|
|
19
|
+
this.requestId = requestId;
|
|
17
20
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
18
21
|
}
|
|
19
22
|
};
|
|
@@ -26,8 +29,8 @@ var NotFound = class extends SongbrainError {
|
|
|
26
29
|
var RateLimited = class extends SongbrainError {
|
|
27
30
|
/** Seconds to wait, from the Retry-After header (null if absent). */
|
|
28
31
|
retryAfter;
|
|
29
|
-
constructor(status, code, message, body, retryAfter = null) {
|
|
30
|
-
super(status, code, message, body);
|
|
32
|
+
constructor(status, code, message, body, retryAfter = null, requestId = null) {
|
|
33
|
+
super(status, code, message, body, requestId);
|
|
31
34
|
this.retryAfter = retryAfter;
|
|
32
35
|
}
|
|
33
36
|
};
|
|
@@ -51,10 +54,12 @@ var WaitTimeout = class extends SongbrainError {
|
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
// src/version.ts
|
|
54
|
-
var VERSION = "0.
|
|
57
|
+
var VERSION = "0.2.0";
|
|
55
58
|
|
|
56
59
|
// src/client.ts
|
|
57
60
|
var DEFAULT_BASE_URL = "https://api.songbrain.ai/v1";
|
|
61
|
+
var REQUEST_ID_HEADER = "Songbrain-Request-Id";
|
|
62
|
+
var IDEMPOTENCY_HEADER = "Idempotency-Key";
|
|
58
63
|
var MAX_RETRY_WAIT_SEC = 60;
|
|
59
64
|
var MIME = {
|
|
60
65
|
mp3: "audio/mpeg",
|
|
@@ -99,12 +104,47 @@ function retryAfterSeconds(value) {
|
|
|
99
104
|
return Number.isNaN(when) ? null : Math.max(0, (when - Date.now()) / 1e3);
|
|
100
105
|
}
|
|
101
106
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
107
|
+
function intHeader(value) {
|
|
108
|
+
if (value === null || value === "") return null;
|
|
109
|
+
const n = Number(value);
|
|
110
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
111
|
+
}
|
|
112
|
+
function rateLimitFrom(headers) {
|
|
113
|
+
const limit = intHeader(headers.get("X-RateLimit-Limit"));
|
|
114
|
+
if (limit === null) return null;
|
|
115
|
+
return {
|
|
116
|
+
limit,
|
|
117
|
+
remaining: intHeader(headers.get("X-RateLimit-Remaining")),
|
|
118
|
+
reset: intHeader(headers.get("X-RateLimit-Reset"))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function newIdempotencyKey() {
|
|
122
|
+
const c = globalThis.crypto;
|
|
123
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
124
|
+
try {
|
|
125
|
+
const { randomUUID } = await import('crypto');
|
|
126
|
+
return randomUUID();
|
|
127
|
+
} catch {
|
|
128
|
+
const hex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16));
|
|
129
|
+
hex[12] = "4";
|
|
130
|
+
hex[16] = (parseInt(hex[16], 16) & 3 | 8).toString(16);
|
|
131
|
+
const h = hex.join("");
|
|
132
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
102
135
|
var Songbrain = class {
|
|
103
136
|
apiKey;
|
|
104
137
|
baseUrl;
|
|
105
138
|
timeoutMs;
|
|
106
139
|
maxRetries;
|
|
107
140
|
fetchImpl;
|
|
141
|
+
/**
|
|
142
|
+
* `{ limit, remaining, reset }` from the `X-RateLimit-*` headers of the last response
|
|
143
|
+
* that had them (null before that). `reset` is seconds until the window has room.
|
|
144
|
+
*/
|
|
145
|
+
lastRateLimit = null;
|
|
146
|
+
/** The `Songbrain-Request-Id` of the last response (null before the first one). */
|
|
147
|
+
lastRequestId = null;
|
|
108
148
|
/** @internal Overridable in tests. */
|
|
109
149
|
_sleep = sleep;
|
|
110
150
|
constructor(options = {}) {
|
|
@@ -117,21 +157,29 @@ var Songbrain = class {
|
|
|
117
157
|
this.fetchImpl = f;
|
|
118
158
|
}
|
|
119
159
|
async analyze(options) {
|
|
120
|
-
const { file, audioUrl } = options;
|
|
121
|
-
if (file
|
|
160
|
+
const { file, audioUrl, test } = options;
|
|
161
|
+
if (file !== void 0 && audioUrl !== void 0) {
|
|
122
162
|
throw new TypeError("Pass exactly one of `file` or `audioUrl`.");
|
|
123
163
|
}
|
|
164
|
+
if (file === void 0 && audioUrl === void 0 && !test) {
|
|
165
|
+
throw new TypeError("Pass exactly one of `file` or `audioUrl` (or `test: true`).");
|
|
166
|
+
}
|
|
124
167
|
const fields = {};
|
|
125
168
|
if (options.title !== void 0) fields.title = options.title;
|
|
126
169
|
if (options.artist !== void 0) fields.artist = options.artist;
|
|
127
170
|
if (options.webhookUrl !== void 0) fields.webhook_url = options.webhookUrl;
|
|
128
171
|
if (options.externalRef !== void 0) fields.external_ref = options.externalRef;
|
|
172
|
+
const headers = { [IDEMPOTENCY_HEADER]: options.idempotencyKey || await newIdempotencyKey() };
|
|
129
173
|
let created;
|
|
130
|
-
if (
|
|
131
|
-
|
|
174
|
+
if (file === void 0) {
|
|
175
|
+
const json = audioUrl !== void 0 ? { audio_url: audioUrl, ...fields } : { ...fields };
|
|
176
|
+
if (test) json.test = true;
|
|
177
|
+
created = await this.request("POST", "/songs", { json, headers });
|
|
132
178
|
} else {
|
|
179
|
+
if (test) fields.test = "true";
|
|
133
180
|
const { blob, name } = await toUpload(file, options.filename);
|
|
134
181
|
created = await this.request("POST", "/songs", {
|
|
182
|
+
headers,
|
|
135
183
|
form: () => {
|
|
136
184
|
const form = new FormData();
|
|
137
185
|
form.append("file", blob, name);
|
|
@@ -175,9 +223,28 @@ var Songbrain = class {
|
|
|
175
223
|
shotPlan(songId) {
|
|
176
224
|
return this.request("GET", `/songs/${encodeURIComponent(songId)}/shot-plan`);
|
|
177
225
|
}
|
|
178
|
-
/**
|
|
226
|
+
/** One page of your songs, newest first: `{ object: "list", data, has_more, next_cursor }`. */
|
|
179
227
|
listSongs(options = {}) {
|
|
180
|
-
return this.request("GET", "/songs", {
|
|
228
|
+
return this.request("GET", "/songs", {
|
|
229
|
+
query: { limit: String(options.limit ?? 20), starting_after: options.startingAfter }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* All your songs, newest first, fetching pages as needed.
|
|
234
|
+
*
|
|
235
|
+
* ```ts
|
|
236
|
+
* for await (const item of sb.iterSongs()) console.log(item.id, item.status);
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
async *iterSongs(options = {}) {
|
|
240
|
+
let cursor = options.startingAfter;
|
|
241
|
+
for (; ; ) {
|
|
242
|
+
const page = await this.listSongs({ limit: options.pageSize ?? 100, startingAfter: cursor });
|
|
243
|
+
const data = page.data ?? [];
|
|
244
|
+
yield* data;
|
|
245
|
+
cursor = page.next_cursor ?? data[data.length - 1]?.id;
|
|
246
|
+
if (!page.has_more || !cursor) return;
|
|
247
|
+
}
|
|
181
248
|
}
|
|
182
249
|
/** Delete a song's audio and analysis now. 409 `still_processing` while it runs. */
|
|
183
250
|
deleteSong(songId) {
|
|
@@ -192,6 +259,20 @@ var Songbrain = class {
|
|
|
192
259
|
pricing() {
|
|
193
260
|
return this.request("GET", "/pricing");
|
|
194
261
|
}
|
|
262
|
+
// ── webhooks ─────────────────────────────────────────────────────────────
|
|
263
|
+
/**
|
|
264
|
+
* Send a signed `ping` event to `url` right now:
|
|
265
|
+
* `{ delivered, status_code, latency_ms, event_id }`. Checks your receiver before real songs.
|
|
266
|
+
*/
|
|
267
|
+
testWebhook(url) {
|
|
268
|
+
return this.request("POST", "/webhooks/test", { json: { url } });
|
|
269
|
+
}
|
|
270
|
+
/** The last webhook delivery attempts for your account, newest first. */
|
|
271
|
+
webhookDeliveries(options = {}) {
|
|
272
|
+
return this.request("GET", "/webhooks/deliveries", {
|
|
273
|
+
query: { limit: String(options.limit ?? 20) }
|
|
274
|
+
});
|
|
275
|
+
}
|
|
195
276
|
// ── examples (no key) ────────────────────────────────────────────────────
|
|
196
277
|
/** Example analyses of Songbrain's own songs. No key needed. */
|
|
197
278
|
examples() {
|
|
@@ -212,13 +293,15 @@ var Songbrain = class {
|
|
|
212
293
|
async request(method, path, opts = {}) {
|
|
213
294
|
const url = new URL(this.baseUrl + path);
|
|
214
295
|
for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
|
|
215
|
-
const
|
|
296
|
+
const hasKey = Boolean(opts.headers?.[IDEMPOTENCY_HEADER]);
|
|
297
|
+
const idempotent = method !== "POST" || hasKey;
|
|
216
298
|
for (let attempt = 0; ; attempt++) {
|
|
217
299
|
const headers = {
|
|
218
300
|
Accept: "application/json",
|
|
219
301
|
"User-Agent": `songbrain-js/${VERSION}`
|
|
220
302
|
};
|
|
221
303
|
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
304
|
+
Object.assign(headers, opts.headers);
|
|
222
305
|
let body;
|
|
223
306
|
if (opts.json) {
|
|
224
307
|
headers["Content-Type"] = "application/json";
|
|
@@ -237,6 +320,9 @@ var Songbrain = class {
|
|
|
237
320
|
const message = err instanceof Error ? err.message : String(err);
|
|
238
321
|
throw new SongbrainError(0, "connection_error", message);
|
|
239
322
|
}
|
|
323
|
+
this.lastRequestId = res.headers.get(REQUEST_ID_HEADER) || null;
|
|
324
|
+
const rateLimit = rateLimitFrom(res.headers);
|
|
325
|
+
if (rateLimit) this.lastRateLimit = rateLimit;
|
|
240
326
|
if (res.ok) {
|
|
241
327
|
const text = await res.text();
|
|
242
328
|
if (!text) return {};
|
|
@@ -247,10 +333,12 @@ var Songbrain = class {
|
|
|
247
333
|
}
|
|
248
334
|
}
|
|
249
335
|
const error = await errorFrom(res);
|
|
250
|
-
const
|
|
336
|
+
const inProgress = hasKey && res.status === 409 && error.code === "idempotency_in_progress";
|
|
337
|
+
const retryable = res.status === 429 || inProgress || res.status >= 500 && (idempotent || res.status === 502 || res.status === 503);
|
|
251
338
|
if (retryable && attempt < this.maxRetries) {
|
|
252
339
|
const after = error instanceof RateLimited ? error.retryAfter : null;
|
|
253
|
-
|
|
340
|
+
let delayMs = after !== null ? after * 1e3 : this.backoffMs(attempt);
|
|
341
|
+
if (inProgress) delayMs = Math.max(1e3, delayMs);
|
|
254
342
|
if (delayMs <= MAX_RETRY_WAIT_SEC * 1e3) {
|
|
255
343
|
await this._sleep(delayMs);
|
|
256
344
|
continue;
|
|
@@ -269,6 +357,7 @@ async function errorFrom(res) {
|
|
|
269
357
|
let body;
|
|
270
358
|
let code = `http_${res.status}`;
|
|
271
359
|
let message = text.trim().slice(0, 500) || res.statusText || `HTTP ${res.status}`;
|
|
360
|
+
let requestId = res.headers.get(REQUEST_ID_HEADER) || null;
|
|
272
361
|
try {
|
|
273
362
|
body = JSON.parse(text);
|
|
274
363
|
} catch {
|
|
@@ -280,6 +369,7 @@ async function errorFrom(res) {
|
|
|
280
369
|
const e = err;
|
|
281
370
|
if (e.code) code = String(e.code);
|
|
282
371
|
if (e.message) message = String(e.message);
|
|
372
|
+
if (e.request_id) requestId = String(e.request_id);
|
|
283
373
|
} else if ("detail" in body) {
|
|
284
374
|
code = "validation_error";
|
|
285
375
|
message = JSON.stringify(body.detail);
|
|
@@ -287,15 +377,15 @@ async function errorFrom(res) {
|
|
|
287
377
|
}
|
|
288
378
|
switch (res.status) {
|
|
289
379
|
case 401:
|
|
290
|
-
return new AuthenticationError(401, code, message, body);
|
|
380
|
+
return new AuthenticationError(401, code, message, body, requestId);
|
|
291
381
|
case 402:
|
|
292
|
-
return new InsufficientCredits(402, code, message, body);
|
|
382
|
+
return new InsufficientCredits(402, code, message, body, requestId);
|
|
293
383
|
case 404:
|
|
294
|
-
return new NotFound(404, code, message, body);
|
|
384
|
+
return new NotFound(404, code, message, body, requestId);
|
|
295
385
|
case 429:
|
|
296
|
-
return new RateLimited(429, code, message, body, retryAfterSeconds(res.headers.get("Retry-After")));
|
|
386
|
+
return new RateLimited(429, code, message, body, retryAfterSeconds(res.headers.get("Retry-After")), requestId);
|
|
297
387
|
default:
|
|
298
|
-
return new SongbrainError(res.status, code, message, body);
|
|
388
|
+
return new SongbrainError(res.status, code, message, body, requestId);
|
|
299
389
|
}
|
|
300
390
|
}
|
|
301
391
|
async function toUpload(file, filename) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/version.ts","../src/client.ts","../src/webhooks.ts"],"names":[],"mappings":";;;AAKO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA;AAAA,EAE/B,MAAA;AAAA;AAAA,EAEA,IAAA;AAAA;AAAA,EAEA,IAAA;AAAA,EAET,WAAA,CAAY,MAAA,EAAgB,IAAA,EAAc,OAAA,EAAiB,IAAA,EAAgB;AACzE,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,OAAO,GAAA,CAAA,MAAA,CAAW,IAAA;AACvB,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EAClD;AACF;AAGO,IAAM,mBAAA,GAAN,cAAkC,cAAA,CAAe;AAAC;AAGlD,IAAM,mBAAA,GAAN,cAAkC,cAAA,CAAe;AAAC;AAGlD,IAAM,QAAA,GAAN,cAAuB,cAAA,CAAe;AAAC;AAGvC,IAAM,WAAA,GAAN,cAA0B,cAAA,CAAe;AAAA;AAAA,EAErC,UAAA;AAAA,EAET,YAAY,MAAA,EAAgB,IAAA,EAAc,OAAA,EAAiB,IAAA,EAAgB,aAA4B,IAAA,EAAM;AAC3G,IAAA,KAAA,CAAM,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,IAAI,CAAA;AACjC,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAAA,EACpB;AACF;AAGO,IAAM,cAAA,GAAN,cAA6B,cAAA,CAAe;AAAA,EACxC,MAAA;AAAA,EAET,WAAA,CAAY,MAAA,EAAgB,IAAA,EAAc,OAAA,EAAiB,IAAA,EAAgB;AACzE,IAAA,KAAA,CAAM,CAAA,EAAG,IAAA,EAAM,OAAA,EAAS,IAAI,CAAA;AAC5B,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;AAGO,IAAM,WAAA,GAAN,cAA0B,cAAA,CAAe;AAAA,EACrC,MAAA;AAAA,EAET,WAAA,CAAY,QAAgB,SAAA,EAAmB;AAC7C,IAAA,KAAA;AAAA,MACE,CAAA;AAAA,MACA,cAAA;AAAA,MACA,CAAA,KAAA,EAAQ,MAAM,CAAA,oBAAA,EAAuB,IAAA,CAAK,MAAM,SAAA,GAAY,GAAI,CAAC,CAAA,yDAAA,EAA4D,MAAM,CAAA,GAAA;AAAA,KACrI;AACA,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;;;ACjEO,IAAM,OAAA,GAAU;;;ACsBhB,IAAM,gBAAA,GAAmB;AAqDhC,IAAM,kBAAA,GAAqB,EAAA;AAC3B,IAAM,IAAA,GAA+B;AAAA,EACnC,GAAA,EAAK,YAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EACN,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EACN,GAAA,EAAK;AACP,CAAA;AAEA,SAAS,IAAI,IAAA,EAAkC;AAC7C,EAAA,MAAM,IAAK,UAAA,CAA0E,OAAA;AACrF,EAAA,OAAO,CAAA,EAAG,GAAA,GAAM,IAAI,CAAA,IAAK,MAAA;AAC3B;AAGO,SAAS,eAAe,IAAA,EAAiC;AAC9D,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,EAAc,EAAA,KAAe,MAAA,CAAO,YAAA,CAAa,GAAG,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,EAAE,CAAC,CAAA;AAC1F,EAAA,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,KAAM,OAAO,OAAO,KAAA;AAClC,EAAA,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,KAAM,MAAA,IAAU,MAAM,CAAA,EAAG,EAAE,CAAA,KAAM,MAAA,EAAQ,OAAO,KAAA;AAC9D,EAAA,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,KAAM,QAAQ,OAAO,MAAA;AACnC,EAAA,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,KAAM,QAAQ,OAAO,KAAA;AACnC,EAAA,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,KAAM,UAAU,CAAC,MAAA,EAAQ,MAAM,CAAA,CAAE,SAAS,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,GAAG,OAAO,MAAA;AAC9E,EAAA,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,KAAM,QAAQ,OAAO,KAAA;AACnC,EAAA,MAAM,EAAA,GAAK,KAAK,CAAC,CAAA;AACjB,EAAA,MAAM,EAAA,GAAK,KAAK,CAAC,CAAA;AACjB,EAAA,IAAI,EAAA,KAAO,GAAA,IAAQ,EAAA,KAAO,MAAA,IAAA,CAAc,EAAA,GAAK,GAAA,MAAU,GAAA,EAAM,OAAA,CAAQ,EAAA,GAAK,CAAA,MAAU,CAAA,GAAI,KAAA,GAAQ,KAAA;AAChG,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,MAAM,IAAA,EAAsB;AACnC,EAAA,MAAM,OAAO,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA,CAAE,KAAI,IAAK,EAAA;AAC1C,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,WAAA,CAAY,GAAG,CAAA;AAC9B,EAAA,OAAO,CAAA,GAAI,IAAI,IAAA,CAAK,KAAA,CAAM,IAAI,CAAC,CAAA,CAAE,aAAY,GAAI,EAAA;AACnD;AAEA,SAAS,SAAS,IAAA,EAAsB;AACtC,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA,CAAE,KAAI,IAAK,IAAA;AACtC;AAEA,SAAS,kBAAkB,KAAA,EAAqC;AAC9D,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,CAAA,GAAI,OAAO,KAAK,CAAA;AACtB,EAAA,IAAI,MAAA,CAAO,SAAS,CAAC,CAAA,SAAU,IAAA,CAAK,GAAA,CAAI,GAAG,CAAC,CAAA;AAC5C,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC7B,EAAA,OAAO,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAA,CAAI,IAAA,GAAO,IAAA,CAAK,GAAA,EAAI,IAAK,GAAI,CAAA;AAC3E;AAEA,IAAM,KAAA,GAAQ,CAAC,EAAA,KAAe,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAY7E,IAAM,YAAN,MAAgB;AAAA,EACZ,MAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACQ,SAAA;AAAA;AAAA,EAEjB,MAAA,GAAwC,KAAA;AAAA,EAExC,WAAA,CAAY,OAAA,GAA4B,EAAC,EAAG;AAC1C,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,KAAW,MAAA,GAAY,QAAQ,MAAA,IAAU,MAAA,GAAY,IAAI,mBAAmB,CAAA;AAClG,IAAA,IAAA,CAAK,OAAA,GAAA,CAAW,QAAQ,OAAA,IAAW,GAAA,CAAI,oBAAoB,CAAA,IAAK,gBAAA,EAAkB,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACpG,IAAA,IAAA,CAAK,SAAA,GAAY,QAAQ,SAAA,IAAa,GAAA;AACtC,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAA,CAAQ,cAAc,CAAC,CAAA;AACrD,IAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,KAAA,IAAU,UAAA,CAAW,KAAA;AACvC,IAAA,IAAI,CAAC,CAAA,EAAG,MAAM,IAAI,MAAM,qEAAqE,CAAA;AAC7F,IAAA,IAAA,CAAK,SAAA,GAAY,CAAA;AAAA,EACnB;AAAA,EAYA,MAAM,QAAQ,OAAA,EAAsD;AAClE,IAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAS,GAAI,OAAA;AAC3B,IAAA,IAAK,IAAA,KAAS,MAAA,MAAgB,QAAA,KAAa,MAAA,CAAA,EAAY;AACrD,MAAA,MAAM,IAAI,UAAU,2CAA2C,CAAA;AAAA,IACjE;AACA,IAAA,MAAM,SAAiC,EAAC;AACxC,IAAA,IAAI,OAAA,CAAQ,KAAA,KAAU,MAAA,EAAW,MAAA,CAAO,QAAQ,OAAA,CAAQ,KAAA;AACxD,IAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,MAAA,EAAW,MAAA,CAAO,SAAS,OAAA,CAAQ,MAAA;AAC1D,IAAA,IAAI,OAAA,CAAQ,UAAA,KAAe,MAAA,EAAW,MAAA,CAAO,cAAc,OAAA,CAAQ,UAAA;AACnE,IAAA,IAAI,OAAA,CAAQ,WAAA,KAAgB,MAAA,EAAW,MAAA,CAAO,eAAe,OAAA,CAAQ,WAAA;AAErE,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,MAAA,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAqB,MAAA,EAAQ,QAAA,EAAU,EAAE,IAAA,EAAM,EAAE,SAAA,EAAW,QAAA,EAAU,GAAG,MAAA,IAAU,CAAA;AAAA,IAC1G,CAAA,MAAO;AACL,MAAA,MAAM,EAAE,MAAM,IAAA,EAAK,GAAI,MAAM,QAAA,CAAS,IAAA,EAAmB,QAAQ,QAAQ,CAAA;AACzE,MAAA,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAqB,MAAA,EAAQ,QAAA,EAAU;AAAA,QAC1D,MAAM,MAAM;AACV,UAAA,MAAM,IAAA,GAAO,IAAI,QAAA,EAAS;AAC1B,UAAA,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,IAAA,EAAM,IAAI,CAAA;AAC9B,UAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,CAAA,IAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG,IAAA,CAAK,MAAA,CAAO,CAAA,EAAG,CAAC,CAAA;AAC7D,UAAA,OAAO,IAAA;AAAA,QACT;AAAA,OACD,CAAA;AAAA,IACH;AACA,IAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,KAAA,EAAO,OAAO,OAAA;AACnC,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,EAAA,EAAI;AAAA,MAC9B,gBAAgB,OAAA,CAAQ,cAAA;AAAA,MACxB,WAAW,OAAA,CAAQ;AAAA,KACpB,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,OAAA,CAAQ,MAAA,EAAgB,OAAA,GAAuB,EAAC,EAAkB;AACtE,IAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,GAAA;AACvC,IAAA,MAAM,WAAW,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,OAAA,CAAQ,kBAAkB,GAAK,CAAA;AAC9D,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAC9B,IAAA,WAAS;AACP,MAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA,CAAQ,MAAM,CAAA;AACrC,MAAA,IAAI,GAAA,CAAI,MAAA,KAAW,MAAA,EAAQ,OAAO,GAAA;AAClC,MAAA,IAAI,GAAA,CAAI,WAAW,QAAA,EAAU;AAC3B,QAAA,MAAM,IAAI,cAAA;AAAA,UACR,MAAA;AAAA,UACA,GAAA,CAAI,OAAO,IAAA,IAAQ,iBAAA;AAAA,UACnB,GAAA,CAAI,OAAO,OAAA,IAAW,gEAAA;AAAA,UACtB;AAAA,SACF;AAAA,MACF;AACA,MAAA,MAAM,SAAA,GAAY,QAAA,GAAW,IAAA,CAAK,GAAA,EAAI;AACtC,MAAA,IAAI,aAAa,CAAA,EAAG,MAAM,IAAI,WAAA,CAAY,QAAQ,SAAS,CAAA;AAC3D,MAAA,MAAM,KAAK,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,SAAS,CAAC,CAAA;AAAA,IACjD;AAAA,EACF;AAAA;AAAA,EAGA,OAAA,CAAQ,MAAA,EAAgB,OAAA,GAA0B,EAAC,EAAkB;AACnE,IAAA,OAAO,IAAA,CAAK,OAAA,CAAc,KAAA,EAAO,CAAA,OAAA,EAAU,kBAAA,CAAmB,MAAM,CAAC,CAAA,CAAA,EAAI,EAAE,KAAA,EAAO,QAAA,CAAS,OAAO,GAAG,CAAA;AAAA,EACvG;AAAA;AAAA,EAGA,SAAS,MAAA,EAAuC;AAC9C,IAAA,OAAO,KAAK,OAAA,CAAsB,KAAA,EAAO,UAAU,kBAAA,CAAmB,MAAM,CAAC,CAAA,UAAA,CAAY,CAAA;AAAA,EAC3F;AAAA;AAAA,EAGA,SAAA,CAAU,OAAA,GAA8B,EAAC,EAAsB;AAC7D,IAAA,OAAO,IAAA,CAAK,OAAA,CAAkB,KAAA,EAAO,QAAA,EAAU,EAAE,KAAA,EAAO,EAAE,KAAA,EAAO,MAAA,CAAO,OAAA,CAAQ,KAAA,IAAS,EAAE,CAAA,IAAK,CAAA;AAAA,EAClG;AAAA;AAAA,EAGA,WAAW,MAAA,EAAuC;AAChD,IAAA,OAAO,KAAK,OAAA,CAAsB,QAAA,EAAU,UAAU,kBAAA,CAAmB,MAAM,CAAC,CAAA,CAAE,CAAA;AAAA,EACpF;AAAA;AAAA;AAAA,EAKA,OAAA,GAA4B;AAC1B,IAAA,OAAO,IAAA,CAAK,OAAA,CAAiB,KAAA,EAAO,UAAU,CAAA;AAAA,EAChD;AAAA;AAAA,EAGA,OAAA,GAA4B;AAC1B,IAAA,OAAO,IAAA,CAAK,OAAA,CAAiB,KAAA,EAAO,UAAU,CAAA;AAAA,EAChD;AAAA;AAAA;AAAA,EAKA,QAAA,GAAiC;AAC/B,IAAA,OAAO,IAAA,CAAK,OAAA,CAAqB,KAAA,EAAO,WAAW,CAAA;AAAA,EACrD;AAAA;AAAA,EAGA,OAAA,CAAQ,SAAA,EAAmB,OAAA,GAA0B,EAAC,EAAkB;AACtE,IAAA,OAAO,IAAA,CAAK,OAAA,CAAc,KAAA,EAAO,CAAA,UAAA,EAAa,kBAAA,CAAmB,SAAS,CAAC,CAAA,CAAA,EAAI,EAAE,KAAA,EAAO,QAAA,CAAS,OAAO,GAAG,CAAA;AAAA,EAC7G;AAAA;AAAA,EAGA,gBAAgB,SAAA,EAA0C;AACxD,IAAA,OAAO,KAAK,OAAA,CAAsB,KAAA,EAAO,aAAa,kBAAA,CAAmB,SAAS,CAAC,CAAA,UAAA,CAAY,CAAA;AAAA,EACjG;AAAA;AAAA,EAIQ,UAAU,OAAA,EAAyB;AACzC,IAAA,OAAO,IAAA,CAAK,IAAI,GAAA,GAAM,CAAA,IAAK,SAAS,GAAK,CAAA,GAAI,IAAA,CAAK,MAAA,EAAO,GAAI,GAAA;AAAA,EAC/D;AAAA,EAEA,MAAc,OAAA,CACZ,MAAA,EACA,IAAA,EACA,IAAA,GAA2F,EAAC,EAChF;AACZ,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,IAAA,CAAK,UAAU,IAAI,CAAA;AACvC,IAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,KAAA,IAAS,EAAE,CAAA,MAAO,CAAA,KAAM,MAAA,MAAe,YAAA,CAAa,GAAA,CAAI,GAAG,CAAC,CAAA;AACrG,IAAA,MAAM,aAAa,MAAA,KAAW,MAAA;AAE9B,IAAA,KAAA,IAAS,OAAA,GAAU,KAAK,OAAA,EAAA,EAAW;AACjC,MAAA,MAAM,OAAA,GAAkC;AAAA,QACtC,MAAA,EAAQ,kBAAA;AAAA,QACR,YAAA,EAAc,gBAAgB,OAAO,CAAA;AAAA,OACvC;AACA,MAAA,IAAI,KAAK,MAAA,EAAQ,OAAA,CAAQ,aAAA,GAAgB,CAAA,OAAA,EAAU,KAAK,MAAM,CAAA,CAAA;AAC9D,MAAA,IAAI,IAAA;AACJ,MAAA,IAAI,KAAK,IAAA,EAAM;AACb,QAAA,OAAA,CAAQ,cAAc,CAAA,GAAI,kBAAA;AAC1B,QAAA,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA;AAAA,MACjC,CAAA,MAAA,IAAW,KAAK,IAAA,EAAM;AACpB,QAAA,IAAA,GAAO,KAAK,IAAA,EAAK;AAAA,MACnB;AAEA,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,GAAA,GAAM,MAAM,IAAA,CAAK,SAAA,CAAU,GAAA,EAAK,EAAE,MAAA,EAAQ,OAAA,EAAS,IAAA,EAAM,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,IAAA,CAAK,SAAS,GAAG,CAAA;AAAA,MACxG,SAAS,GAAA,EAAK;AAEZ,QAAA,IAAI,UAAA,IAAc,OAAA,GAAU,IAAA,CAAK,UAAA,EAAY;AAC3C,UAAA,MAAM,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AACzC,UAAA;AAAA,QACF;AACA,QAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,QAAA,MAAM,IAAI,cAAA,CAAe,CAAA,EAAG,kBAAA,EAAoB,OAAO,CAAA;AAAA,MACzD;AAEA,MAAA,IAAI,IAAI,EAAA,EAAI;AACV,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,IAAI,CAAC,IAAA,EAAM,OAAO,EAAC;AACnB,QAAA,IAAI;AACF,UAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,QACxB,CAAA,CAAA,MAAQ;AACN,UAAA,MAAM,IAAI,cAAA,CAAe,GAAA,CAAI,MAAA,EAAQ,oBAAoB,uBAAuB,CAAA;AAAA,QAClF;AAAA,MACF;AAEA,MAAA,MAAM,KAAA,GAAQ,MAAM,SAAA,CAAU,GAAG,CAAA;AAEjC,MAAA,MAAM,SAAA,GACJ,GAAA,CAAI,MAAA,KAAW,GAAA,IAAQ,GAAA,CAAI,MAAA,IAAU,GAAA,KAAQ,UAAA,IAAc,GAAA,CAAI,MAAA,KAAW,GAAA,IAAO,GAAA,CAAI,MAAA,KAAW,GAAA,CAAA;AAClG,MAAA,IAAI,SAAA,IAAa,OAAA,GAAU,IAAA,CAAK,UAAA,EAAY;AAC1C,QAAA,MAAM,KAAA,GAAQ,KAAA,YAAiB,WAAA,GAAc,KAAA,CAAM,UAAA,GAAa,IAAA;AAChE,QAAA,MAAM,UAAU,KAAA,KAAU,IAAA,GAAO,QAAQ,GAAA,GAAO,IAAA,CAAK,UAAU,OAAO,CAAA;AACtE,QAAA,IAAI,OAAA,IAAW,qBAAqB,GAAA,EAAM;AACxC,UAAA,MAAM,IAAA,CAAK,OAAO,OAAO,CAAA;AACzB,UAAA;AAAA,QACF;AAAA,MACF;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF;AAEA,SAAS,SAAS,OAAA,EAA6D;AAC7E,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,OAAO,CAAA,GAAI,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,GAAG,CAAA,GAAI,OAAA,CAAQ,OAAA;AACrF,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,CAAQ,IAAA,EAAM,OAAA,EAAQ;AACvC;AAEA,eAAe,UAAU,GAAA,EAAwC;AAC/D,EAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC5C,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI,IAAA,GAAO,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAA;AAC7B,EAAA,IAAI,OAAA,GAAU,IAAA,CAAK,IAAA,EAAK,CAAE,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,IAAK,GAAA,CAAI,UAAA,IAAc,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAA;AAC/E,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACxB,CAAA,CAAA,MAAQ;AACN,IAAA,IAAA,GAAO,MAAA;AAAA,EACT;AACA,EAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,EAAU;AACpC,IAAA,MAAM,MAAO,IAAA,CAA6B,KAAA;AAC1C,IAAA,IAAI,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,EAAU;AAClC,MAAA,MAAM,CAAA,GAAI,GAAA;AACV,MAAA,IAAI,CAAA,CAAE,IAAA,EAAM,IAAA,GAAO,MAAA,CAAO,EAAE,IAAI,CAAA;AAChC,MAAA,IAAI,CAAA,CAAE,OAAA,EAAS,OAAA,GAAU,MAAA,CAAO,EAAE,OAAO,CAAA;AAAA,IAC3C,CAAA,MAAA,IAAW,YAAa,IAAA,EAAiB;AACvC,MAAA,IAAA,GAAO,kBAAA;AACP,MAAA,OAAA,GAAU,IAAA,CAAK,SAAA,CAAW,IAAA,CAA6B,MAAM,CAAA;AAAA,IAC/D;AAAA,EACF;AACA,EAAA,QAAQ,IAAI,MAAA;AAAQ,IAClB,KAAK,GAAA;AACH,MAAA,OAAO,IAAI,mBAAA,CAAoB,GAAA,EAAK,IAAA,EAAM,SAAS,IAAI,CAAA;AAAA,IACzD,KAAK,GAAA;AACH,MAAA,OAAO,IAAI,mBAAA,CAAoB,GAAA,EAAK,IAAA,EAAM,SAAS,IAAI,CAAA;AAAA,IACzD,KAAK,GAAA;AACH,MAAA,OAAO,IAAI,QAAA,CAAS,GAAA,EAAK,IAAA,EAAM,SAAS,IAAI,CAAA;AAAA,IAC9C,KAAK,GAAA;AACH,MAAA,OAAO,IAAI,WAAA,CAAY,GAAA,EAAK,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,iBAAA,CAAkB,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAC,CAAC,CAAA;AAAA,IACpG;AACE,MAAA,OAAO,IAAI,cAAA,CAAe,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,SAAS,IAAI,CAAA;AAAA;AAE/D;AAEA,eAAe,QAAA,CAAS,MAAiB,QAAA,EAA0D;AACjG,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,MAAM,EAAE,QAAA,EAAS,GAAI,MAAM,OAAO,aAAkB,CAAA;AACpD,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAI,CAAA;AAChC,IAAA,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,IAAgB,CAAC,CAAA;AAClC,IAAA,UAAA,GAAa,SAAS,IAAI,CAAA;AAAA,EAC5B,CAAA,MAAA,IAAW,gBAAgB,WAAA,EAAa;AACtC,IAAA,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,IAAI,CAAC,CAAA;AAAA,EACxB,CAAA,MAAA,IAAW,gBAAgB,UAAA,EAAY;AACrC,IAAA,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,IAAgB,CAAC,CAAA;AAAA,EACpC,CAAA,MAAA,IAAW,OAAO,IAAA,KAAS,WAAA,IAAe,gBAAgB,IAAA,EAAM;AAC9D,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,MAAM,IAAK,IAAA,CAAmC,IAAA;AAC9C,IAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,EAAG,UAAA,GAAa,SAAS,CAAC,CAAA;AAAA,EACzD,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,UAAU,4EAA4E,CAAA;AAAA,EAClG;AAEA,EAAA,IAAI,OAAO,QAAA,IAAY,UAAA;AACvB,EAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,KAAA,CAAM,IAAI,CAAA,EAAG;AACzB,IAAA,MAAM,IAAA,GAAO,IAAI,UAAA,CAAW,MAAM,IAAA,CAAK,MAAM,CAAA,EAAG,EAAE,CAAA,CAAE,WAAA,EAAa,CAAA;AACjE,IAAA,MAAM,GAAA,GAAM,eAAe,IAAI,CAAA;AAC/B,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,MAAM,IAAI,UAAU,gGAAgG,CAAA;AAAA,IACtH;AACA,IAAA,IAAA,GAAO,CAAA,EAAA,CAAI,QAAQ,MAAA,EAAQ,OAAA,CAAQ,OAAO,EAAE,CAAC,IAAI,GAAG,CAAA,CAAA;AAAA,EACtD;AACA,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAI,CAAC,CAAA;AAC7B,EAAA,IAAI,IAAA,IAAQ,CAAC,IAAA,CAAK,IAAA,EAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,IAAI,CAAA,EAAG,EAAE,IAAA,EAAM,CAAA;AACxD,EAAA,OAAO,EAAE,MAAM,IAAA,EAAK;AACtB;ACtZO,IAAM,gBAAA,GAAmB;AAIhC,SAAS,SAAS,IAAA,EAAuB;AACvC,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,SAAiB,MAAA,CAAO,IAAA,CAAK,MAAM,MAAM,CAAA;AAC7D,EAAA,IAAI,IAAA,YAAgB,aAAa,OAAO,MAAA,CAAO,KAAK,IAAI,UAAA,CAAW,IAAI,CAAC,CAAA;AACxE,EAAA,OAAO,OAAO,IAAA,CAAK,IAAA,CAAK,QAAQ,IAAA,CAAK,UAAA,EAAY,KAAK,UAAU,CAAA;AAClE;AAEA,SAAS,YAAY,MAAA,EAAoD;AACvE,EAAA,IAAI,CAAA,GAAmB,IAAA;AACvB,EAAA,MAAM,KAAe,EAAC;AACtB,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,EAAG;AACpC,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAA;AAC1B,IAAA,IAAI,IAAI,CAAA,EAAG;AACX,IAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,CAAC,EAAE,IAAA,EAAK;AAClC,IAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,CAAM,CAAA,GAAI,CAAC,EAAE,IAAA,EAAK;AACrC,IAAA,IAAI,QAAQ,GAAA,EAAK;AACf,MAAA,IAAI,CAAC,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,EAAG,OAAO,EAAE,CAAA,EAAG,IAAA,EAAM,EAAA,EAAI,EAAC,EAAE;AACnD,MAAA,CAAA,GAAI,OAAO,KAAK,CAAA;AAAA,IAClB,CAAA,MAAA,IAAW,GAAA,KAAQ,IAAA,IAAQ,KAAA,EAAO;AAChC,MAAA,EAAA,CAAG,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,CAAA;AAAA,IAC7B;AAAA,EACF;AACA,EAAA,OAAO,EAAE,GAAG,EAAA,EAAG;AACjB;AAEA,SAAS,GAAA,CAAI,IAAA,EAAc,CAAA,EAAW,MAAA,EAAwB;AAC5D,EAAA,OAAO,UAAA,CAAW,QAAA,EAAU,MAAM,CAAA,CAAE,MAAA,CAAO,CAAA,EAAG,CAAC,CAAA,CAAA,CAAG,CAAA,CAAE,MAAA,CAAO,IAAI,CAAA,CAAE,OAAO,KAAK,CAAA;AAC/E;AAWO,SAAS,aAAA,CACd,OAAA,EACA,MAAA,EACA,MAAA,EACA,eAAe,GAAA,EACN;AACT,EAAA,IAAI,CAAC,MAAA,IAAU,CAAC,MAAA,EAAQ,OAAO,KAAA;AAC/B,EAAA,MAAM,EAAE,CAAA,EAAG,EAAA,EAAG,GAAI,YAAY,MAAM,CAAA;AACpC,EAAA,IAAI,CAAA,KAAM,IAAA,IAAQ,EAAA,CAAG,MAAA,KAAW,GAAG,OAAO,KAAA;AAC1C,EAAA,IAAI,YAAA,GAAe,CAAA,IAAK,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,EAAI,GAAI,GAAA,GAAO,CAAC,CAAA,GAAI,YAAA,EAAc,OAAO,KAAA;AAC/E,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,OAAO,CAAA,EAAG,CAAA,EAAG,MAAM,CAAA,EAAG,MAAM,CAAA;AACtE,EAAA,OAAO,EAAA,CAAG,IAAA,CAAK,CAAC,GAAA,KAAQ;AACtB,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,GAAA,EAAK,MAAM,CAAA;AACrC,IAAA,OAAO,MAAM,MAAA,KAAW,QAAA,CAAS,MAAA,IAAU,eAAA,CAAgB,OAAO,QAAQ,CAAA;AAAA,EAC5E,CAAC,CAAA;AACH;AAOO,SAAS,qBAAA,CACd,OAAA,EACA,MAAA,EACA,MAAA,EACA,eAAe,GAAA,EACE;AACjB,EAAA,IAAI,CAAC,aAAA,CAAc,OAAA,EAAS,MAAA,EAAQ,MAAA,EAAQ,YAAY,CAAA,EAAG;AACzD,IAAA,MAAM,IAAI,MAAM,6BAA6B,CAAA;AAAA,EAC/C;AACA,EAAA,OAAO,KAAK,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA,CAAE,QAAA,CAAS,MAAM,CAAC,CAAA;AACtD;AAGO,SAAS,WAAA,CAAY,OAAA,EAAkB,MAAA,EAAgB,YAAA,GAAe,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA,EAAI,GAAI,GAAI,CAAA,EAAW;AAClH,EAAA,OAAO,CAAA,EAAA,EAAK,YAAY,CAAA,IAAA,EAAO,GAAA,CAAI,SAAS,OAAO,CAAA,EAAG,YAAA,EAAc,MAAM,CAAC,CAAA,CAAA;AAC7E","file":"index.js","sourcesContent":["/**\n * Every API error has the shape `{ error: { code, message } }`. The client\n * turns it into a `SongbrainError` (or a subclass) with `status`, `code` and\n * `message`.\n */\nexport class SongbrainError extends Error {\n /** HTTP status, or 0 when the error did not come from an HTTP response. */\n readonly status: number;\n /** Machine-readable code, e.g. \"invalid_url\" or \"rate_limited\". */\n readonly code: string;\n /** The parsed response body, when there was one. */\n readonly body: unknown;\n\n constructor(status: number, code: string, message: string, body?: unknown) {\n super(message);\n this.name = new.target.name;\n this.status = status;\n this.code = code;\n this.body = body;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** 401: the API key is missing, invalid or revoked. */\nexport class AuthenticationError extends SongbrainError {}\n\n/** 402: the free songs for this month are used up and the balance is below one song. */\nexport class InsufficientCredits extends SongbrainError {}\n\n/** 404: unknown id, or the song belongs to another account. */\nexport class NotFound extends SongbrainError {}\n\n/** 429: too many requests, too many songs in flight, daily cap or busy pipeline. */\nexport class RateLimited extends SongbrainError {\n /** Seconds to wait, from the Retry-After header (null if absent). */\n readonly retryAfter: number | null;\n\n constructor(status: number, code: string, message: string, body?: unknown, retryAfter: number | null = null) {\n super(status, code, message, body);\n this.retryAfter = retryAfter;\n }\n}\n\n/** The song was accepted but its analysis failed. Paid credits are refunded automatically. */\nexport class AnalysisFailed extends SongbrainError {\n readonly songId: string;\n\n constructor(songId: string, code: string, message: string, body?: unknown) {\n super(0, code, message, body);\n this.songId = songId;\n }\n}\n\n/** `analyze({ wait: true })` gave up before the song was done. It keeps processing on the server. */\nexport class WaitTimeout extends SongbrainError {\n readonly songId: string;\n\n constructor(songId: string, timeoutMs: number) {\n super(\n 0,\n \"wait_timeout\",\n `Song ${songId} was not done after ${Math.round(timeoutMs / 1000)} s. It is still processing; fetch it later with getSong(\"${songId}\").`,\n );\n this.songId = songId;\n }\n}\n","export const VERSION = \"0.1.2\";\n","import {\n AnalysisFailed,\n AuthenticationError,\n InsufficientCredits,\n NotFound,\n RateLimited,\n SongbrainError,\n WaitTimeout,\n} from \"./errors.js\";\nimport type {\n Account,\n DeleteResult,\n ExampleList,\n Pricing,\n SectionName,\n Song,\n SongCreated,\n SongList,\n SongShotPlan,\n} from \"./types.js\";\nimport { VERSION } from \"./version.js\";\n\nexport const DEFAULT_BASE_URL = \"https://api.songbrain.ai/v1\";\n\n/** A Blob/File, a Buffer or bytes, or a local file path (Node). */\nexport type FileInput = Blob | Uint8Array | ArrayBuffer | string;\n\nexport interface SongbrainOptions {\n /** Your key (`sb_live_…`). Defaults to `process.env.SONGBRAIN_API_KEY`. The example endpoints work without a key. */\n apiKey?: string;\n /** Defaults to `process.env.SONGBRAIN_BASE_URL` or https://api.songbrain.ai/v1. */\n baseUrl?: string;\n /** Timeout for each HTTP request in ms (uploads included). Default 60 000. */\n timeoutMs?: number;\n /** How often to retry 429 and 5xx responses, with backoff. Default 3. */\n maxRetries?: number;\n /** A custom fetch implementation. Defaults to the global fetch. */\n fetch?: typeof fetch;\n}\n\nexport interface AnalyzeOptions {\n /** A Blob/File, a Buffer or bytes, or a local file path. Pass this or `audioUrl`. */\n file?: FileInput;\n /** A public http(s) URL of the audio file. Pass this or `file`. */\n audioUrl?: string;\n title?: string;\n artist?: string;\n /** Receives `song.done` / `song.failed`. */\n webhookUrl?: string;\n /** Your own id, echoed back in responses and webhooks. */\n externalRef?: string;\n /** File name for an upload. Its extension must match the audio format. Taken from the path or File name, or guessed from the bytes. */\n filename?: string;\n /** Wait until the song is done and return the full document (default true). */\n wait?: boolean;\n /** Time between status checks while waiting. Default 5 000. */\n pollIntervalMs?: number;\n /** Total time to wait before throwing `WaitTimeout`. Default 300 000. */\n timeoutMs?: number;\n}\n\nexport interface WaitOptions {\n pollIntervalMs?: number;\n timeoutMs?: number;\n}\n\nexport interface GetSongOptions {\n /** \"summary\" drops word timings and beat arrays (about 3x smaller). */\n view?: \"full\" | \"summary\";\n /** Only these sections, e.g. [\"song_dna\", \"shot_plan\"]. */\n include?: SectionName[] | string;\n}\n\ntype Json = Record<string, unknown>;\n\nconst MAX_RETRY_WAIT_SEC = 60;\nconst MIME: Record<string, string> = {\n mp3: \"audio/mpeg\",\n wav: \"audio/wav\",\n flac: \"audio/flac\",\n ogg: \"audio/ogg\",\n m4a: \"audio/mp4\",\n aac: \"audio/aac\",\n aiff: \"audio/aiff\",\n aif: \"audio/aiff\",\n};\n\nfunction env(name: string): string | undefined {\n const p = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n return p?.env?.[name] || undefined;\n}\n\n/** Guess the audio format from the first bytes (the API checks the extension against the contents). */\nexport function sniffExtension(head: Uint8Array): string | null {\n const ascii = (from: number, to: number) => String.fromCharCode(...head.subarray(from, to));\n if (ascii(0, 3) === \"ID3\") return \"mp3\";\n if (ascii(0, 4) === \"RIFF\" && ascii(8, 12) === \"WAVE\") return \"wav\";\n if (ascii(0, 4) === \"fLaC\") return \"flac\";\n if (ascii(0, 4) === \"OggS\") return \"ogg\";\n if (ascii(0, 4) === \"FORM\" && [\"AIFF\", \"AIFC\"].includes(ascii(8, 12))) return \"aiff\";\n if (ascii(4, 8) === \"ftyp\") return \"m4a\";\n const b0 = head[0];\n const b1 = head[1];\n if (b0 === 0xff && b1 !== undefined && (b1 & 0xe0) === 0xe0) return (b1 & 0x06) === 0 ? \"aac\" : \"mp3\";\n return null;\n}\n\nfunction extOf(name: string): string {\n const base = name.split(/[\\\\/]/).pop() ?? \"\";\n const i = base.lastIndexOf(\".\");\n return i > 0 ? base.slice(i + 1).toLowerCase() : \"\";\n}\n\nfunction baseName(name: string): string {\n return name.split(/[\\\\/]/).pop() ?? name;\n}\n\nfunction retryAfterSeconds(value: string | null): number | null {\n if (!value) return null;\n const n = Number(value);\n if (Number.isFinite(n)) return Math.max(0, n);\n const when = Date.parse(value);\n return Number.isNaN(when) ? null : Math.max(0, (when - Date.now()) / 1000);\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * Client for the Songbrain API. Every method resolves to the JSON body of\n * the response.\n *\n * ```ts\n * const sb = new Songbrain(); // reads SONGBRAIN_API_KEY\n * const song = await sb.analyze({ file: \"song.mp3\" });\n * for (const s of song.shot_plan?.clip?.scenes ?? []) console.log(s.start_sec, s.prompt);\n * ```\n */\nexport class Songbrain {\n readonly apiKey: string | undefined;\n readonly baseUrl: string;\n readonly timeoutMs: number;\n readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n /** @internal Overridable in tests. */\n _sleep: (ms: number) => Promise<void> = sleep;\n\n constructor(options: SongbrainOptions = {}) {\n this.apiKey = options.apiKey !== undefined ? options.apiKey || undefined : env(\"SONGBRAIN_API_KEY\");\n this.baseUrl = (options.baseUrl ?? env(\"SONGBRAIN_BASE_URL\") ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.timeoutMs = options.timeoutMs ?? 60_000;\n this.maxRetries = Math.max(0, options.maxRetries ?? 3);\n const f = options.fetch ?? (globalThis.fetch as typeof fetch | undefined);\n if (!f) throw new Error(\"No global fetch found. Use Node 18+ or pass `fetch` in the options.\");\n this.fetchImpl = f;\n }\n\n // ── songs ────────────────────────────────────────────────────────────────\n\n /**\n * Analyse a song from a file or a public URL. Pass exactly one of `file` or `audioUrl`.\n * Typically 60-90 s. With `wait: true` (default) resolves to the full document;\n * with `wait: false` to the 202 body `{ id, status: \"processing\", eta_sec, billing }`.\n */\n analyze(options: AnalyzeOptions & { wait: false }): Promise<SongCreated>;\n analyze(options: AnalyzeOptions & { wait?: true }): Promise<Song>;\n analyze(options: AnalyzeOptions): Promise<Song | SongCreated>;\n async analyze(options: AnalyzeOptions): Promise<Song | SongCreated> {\n const { file, audioUrl } = options;\n if ((file === undefined) === (audioUrl === undefined)) {\n throw new TypeError(\"Pass exactly one of `file` or `audioUrl`.\");\n }\n const fields: Record<string, string> = {};\n if (options.title !== undefined) fields.title = options.title;\n if (options.artist !== undefined) fields.artist = options.artist;\n if (options.webhookUrl !== undefined) fields.webhook_url = options.webhookUrl;\n if (options.externalRef !== undefined) fields.external_ref = options.externalRef;\n\n let created: SongCreated;\n if (audioUrl !== undefined) {\n created = await this.request<SongCreated>(\"POST\", \"/songs\", { json: { audio_url: audioUrl, ...fields } });\n } else {\n const { blob, name } = await toUpload(file as FileInput, options.filename);\n created = await this.request<SongCreated>(\"POST\", \"/songs\", {\n form: () => {\n const form = new FormData();\n form.append(\"file\", blob, name);\n for (const [k, v] of Object.entries(fields)) form.append(k, v);\n return form;\n },\n });\n }\n if (options.wait === false) return created;\n return this.waitFor(created.id, {\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n });\n }\n\n /** Poll GET /songs/{id} until the song is done. Throws `AnalysisFailed` or `WaitTimeout`. */\n async waitFor(songId: string, options: WaitOptions = {}): Promise<Song> {\n const timeoutMs = options.timeoutMs ?? 300_000;\n const interval = Math.max(500, options.pollIntervalMs ?? 5_000);\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const doc = await this.getSong(songId);\n if (doc.status === \"done\") return doc;\n if (doc.status === \"failed\") {\n throw new AnalysisFailed(\n songId,\n doc.error?.code ?? \"analysis_failed\",\n doc.error?.message ?? \"The analysis failed. Paid credits were refunded automatically.\",\n doc,\n );\n }\n const remaining = deadline - Date.now();\n if (remaining <= 0) throw new WaitTimeout(songId, timeoutMs);\n await this._sleep(Math.min(interval, remaining));\n }\n }\n\n /** Status while processing, then the full document. */\n getSong(songId: string, options: GetSongOptions = {}): Promise<Song> {\n return this.request<Song>(\"GET\", `/songs/${encodeURIComponent(songId)}`, { query: docQuery(options) });\n }\n\n /** Only the story and the shot plan: `{ id, status, song, story, shot_plan }`. */\n shotPlan(songId: string): Promise<SongShotPlan> {\n return this.request<SongShotPlan>(\"GET\", `/songs/${encodeURIComponent(songId)}/shot-plan`);\n }\n\n /** Your songs, newest first. `limit` is 1-100 (default 20). */\n listSongs(options: { limit?: number } = {}): Promise<SongList> {\n return this.request<SongList>(\"GET\", \"/songs\", { query: { limit: String(options.limit ?? 20) } });\n }\n\n /** Delete a song's audio and analysis now. 409 `still_processing` while it runs. */\n deleteSong(songId: string): Promise<DeleteResult> {\n return this.request<DeleteResult>(\"DELETE\", `/songs/${encodeURIComponent(songId)}`);\n }\n\n // ── account ──────────────────────────────────────────────────────────────\n\n /** Free songs left this month, credit balance and price per song. */\n account(): Promise<Account> {\n return this.request<Account>(\"GET\", \"/account\");\n }\n\n /** Prices and limits. No key needed. */\n pricing(): Promise<Pricing> {\n return this.request<Pricing>(\"GET\", \"/pricing\");\n }\n\n // ── examples (no key) ────────────────────────────────────────────────────\n\n /** Example analyses of Songbrain's own songs. No key needed. */\n examples(): Promise<ExampleList> {\n return this.request<ExampleList>(\"GET\", \"/examples\");\n }\n\n /** A full example document, in the same format as `getSong`. No key needed. */\n example(exampleId: string, options: GetSongOptions = {}): Promise<Song> {\n return this.request<Song>(\"GET\", `/examples/${encodeURIComponent(exampleId)}`, { query: docQuery(options) });\n }\n\n /** An example story and shot plan, in the same format as `shotPlan`. No key needed. */\n exampleShotPlan(exampleId: string): Promise<SongShotPlan> {\n return this.request<SongShotPlan>(\"GET\", `/examples/${encodeURIComponent(exampleId)}/shot-plan`);\n }\n\n // ── internals ────────────────────────────────────────────────────────────\n\n private backoffMs(attempt: number): number {\n return Math.min(500 * 2 ** attempt, 8_000) + Math.random() * 250;\n }\n\n private async request<T>(\n method: \"GET\" | \"POST\" | \"DELETE\",\n path: string,\n opts: { query?: Record<string, string | undefined>; json?: Json; form?: () => FormData } = {},\n ): Promise<T> {\n const url = new URL(this.baseUrl + path);\n for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== undefined) url.searchParams.set(k, v);\n const idempotent = method !== \"POST\";\n\n for (let attempt = 0; ; attempt++) {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"User-Agent\": `songbrain-js/${VERSION}`,\n };\n if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;\n let body: BodyInit | undefined;\n if (opts.json) {\n headers[\"Content-Type\"] = \"application/json\";\n body = JSON.stringify(opts.json);\n } else if (opts.form) {\n body = opts.form(); // fresh FormData per attempt; fetch sets the boundary\n }\n\n let res: Response;\n try {\n res = await this.fetchImpl(url, { method, headers, body, signal: AbortSignal.timeout(this.timeoutMs) });\n } catch (err) {\n // Network errors and timeouts: only resend if nothing can have been created.\n if (idempotent && attempt < this.maxRetries) {\n await this._sleep(this.backoffMs(attempt));\n continue;\n }\n const message = err instanceof Error ? err.message : String(err);\n throw new SongbrainError(0, \"connection_error\", message);\n }\n\n if (res.ok) {\n const text = await res.text();\n if (!text) return {} as T;\n try {\n return JSON.parse(text) as T;\n } catch {\n throw new SongbrainError(res.status, \"invalid_response\", \"Response is not JSON.\");\n }\n }\n\n const error = await errorFrom(res);\n // 429 never started any work. For POST only retry gateway errors (502/503).\n const retryable =\n res.status === 429 || (res.status >= 500 && (idempotent || res.status === 502 || res.status === 503));\n if (retryable && attempt < this.maxRetries) {\n const after = error instanceof RateLimited ? error.retryAfter : null;\n const delayMs = after !== null ? after * 1000 : this.backoffMs(attempt);\n if (delayMs <= MAX_RETRY_WAIT_SEC * 1000) {\n await this._sleep(delayMs);\n continue;\n }\n }\n throw error;\n }\n }\n}\n\nfunction docQuery(options: GetSongOptions): Record<string, string | undefined> {\n const include = Array.isArray(options.include) ? options.include.join(\",\") : options.include;\n return { view: options.view, include };\n}\n\nasync function errorFrom(res: Response): Promise<SongbrainError> {\n const text = await res.text().catch(() => \"\");\n let body: unknown;\n let code = `http_${res.status}`;\n let message = text.trim().slice(0, 500) || res.statusText || `HTTP ${res.status}`;\n try {\n body = JSON.parse(text);\n } catch {\n body = undefined;\n }\n if (body && typeof body === \"object\") {\n const err = (body as { error?: unknown }).error;\n if (err && typeof err === \"object\") {\n const e = err as { code?: unknown; message?: unknown };\n if (e.code) code = String(e.code);\n if (e.message) message = String(e.message);\n } else if (\"detail\" in (body as object)) {\n code = \"validation_error\";\n message = JSON.stringify((body as { detail: unknown }).detail);\n }\n }\n switch (res.status) {\n case 401:\n return new AuthenticationError(401, code, message, body);\n case 402:\n return new InsufficientCredits(402, code, message, body);\n case 404:\n return new NotFound(404, code, message, body);\n case 429:\n return new RateLimited(429, code, message, body, retryAfterSeconds(res.headers.get(\"Retry-After\")));\n default:\n return new SongbrainError(res.status, code, message, body);\n }\n}\n\nasync function toUpload(file: FileInput, filename?: string): Promise<{ blob: Blob; name: string }> {\n let blob: Blob;\n let sourceName: string | undefined;\n if (typeof file === \"string\") {\n const { readFile } = await import(\"node:fs/promises\");\n const data = await readFile(file);\n blob = new Blob([data as BlobPart]);\n sourceName = baseName(file);\n } else if (file instanceof ArrayBuffer) {\n blob = new Blob([file]);\n } else if (file instanceof Uint8Array) {\n blob = new Blob([file as BlobPart]);\n } else if (typeof Blob !== \"undefined\" && file instanceof Blob) {\n blob = file;\n const n = (file as Blob & { name?: unknown }).name;\n if (typeof n === \"string\" && n) sourceName = baseName(n);\n } else {\n throw new TypeError(\"`file` must be a Blob, a Buffer/Uint8Array, an ArrayBuffer or a file path.\");\n }\n\n let name = filename ?? sourceName;\n if (!name || !extOf(name)) {\n const head = new Uint8Array(await blob.slice(0, 16).arrayBuffer());\n const ext = sniffExtension(head);\n if (!ext) {\n throw new TypeError('Could not tell the audio format. Pass `filename` with an extension, e.g. filename: \"song.mp3\".');\n }\n name = `${(name ?? \"song\").replace(/\\.$/, \"\")}.${ext}`;\n }\n const type = MIME[extOf(name)];\n if (type && !blob.type) blob = new Blob([blob], { type });\n return { blob, name };\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport type { WebhookEvent } from \"./types.js\";\n\n/** The header Songbrain signs webhooks with. */\nexport const SIGNATURE_HEADER = \"Songbrain-Signature\";\n\ntype RawBody = string | Uint8Array | ArrayBuffer;\n\nfunction toBuffer(body: RawBody): Buffer {\n if (typeof body === \"string\") return Buffer.from(body, \"utf8\");\n if (body instanceof ArrayBuffer) return Buffer.from(new Uint8Array(body));\n return Buffer.from(body.buffer, body.byteOffset, body.byteLength);\n}\n\nfunction parseHeader(header: string): { t: number | null; v1: string[] } {\n let t: number | null = null;\n const v1: string[] = [];\n for (const part of header.split(\",\")) {\n const i = part.indexOf(\"=\");\n if (i < 0) continue;\n const key = part.slice(0, i).trim();\n const value = part.slice(i + 1).trim();\n if (key === \"t\") {\n if (!/^\\d+$/.test(value)) return { t: null, v1: [] };\n t = Number(value);\n } else if (key === \"v1\" && value) {\n v1.push(value.toLowerCase());\n }\n }\n return { t, v1 };\n}\n\nfunction mac(body: Buffer, t: number, secret: string): string {\n return createHmac(\"sha256\", secret).update(`${t}.`).update(body).digest(\"hex\");\n}\n\n/**\n * Check a `Songbrain-Signature: t=<unix>,v1=<hex>` header, where v1 is the\n * HMAC-SHA256 of `\"<t>.<raw body>\"` with your key's webhook secret.\n *\n * Pass the raw request body exactly as received, before any JSON parsing.\n * Returns false (never throws) for a missing, malformed, wrong or old signature.\n *\n * @param toleranceSec Maximum age in seconds (default 300). 0 disables the check.\n */\nexport function verifyWebhook(\n rawBody: RawBody,\n header: string | null | undefined,\n secret: string,\n toleranceSec = 300,\n): boolean {\n if (!header || !secret) return false;\n const { t, v1 } = parseHeader(header);\n if (t === null || v1.length === 0) return false;\n if (toleranceSec > 0 && Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n const expected = Buffer.from(mac(toBuffer(rawBody), t, secret), \"utf8\");\n return v1.some((sig) => {\n const given = Buffer.from(sig, \"utf8\");\n return given.length === expected.length && timingSafeEqual(given, expected);\n });\n}\n\n/**\n * Verify the signature and return the parsed event:\n * `{ type: \"song.done\" | \"song.failed\" | \"account.low_balance\", created, data }`.\n * Throws if the signature is not valid.\n */\nexport function constructWebhookEvent<T = Record<string, unknown>>(\n rawBody: RawBody,\n header: string | null | undefined,\n secret: string,\n toleranceSec = 300,\n): WebhookEvent<T> {\n if (!verifyWebhook(rawBody, header, secret, toleranceSec)) {\n throw new Error(\"Invalid Songbrain-Signature\");\n }\n return JSON.parse(toBuffer(rawBody).toString(\"utf8\")) as WebhookEvent<T>;\n}\n\n/** Build a signature header value. Useful to test your webhook handler. */\nexport function signWebhook(rawBody: RawBody, secret: string, timestampSec = Math.floor(Date.now() / 1000)): string {\n return `t=${timestampSec},v1=${mac(toBuffer(rawBody), timestampSec, secret)}`;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/version.ts","../src/client.ts","../src/webhooks.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Every API error has the shape `{ error: { code, message, request_id } }`.\n * The client turns it into a `SongbrainError` (or a subclass) with `status`,\n * `code`, `message` and `requestId`. The message ends with\n * ` (request_id: req_…)` when there is one; quote it when you contact support.\n */\nexport class SongbrainError extends Error {\n /** HTTP status, or 0 when the error did not come from an HTTP response. */\n readonly status: number;\n /** Machine-readable code, e.g. \"invalid_url\" or \"rate_limited\". */\n readonly code: string;\n /** The parsed response body, when there was one. */\n readonly body: unknown;\n /** The `Songbrain-Request-Id` of the failed request (`req_…`), or null. */\n readonly requestId: string | null;\n\n constructor(status: number, code: string, message: string, body?: unknown, requestId: string | null = null) {\n super(requestId ? `${message} (request_id: ${requestId})` : message);\n this.name = new.target.name;\n this.status = status;\n this.code = code;\n this.body = body;\n this.requestId = requestId;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** 401: the API key is missing, invalid or revoked. */\nexport class AuthenticationError extends SongbrainError {}\n\n/** 402: the free songs for this month are used up and the balance is below one song. */\nexport class InsufficientCredits extends SongbrainError {}\n\n/** 404: unknown id, or the song belongs to another account. */\nexport class NotFound extends SongbrainError {}\n\n/** 429: too many requests, too many songs in flight, daily cap or busy pipeline. */\nexport class RateLimited extends SongbrainError {\n /** Seconds to wait, from the Retry-After header (null if absent). */\n readonly retryAfter: number | null;\n\n constructor(\n status: number,\n code: string,\n message: string,\n body?: unknown,\n retryAfter: number | null = null,\n requestId: string | null = null,\n ) {\n super(status, code, message, body, requestId);\n this.retryAfter = retryAfter;\n }\n}\n\n/** The song was accepted but its analysis failed. Paid credits are refunded automatically. */\nexport class AnalysisFailed extends SongbrainError {\n readonly songId: string;\n\n constructor(songId: string, code: string, message: string, body?: unknown) {\n super(0, code, message, body);\n this.songId = songId;\n }\n}\n\n/** `analyze({ wait: true })` gave up before the song was done. It keeps processing on the server. */\nexport class WaitTimeout extends SongbrainError {\n readonly songId: string;\n\n constructor(songId: string, timeoutMs: number) {\n super(\n 0,\n \"wait_timeout\",\n `Song ${songId} was not done after ${Math.round(timeoutMs / 1000)} s. It is still processing; fetch it later with getSong(\"${songId}\").`,\n );\n this.songId = songId;\n }\n}\n","export const VERSION = \"0.2.0\";\n","import {\n AnalysisFailed,\n AuthenticationError,\n InsufficientCredits,\n NotFound,\n RateLimited,\n SongbrainError,\n WaitTimeout,\n} from \"./errors.js\";\nimport type {\n Account,\n DeleteResult,\n ExampleList,\n Pricing,\n RateLimitInfo,\n SectionName,\n Song,\n SongCreated,\n SongList,\n SongListItem,\n SongShotPlan,\n WebhookDeliveryList,\n WebhookTestResult,\n} from \"./types.js\";\nimport { VERSION } from \"./version.js\";\n\nexport const DEFAULT_BASE_URL = \"https://api.songbrain.ai/v1\";\nconst REQUEST_ID_HEADER = \"Songbrain-Request-Id\";\nconst IDEMPOTENCY_HEADER = \"Idempotency-Key\";\n\n/** A Blob/File, a Buffer or bytes, or a local file path (Node). */\nexport type FileInput = Blob | Uint8Array | ArrayBuffer | string;\n\nexport interface SongbrainOptions {\n /** Your key (`sb_live_…`). Defaults to `process.env.SONGBRAIN_API_KEY`. The example endpoints work without a key. */\n apiKey?: string;\n /** Defaults to `process.env.SONGBRAIN_BASE_URL` or https://api.songbrain.ai/v1. */\n baseUrl?: string;\n /** Timeout for each HTTP request in ms (uploads included). Default 60 000. */\n timeoutMs?: number;\n /**\n * How often to retry network errors, 429 and 5xx responses, with backoff. Default 3.\n * Song creation is retried too, because every create sends an `Idempotency-Key`.\n */\n maxRetries?: number;\n /** A custom fetch implementation. Defaults to the global fetch. */\n fetch?: typeof fetch;\n}\n\nexport interface AnalyzeOptions {\n /** A Blob/File, a Buffer or bytes, or a local file path. Pass this or `audioUrl`. */\n file?: FileInput;\n /** A public http(s) URL of the audio file. Pass this or `file`. */\n audioUrl?: string;\n title?: string;\n artist?: string;\n /** Receives `song.done` / `song.failed`. */\n webhookUrl?: string;\n /** Your own id, echoed back in responses and webhooks. */\n externalRef?: string;\n /** File name for an upload. Its extension must match the audio format. Taken from the path or File name, or guessed from the bytes. */\n filename?: string;\n /**\n * Test mode: free, never charged, no audio needed. The song is done right away and\n * returns the Sugar Rush example analysis with `livemode: false`. `song.done` is\n * still sent to `webhookUrl`. Made for CI and integration tests.\n */\n test?: boolean;\n /**\n * Sent as `Idempotency-Key`. By default a random UUID per call, reused across the\n * client's own retries, so a retried create never makes a second song. Pass your\n * own (e.g. your job id) to make retries across processes safe too (24 h window).\n */\n idempotencyKey?: string;\n /** Wait until the song is done and return the full document (default true). */\n wait?: boolean;\n /** Time between status checks while waiting. Default 5 000. */\n pollIntervalMs?: number;\n /** Total time to wait before throwing `WaitTimeout`. Default 300 000. */\n timeoutMs?: number;\n}\n\nexport interface WaitOptions {\n pollIntervalMs?: number;\n timeoutMs?: number;\n}\n\nexport interface ListSongsOptions {\n /** Page size, 1-100. Default 20. */\n limit?: number;\n /** A song id, usually the previous page's `next_cursor`. Returns the songs after it. */\n startingAfter?: string;\n}\n\nexport interface IterSongsOptions {\n /** Songs per request, 1-100. Default 100. */\n pageSize?: number;\n /** Start after this song id. */\n startingAfter?: string;\n}\n\nexport interface GetSongOptions {\n /** \"summary\" drops word timings and beat arrays (about 3x smaller). */\n view?: \"full\" | \"summary\";\n /** Only these sections, e.g. [\"song_dna\", \"shot_plan\"]. */\n include?: SectionName[] | string;\n}\n\ntype Json = Record<string, unknown>;\n\nconst MAX_RETRY_WAIT_SEC = 60;\nconst MIME: Record<string, string> = {\n mp3: \"audio/mpeg\",\n wav: \"audio/wav\",\n flac: \"audio/flac\",\n ogg: \"audio/ogg\",\n m4a: \"audio/mp4\",\n aac: \"audio/aac\",\n aiff: \"audio/aiff\",\n aif: \"audio/aiff\",\n};\n\nfunction env(name: string): string | undefined {\n const p = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n return p?.env?.[name] || undefined;\n}\n\n/** Guess the audio format from the first bytes (the API checks the extension against the contents). */\nexport function sniffExtension(head: Uint8Array): string | null {\n const ascii = (from: number, to: number) => String.fromCharCode(...head.subarray(from, to));\n if (ascii(0, 3) === \"ID3\") return \"mp3\";\n if (ascii(0, 4) === \"RIFF\" && ascii(8, 12) === \"WAVE\") return \"wav\";\n if (ascii(0, 4) === \"fLaC\") return \"flac\";\n if (ascii(0, 4) === \"OggS\") return \"ogg\";\n if (ascii(0, 4) === \"FORM\" && [\"AIFF\", \"AIFC\"].includes(ascii(8, 12))) return \"aiff\";\n if (ascii(4, 8) === \"ftyp\") return \"m4a\";\n const b0 = head[0];\n const b1 = head[1];\n if (b0 === 0xff && b1 !== undefined && (b1 & 0xe0) === 0xe0) return (b1 & 0x06) === 0 ? \"aac\" : \"mp3\";\n return null;\n}\n\nfunction extOf(name: string): string {\n const base = name.split(/[\\\\/]/).pop() ?? \"\";\n const i = base.lastIndexOf(\".\");\n return i > 0 ? base.slice(i + 1).toLowerCase() : \"\";\n}\n\nfunction baseName(name: string): string {\n return name.split(/[\\\\/]/).pop() ?? name;\n}\n\nfunction retryAfterSeconds(value: string | null): number | null {\n if (!value) return null;\n const n = Number(value);\n if (Number.isFinite(n)) return Math.max(0, n);\n const when = Date.parse(value);\n return Number.isNaN(when) ? null : Math.max(0, (when - Date.now()) / 1000);\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nfunction intHeader(value: string | null): number | null {\n if (value === null || value === \"\") return null;\n const n = Number(value);\n return Number.isFinite(n) ? Math.trunc(n) : null;\n}\n\nfunction rateLimitFrom(headers: Headers): RateLimitInfo | null {\n const limit = intHeader(headers.get(\"X-RateLimit-Limit\"));\n if (limit === null) return null;\n return {\n limit,\n remaining: intHeader(headers.get(\"X-RateLimit-Remaining\")),\n reset: intHeader(headers.get(\"X-RateLimit-Reset\")),\n };\n}\n\n/** A random UUID v4: Web Crypto where available (browsers, Node 19+), node:crypto on Node 18. */\nasync function newIdempotencyKey(): Promise<string> {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (typeof c?.randomUUID === \"function\") return c.randomUUID();\n try {\n const { randomUUID } = await import(\"node:crypto\");\n return randomUUID();\n } catch {\n const hex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16));\n hex[12] = \"4\";\n hex[16] = ((parseInt(hex[16] as string, 16) & 0x3) | 0x8).toString(16);\n const h = hex.join(\"\");\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n }\n}\n\n/**\n * Client for the Songbrain API. Every method resolves to the JSON body of\n * the response.\n *\n * ```ts\n * const sb = new Songbrain(); // reads SONGBRAIN_API_KEY\n * const song = await sb.analyze({ file: \"song.mp3\" });\n * for (const s of song.shot_plan?.clip?.scenes ?? []) console.log(s.start_sec, s.prompt);\n * ```\n */\nexport class Songbrain {\n readonly apiKey: string | undefined;\n readonly baseUrl: string;\n readonly timeoutMs: number;\n readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n /**\n * `{ limit, remaining, reset }` from the `X-RateLimit-*` headers of the last response\n * that had them (null before that). `reset` is seconds until the window has room.\n */\n lastRateLimit: RateLimitInfo | null = null;\n /** The `Songbrain-Request-Id` of the last response (null before the first one). */\n lastRequestId: string | null = null;\n /** @internal Overridable in tests. */\n _sleep: (ms: number) => Promise<void> = sleep;\n\n constructor(options: SongbrainOptions = {}) {\n this.apiKey = options.apiKey !== undefined ? options.apiKey || undefined : env(\"SONGBRAIN_API_KEY\");\n this.baseUrl = (options.baseUrl ?? env(\"SONGBRAIN_BASE_URL\") ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.timeoutMs = options.timeoutMs ?? 60_000;\n this.maxRetries = Math.max(0, options.maxRetries ?? 3);\n const f = options.fetch ?? (globalThis.fetch as typeof fetch | undefined);\n if (!f) throw new Error(\"No global fetch found. Use Node 18+ or pass `fetch` in the options.\");\n this.fetchImpl = f;\n }\n\n // ── songs ────────────────────────────────────────────────────────────────\n\n /**\n * Analyse a song from a file or a public URL. Pass exactly one of `file` or `audioUrl`\n * (or neither with `test: true`).\n * Typically 60-90 s. With `wait: true` (default) resolves to the full document;\n * with `wait: false` to the 202 body `{ id, status: \"processing\", eta_sec, billing }`.\n */\n analyze(options: AnalyzeOptions & { wait: false }): Promise<SongCreated>;\n analyze(options: AnalyzeOptions & { wait?: true }): Promise<Song>;\n analyze(options: AnalyzeOptions): Promise<Song | SongCreated>;\n async analyze(options: AnalyzeOptions): Promise<Song | SongCreated> {\n const { file, audioUrl, test } = options;\n if (file !== undefined && audioUrl !== undefined) {\n throw new TypeError(\"Pass exactly one of `file` or `audioUrl`.\");\n }\n if (file === undefined && audioUrl === undefined && !test) {\n throw new TypeError(\"Pass exactly one of `file` or `audioUrl` (or `test: true`).\");\n }\n const fields: Record<string, string> = {};\n if (options.title !== undefined) fields.title = options.title;\n if (options.artist !== undefined) fields.artist = options.artist;\n if (options.webhookUrl !== undefined) fields.webhook_url = options.webhookUrl;\n if (options.externalRef !== undefined) fields.external_ref = options.externalRef;\n const headers = { [IDEMPOTENCY_HEADER]: options.idempotencyKey || (await newIdempotencyKey()) };\n\n let created: SongCreated;\n if (file === undefined) {\n const json: Json = audioUrl !== undefined ? { audio_url: audioUrl, ...fields } : { ...fields };\n if (test) json.test = true;\n created = await this.request<SongCreated>(\"POST\", \"/songs\", { json, headers });\n } else {\n if (test) fields.test = \"true\";\n const { blob, name } = await toUpload(file, options.filename);\n created = await this.request<SongCreated>(\"POST\", \"/songs\", {\n headers,\n form: () => {\n const form = new FormData();\n form.append(\"file\", blob, name);\n for (const [k, v] of Object.entries(fields)) form.append(k, v);\n return form;\n },\n });\n }\n if (options.wait === false) return created;\n return this.waitFor(created.id, {\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n });\n }\n\n /** Poll GET /songs/{id} until the song is done. Throws `AnalysisFailed` or `WaitTimeout`. */\n async waitFor(songId: string, options: WaitOptions = {}): Promise<Song> {\n const timeoutMs = options.timeoutMs ?? 300_000;\n const interval = Math.max(500, options.pollIntervalMs ?? 5_000);\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const doc = await this.getSong(songId);\n if (doc.status === \"done\") return doc;\n if (doc.status === \"failed\") {\n throw new AnalysisFailed(\n songId,\n doc.error?.code ?? \"analysis_failed\",\n doc.error?.message ?? \"The analysis failed. Paid credits were refunded automatically.\",\n doc,\n );\n }\n const remaining = deadline - Date.now();\n if (remaining <= 0) throw new WaitTimeout(songId, timeoutMs);\n await this._sleep(Math.min(interval, remaining));\n }\n }\n\n /** Status while processing, then the full document. */\n getSong(songId: string, options: GetSongOptions = {}): Promise<Song> {\n return this.request<Song>(\"GET\", `/songs/${encodeURIComponent(songId)}`, { query: docQuery(options) });\n }\n\n /** Only the story and the shot plan: `{ id, status, song, story, shot_plan }`. */\n shotPlan(songId: string): Promise<SongShotPlan> {\n return this.request<SongShotPlan>(\"GET\", `/songs/${encodeURIComponent(songId)}/shot-plan`);\n }\n\n /** One page of your songs, newest first: `{ object: \"list\", data, has_more, next_cursor }`. */\n listSongs(options: ListSongsOptions = {}): Promise<SongList> {\n return this.request<SongList>(\"GET\", \"/songs\", {\n query: { limit: String(options.limit ?? 20), starting_after: options.startingAfter },\n });\n }\n\n /**\n * All your songs, newest first, fetching pages as needed.\n *\n * ```ts\n * for await (const item of sb.iterSongs()) console.log(item.id, item.status);\n * ```\n */\n async *iterSongs(options: IterSongsOptions = {}): AsyncGenerator<SongListItem, void, undefined> {\n let cursor = options.startingAfter;\n for (;;) {\n const page = await this.listSongs({ limit: options.pageSize ?? 100, startingAfter: cursor });\n const data = page.data ?? [];\n yield* data;\n cursor = page.next_cursor ?? data[data.length - 1]?.id;\n if (!page.has_more || !cursor) return;\n }\n }\n\n /** Delete a song's audio and analysis now. 409 `still_processing` while it runs. */\n deleteSong(songId: string): Promise<DeleteResult> {\n return this.request<DeleteResult>(\"DELETE\", `/songs/${encodeURIComponent(songId)}`);\n }\n\n // ── account ──────────────────────────────────────────────────────────────\n\n /** Free songs left this month, credit balance and price per song. */\n account(): Promise<Account> {\n return this.request<Account>(\"GET\", \"/account\");\n }\n\n /** Prices and limits. No key needed. */\n pricing(): Promise<Pricing> {\n return this.request<Pricing>(\"GET\", \"/pricing\");\n }\n\n // ── webhooks ─────────────────────────────────────────────────────────────\n\n /**\n * Send a signed `ping` event to `url` right now:\n * `{ delivered, status_code, latency_ms, event_id }`. Checks your receiver before real songs.\n */\n testWebhook(url: string): Promise<WebhookTestResult> {\n return this.request<WebhookTestResult>(\"POST\", \"/webhooks/test\", { json: { url } });\n }\n\n /** The last webhook delivery attempts for your account, newest first. */\n webhookDeliveries(options: { limit?: number } = {}): Promise<WebhookDeliveryList> {\n return this.request<WebhookDeliveryList>(\"GET\", \"/webhooks/deliveries\", {\n query: { limit: String(options.limit ?? 20) },\n });\n }\n\n // ── examples (no key) ────────────────────────────────────────────────────\n\n /** Example analyses of Songbrain's own songs. No key needed. */\n examples(): Promise<ExampleList> {\n return this.request<ExampleList>(\"GET\", \"/examples\");\n }\n\n /** A full example document, in the same format as `getSong`. No key needed. */\n example(exampleId: string, options: GetSongOptions = {}): Promise<Song> {\n return this.request<Song>(\"GET\", `/examples/${encodeURIComponent(exampleId)}`, { query: docQuery(options) });\n }\n\n /** An example story and shot plan, in the same format as `shotPlan`. No key needed. */\n exampleShotPlan(exampleId: string): Promise<SongShotPlan> {\n return this.request<SongShotPlan>(\"GET\", `/examples/${encodeURIComponent(exampleId)}/shot-plan`);\n }\n\n // ── internals ────────────────────────────────────────────────────────────\n\n private backoffMs(attempt: number): number {\n return Math.min(500 * 2 ** attempt, 8_000) + Math.random() * 250;\n }\n\n private async request<T>(\n method: \"GET\" | \"POST\" | \"DELETE\",\n path: string,\n opts: {\n query?: Record<string, string | undefined>;\n json?: Json;\n form?: () => FormData;\n headers?: Record<string, string>;\n } = {},\n ): Promise<T> {\n const url = new URL(this.baseUrl + path);\n for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== undefined) url.searchParams.set(k, v);\n const hasKey = Boolean(opts.headers?.[IDEMPOTENCY_HEADER]);\n // A POST with an Idempotency-Key is safe to resend: the API returns the first answer again.\n const idempotent = method !== \"POST\" || hasKey;\n\n for (let attempt = 0; ; attempt++) {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"User-Agent\": `songbrain-js/${VERSION}`,\n };\n if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;\n Object.assign(headers, opts.headers);\n let body: BodyInit | undefined;\n if (opts.json) {\n headers[\"Content-Type\"] = \"application/json\";\n body = JSON.stringify(opts.json);\n } else if (opts.form) {\n body = opts.form(); // fresh FormData per attempt; fetch sets the boundary\n }\n\n let res: Response;\n try {\n res = await this.fetchImpl(url, { method, headers, body, signal: AbortSignal.timeout(this.timeoutMs) });\n } catch (err) {\n // Network errors and timeouts: only resend if nothing can have been created\n // or the request carries an Idempotency-Key.\n if (idempotent && attempt < this.maxRetries) {\n await this._sleep(this.backoffMs(attempt));\n continue;\n }\n const message = err instanceof Error ? err.message : String(err);\n throw new SongbrainError(0, \"connection_error\", message);\n }\n\n this.lastRequestId = res.headers.get(REQUEST_ID_HEADER) || null;\n const rateLimit = rateLimitFrom(res.headers);\n if (rateLimit) this.lastRateLimit = rateLimit;\n\n if (res.ok) {\n const text = await res.text();\n if (!text) return {} as T;\n try {\n return JSON.parse(text) as T;\n } catch {\n throw new SongbrainError(res.status, \"invalid_response\", \"Response is not JSON.\");\n }\n }\n\n const error = await errorFrom(res);\n // 429 never started any work. For a POST without Idempotency-Key only retry\n // gateway errors (502/503).\n const inProgress = hasKey && res.status === 409 && error.code === \"idempotency_in_progress\";\n const retryable =\n res.status === 429 ||\n inProgress ||\n (res.status >= 500 && (idempotent || res.status === 502 || res.status === 503));\n if (retryable && attempt < this.maxRetries) {\n const after = error instanceof RateLimited ? error.retryAfter : null;\n let delayMs = after !== null ? after * 1000 : this.backoffMs(attempt);\n if (inProgress) delayMs = Math.max(1000, delayMs);\n if (delayMs <= MAX_RETRY_WAIT_SEC * 1000) {\n await this._sleep(delayMs);\n continue;\n }\n }\n throw error;\n }\n }\n}\n\nfunction docQuery(options: GetSongOptions): Record<string, string | undefined> {\n const include = Array.isArray(options.include) ? options.include.join(\",\") : options.include;\n return { view: options.view, include };\n}\n\nasync function errorFrom(res: Response): Promise<SongbrainError> {\n const text = await res.text().catch(() => \"\");\n let body: unknown;\n let code = `http_${res.status}`;\n let message = text.trim().slice(0, 500) || res.statusText || `HTTP ${res.status}`;\n let requestId = res.headers.get(REQUEST_ID_HEADER) || null;\n try {\n body = JSON.parse(text);\n } catch {\n body = undefined;\n }\n if (body && typeof body === \"object\") {\n const err = (body as { error?: unknown }).error;\n if (err && typeof err === \"object\") {\n const e = err as { code?: unknown; message?: unknown; request_id?: unknown };\n if (e.code) code = String(e.code);\n if (e.message) message = String(e.message);\n if (e.request_id) requestId = String(e.request_id);\n } else if (\"detail\" in (body as object)) {\n // request validation, older API versions\n code = \"validation_error\";\n message = JSON.stringify((body as { detail: unknown }).detail);\n }\n }\n switch (res.status) {\n case 401:\n return new AuthenticationError(401, code, message, body, requestId);\n case 402:\n return new InsufficientCredits(402, code, message, body, requestId);\n case 404:\n return new NotFound(404, code, message, body, requestId);\n case 429:\n return new RateLimited(429, code, message, body, retryAfterSeconds(res.headers.get(\"Retry-After\")), requestId);\n default:\n return new SongbrainError(res.status, code, message, body, requestId);\n }\n}\n\nasync function toUpload(file: FileInput, filename?: string): Promise<{ blob: Blob; name: string }> {\n let blob: Blob;\n let sourceName: string | undefined;\n if (typeof file === \"string\") {\n const { readFile } = await import(\"node:fs/promises\");\n const data = await readFile(file);\n blob = new Blob([data as BlobPart]);\n sourceName = baseName(file);\n } else if (file instanceof ArrayBuffer) {\n blob = new Blob([file]);\n } else if (file instanceof Uint8Array) {\n blob = new Blob([file as BlobPart]);\n } else if (typeof Blob !== \"undefined\" && file instanceof Blob) {\n blob = file;\n const n = (file as Blob & { name?: unknown }).name;\n if (typeof n === \"string\" && n) sourceName = baseName(n);\n } else {\n throw new TypeError(\"`file` must be a Blob, a Buffer/Uint8Array, an ArrayBuffer or a file path.\");\n }\n\n let name = filename ?? sourceName;\n if (!name || !extOf(name)) {\n const head = new Uint8Array(await blob.slice(0, 16).arrayBuffer());\n const ext = sniffExtension(head);\n if (!ext) {\n throw new TypeError('Could not tell the audio format. Pass `filename` with an extension, e.g. filename: \"song.mp3\".');\n }\n name = `${(name ?? \"song\").replace(/\\.$/, \"\")}.${ext}`;\n }\n const type = MIME[extOf(name)];\n if (type && !blob.type) blob = new Blob([blob], { type });\n return { blob, name };\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport type { WebhookEvent } from \"./types.js\";\n\n/** The header Songbrain signs webhooks with. */\nexport const SIGNATURE_HEADER = \"Songbrain-Signature\";\n\ntype RawBody = string | Uint8Array | ArrayBuffer;\n\nfunction toBuffer(body: RawBody): Buffer {\n if (typeof body === \"string\") return Buffer.from(body, \"utf8\");\n if (body instanceof ArrayBuffer) return Buffer.from(new Uint8Array(body));\n return Buffer.from(body.buffer, body.byteOffset, body.byteLength);\n}\n\nfunction parseHeader(header: string): { t: number | null; v1: string[] } {\n let t: number | null = null;\n const v1: string[] = [];\n for (const part of header.split(\",\")) {\n const i = part.indexOf(\"=\");\n if (i < 0) continue;\n const key = part.slice(0, i).trim();\n const value = part.slice(i + 1).trim();\n if (key === \"t\") {\n if (!/^\\d+$/.test(value)) return { t: null, v1: [] };\n t = Number(value);\n } else if (key === \"v1\" && value) {\n v1.push(value.toLowerCase());\n }\n }\n return { t, v1 };\n}\n\nfunction mac(body: Buffer, t: number, secret: string): string {\n return createHmac(\"sha256\", secret).update(`${t}.`).update(body).digest(\"hex\");\n}\n\n/**\n * Check a `Songbrain-Signature: t=<unix>,v1=<hex>` header, where v1 is the\n * HMAC-SHA256 of `\"<t>.<raw body>\"` with your key's webhook secret.\n *\n * Pass the raw request body exactly as received, before any JSON parsing.\n * Returns false (never throws) for a missing, malformed, wrong or old signature.\n *\n * @param toleranceSec Maximum age in seconds (default 300). 0 disables the check.\n */\nexport function verifyWebhook(\n rawBody: RawBody,\n header: string | null | undefined,\n secret: string,\n toleranceSec = 300,\n): boolean {\n if (!header || !secret) return false;\n const { t, v1 } = parseHeader(header);\n if (t === null || v1.length === 0) return false;\n if (toleranceSec > 0 && Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n const expected = Buffer.from(mac(toBuffer(rawBody), t, secret), \"utf8\");\n return v1.some((sig) => {\n const given = Buffer.from(sig, \"utf8\");\n return given.length === expected.length && timingSafeEqual(given, expected);\n });\n}\n\n/**\n * Verify the signature and return the parsed event:\n * `{ id, type: \"song.done\" | \"song.failed\" | \"account.low_balance\" | \"ping\", created, livemode, data }`.\n * Throws if the signature is not valid.\n *\n * `id` (`evt_…`, also the `Songbrain-Event-Id` header) stays the same when\n * Songbrain retries an event (up to 10 attempts over about 3 days): store the\n * ids you have handled and skip repeats.\n */\nexport function constructWebhookEvent<T = Record<string, unknown>>(\n rawBody: RawBody,\n header: string | null | undefined,\n secret: string,\n toleranceSec = 300,\n): WebhookEvent<T> {\n if (!verifyWebhook(rawBody, header, secret, toleranceSec)) {\n throw new Error(\"Invalid Songbrain-Signature\");\n }\n return JSON.parse(toBuffer(rawBody).toString(\"utf8\")) as WebhookEvent<T>;\n}\n\n/** Build a signature header value. Useful to test your webhook handler. */\nexport function signWebhook(rawBody: RawBody, secret: string, timestampSec = Math.floor(Date.now() / 1000)): string {\n return `t=${timestampSec},v1=${mac(toBuffer(rawBody), timestampSec, secret)}`;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "songbrain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Official TypeScript/JavaScript client for the Songbrain API: song in, video plan out. Song DNA, beat grid, best moments, timed lyrics, story and a beat-synced shot plan.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Songbrain <support@songbrain.ai> (https://www.songbrain.ai)",
|