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/README.md
CHANGED
|
@@ -46,13 +46,16 @@ Every method resolves to the JSON body of the response. Field names are exactly
|
|
|
46
46
|
|
|
47
47
|
| Method | API call | Key |
|
|
48
48
|
|---|---|---|
|
|
49
|
-
| `analyze({ file?, audioUrl?, title?, artist?, webhookUrl?, externalRef?, filename?, wait = true, pollIntervalMs = 5000, timeoutMs = 300000 })` | `POST /songs`, then polls | yes |
|
|
49
|
+
| `analyze({ file?, audioUrl?, title?, artist?, webhookUrl?, externalRef?, filename?, test?, idempotencyKey?, wait = true, pollIntervalMs = 5000, timeoutMs = 300000 })` | `POST /songs`, then polls | yes |
|
|
50
50
|
| `waitFor(id, { pollIntervalMs?, timeoutMs? })` | polls `GET /songs/{id}` | yes |
|
|
51
51
|
| `getSong(id, { view?, include? })` | `GET /songs/{id}` | yes |
|
|
52
52
|
| `shotPlan(id)` | `GET /songs/{id}/shot-plan` | yes |
|
|
53
|
-
| `listSongs({ limit = 20 })` | `GET /songs` | yes |
|
|
53
|
+
| `listSongs({ limit = 20, startingAfter? })` | `GET /songs` (one page) | yes |
|
|
54
|
+
| `iterSongs({ pageSize = 100, startingAfter? })` | `GET /songs`, all pages (async iterator) | yes |
|
|
54
55
|
| `deleteSong(id)` | `DELETE /songs/{id}` | yes |
|
|
55
56
|
| `account()` | `GET /account` | yes |
|
|
57
|
+
| `testWebhook(url)` | `POST /webhooks/test` | yes |
|
|
58
|
+
| `webhookDeliveries({ limit = 20 })` | `GET /webhooks/deliveries` | yes |
|
|
56
59
|
| `pricing()` | `GET /pricing` | no |
|
|
57
60
|
| `examples()` | `GET /examples` | no |
|
|
58
61
|
| `example(id, { view?, include? })` | `GET /examples/{id}` | no |
|
|
@@ -68,11 +71,47 @@ new Songbrain({ apiKey: "sb_live_…", timeoutMs: 60_000, maxRetries: 3 });
|
|
|
68
71
|
|
|
69
72
|
Keep the key on your server. Never ship it in a browser or app bundle.
|
|
70
73
|
|
|
74
|
+
## Test mode
|
|
75
|
+
|
|
76
|
+
`analyze({ test: true })` is free, needs no audio and is done right away. It returns the Sugar Rush example analysis with your `title` and `externalRef`, `livemode: false` and `billing.type === "test"`. A `webhookUrl` still gets a signed `song.done`. Use it in CI:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const song = await sb.analyze({ test: true, title: "CI smoke test", externalRef: "build-123" });
|
|
80
|
+
assert(song.status === "done" && song.livemode === false);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Idempotency
|
|
84
|
+
|
|
85
|
+
Every `analyze()` sends an `Idempotency-Key` (a random UUID) and reuses it when the client retries. So network errors, 5xx and 429 are retried for uploads too, and a retry never creates or charges a second song. Pass your own key to make retries across processes safe (same key within 24 h = same song):
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await sb.analyze({ audioUrl: url, idempotencyKey: `order-${orderId}`, wait: false });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The same key with a different request throws a 409 `idempotency_key_reused`.
|
|
92
|
+
|
|
93
|
+
## Pagination
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
let page = await sb.listSongs({ limit: 50 }); // { data, has_more, next_cursor }
|
|
97
|
+
page = await sb.listSongs({ limit: 50, startingAfter: page.next_cursor ?? undefined });
|
|
98
|
+
|
|
99
|
+
for await (const item of sb.iterSongs()) console.log(item.id, item.status, item.livemode);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Rate limits and request ids
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
await sb.account();
|
|
106
|
+
console.log(sb.lastRateLimit); // { limit: 120, remaining: 119, reset: 0 }
|
|
107
|
+
console.log(sb.lastRequestId); // "req_…"
|
|
108
|
+
```
|
|
109
|
+
|
|
71
110
|
## Errors
|
|
72
111
|
|
|
73
|
-
API errors throw `SongbrainError` with `status`, `code` and `message
|
|
112
|
+
API errors throw `SongbrainError` with `status`, `code`, `message` and `requestId` (`req_…`; the message ends with `(request_id: req_…)`, quote it when you write to support). Subclasses: `AuthenticationError` (401), `InsufficientCredits` (402), `NotFound` (404), `RateLimited` (429, with `retryAfter` in seconds), `AnalysisFailed` (the song failed; credits are refunded) and `WaitTimeout` (the song keeps processing).
|
|
74
113
|
|
|
75
|
-
The client retries 429 and 5xx responses up to 3 times with backoff and honours `Retry-After` (up to 60 s).
|
|
114
|
+
The client retries network errors, 429 and 5xx responses up to 3 times with backoff and honours `Retry-After` (up to 60 s). Song creation is retried too, because it carries an `Idempotency-Key`.
|
|
76
115
|
|
|
77
116
|
```ts
|
|
78
117
|
import { Songbrain, InsufficientCredits, RateLimited } from "songbrain";
|
|
@@ -99,13 +138,20 @@ app.post("/songbrain", express.raw({ type: "application/json" }), (req, res) =>
|
|
|
99
138
|
return res.sendStatus(400);
|
|
100
139
|
}
|
|
101
140
|
const event = JSON.parse(req.body.toString("utf8"));
|
|
102
|
-
// event.type: "song.done" | "song.failed" | "account.low_balance"
|
|
141
|
+
// event.id: "evt_…" (same on every retry), event.type: "song.done" | "song.failed" | "account.low_balance" | "ping"
|
|
103
142
|
res.sendStatus(200);
|
|
104
143
|
});
|
|
105
144
|
```
|
|
106
145
|
|
|
107
146
|
`verifyWebhook(rawBody, header, secret, toleranceSec = 300)` returns a boolean. `constructWebhookEvent()` verifies and parses in one step.
|
|
108
147
|
|
|
148
|
+
Songbrain retries a failed delivery up to 10 times over about 3 days, with the same `event.id` every time. Store the ids you have handled and skip repeats. Check your receiver with a signed `ping`:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await sb.testWebhook("https://example.com/songbrain"); // { delivered, status_code, latency_ms, event_id }
|
|
152
|
+
await sb.webhookDeliveries({ limit: 20 }); // the last delivery attempts
|
|
153
|
+
```
|
|
154
|
+
|
|
109
155
|
## Pricing
|
|
110
156
|
|
|
111
157
|
5 free songs per account per month. Then 25 credits per song (about $0.50). No subscription. Everything is included: analysis, story and shot plan. Results are yours, also in a paid or white-label product. See [Terms §17](https://www.songbrain.ai/terms#api).
|
package/dist/index.cjs
CHANGED
|
@@ -10,12 +10,15 @@ var SongbrainError = class extends Error {
|
|
|
10
10
|
code;
|
|
11
11
|
/** The parsed response body, when there was one. */
|
|
12
12
|
body;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/** The `Songbrain-Request-Id` of the failed request (`req_…`), or null. */
|
|
14
|
+
requestId;
|
|
15
|
+
constructor(status, code, message, body, requestId = null) {
|
|
16
|
+
super(requestId ? `${message} (request_id: ${requestId})` : message);
|
|
15
17
|
this.name = new.target.name;
|
|
16
18
|
this.status = status;
|
|
17
19
|
this.code = code;
|
|
18
20
|
this.body = body;
|
|
21
|
+
this.requestId = requestId;
|
|
19
22
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
20
23
|
}
|
|
21
24
|
};
|
|
@@ -28,8 +31,8 @@ var NotFound = class extends SongbrainError {
|
|
|
28
31
|
var RateLimited = class extends SongbrainError {
|
|
29
32
|
/** Seconds to wait, from the Retry-After header (null if absent). */
|
|
30
33
|
retryAfter;
|
|
31
|
-
constructor(status, code, message, body, retryAfter = null) {
|
|
32
|
-
super(status, code, message, body);
|
|
34
|
+
constructor(status, code, message, body, retryAfter = null, requestId = null) {
|
|
35
|
+
super(status, code, message, body, requestId);
|
|
33
36
|
this.retryAfter = retryAfter;
|
|
34
37
|
}
|
|
35
38
|
};
|
|
@@ -53,10 +56,12 @@ var WaitTimeout = class extends SongbrainError {
|
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
// src/version.ts
|
|
56
|
-
var VERSION = "0.
|
|
59
|
+
var VERSION = "0.2.0";
|
|
57
60
|
|
|
58
61
|
// src/client.ts
|
|
59
62
|
var DEFAULT_BASE_URL = "https://api.songbrain.ai/v1";
|
|
63
|
+
var REQUEST_ID_HEADER = "Songbrain-Request-Id";
|
|
64
|
+
var IDEMPOTENCY_HEADER = "Idempotency-Key";
|
|
60
65
|
var MAX_RETRY_WAIT_SEC = 60;
|
|
61
66
|
var MIME = {
|
|
62
67
|
mp3: "audio/mpeg",
|
|
@@ -101,12 +106,47 @@ function retryAfterSeconds(value) {
|
|
|
101
106
|
return Number.isNaN(when) ? null : Math.max(0, (when - Date.now()) / 1e3);
|
|
102
107
|
}
|
|
103
108
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
function intHeader(value) {
|
|
110
|
+
if (value === null || value === "") return null;
|
|
111
|
+
const n = Number(value);
|
|
112
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
113
|
+
}
|
|
114
|
+
function rateLimitFrom(headers) {
|
|
115
|
+
const limit = intHeader(headers.get("X-RateLimit-Limit"));
|
|
116
|
+
if (limit === null) return null;
|
|
117
|
+
return {
|
|
118
|
+
limit,
|
|
119
|
+
remaining: intHeader(headers.get("X-RateLimit-Remaining")),
|
|
120
|
+
reset: intHeader(headers.get("X-RateLimit-Reset"))
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function newIdempotencyKey() {
|
|
124
|
+
const c = globalThis.crypto;
|
|
125
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
126
|
+
try {
|
|
127
|
+
const { randomUUID } = await import('crypto');
|
|
128
|
+
return randomUUID();
|
|
129
|
+
} catch {
|
|
130
|
+
const hex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16));
|
|
131
|
+
hex[12] = "4";
|
|
132
|
+
hex[16] = (parseInt(hex[16], 16) & 3 | 8).toString(16);
|
|
133
|
+
const h = hex.join("");
|
|
134
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
104
137
|
var Songbrain = class {
|
|
105
138
|
apiKey;
|
|
106
139
|
baseUrl;
|
|
107
140
|
timeoutMs;
|
|
108
141
|
maxRetries;
|
|
109
142
|
fetchImpl;
|
|
143
|
+
/**
|
|
144
|
+
* `{ limit, remaining, reset }` from the `X-RateLimit-*` headers of the last response
|
|
145
|
+
* that had them (null before that). `reset` is seconds until the window has room.
|
|
146
|
+
*/
|
|
147
|
+
lastRateLimit = null;
|
|
148
|
+
/** The `Songbrain-Request-Id` of the last response (null before the first one). */
|
|
149
|
+
lastRequestId = null;
|
|
110
150
|
/** @internal Overridable in tests. */
|
|
111
151
|
_sleep = sleep;
|
|
112
152
|
constructor(options = {}) {
|
|
@@ -119,21 +159,29 @@ var Songbrain = class {
|
|
|
119
159
|
this.fetchImpl = f;
|
|
120
160
|
}
|
|
121
161
|
async analyze(options) {
|
|
122
|
-
const { file, audioUrl } = options;
|
|
123
|
-
if (file
|
|
162
|
+
const { file, audioUrl, test } = options;
|
|
163
|
+
if (file !== void 0 && audioUrl !== void 0) {
|
|
124
164
|
throw new TypeError("Pass exactly one of `file` or `audioUrl`.");
|
|
125
165
|
}
|
|
166
|
+
if (file === void 0 && audioUrl === void 0 && !test) {
|
|
167
|
+
throw new TypeError("Pass exactly one of `file` or `audioUrl` (or `test: true`).");
|
|
168
|
+
}
|
|
126
169
|
const fields = {};
|
|
127
170
|
if (options.title !== void 0) fields.title = options.title;
|
|
128
171
|
if (options.artist !== void 0) fields.artist = options.artist;
|
|
129
172
|
if (options.webhookUrl !== void 0) fields.webhook_url = options.webhookUrl;
|
|
130
173
|
if (options.externalRef !== void 0) fields.external_ref = options.externalRef;
|
|
174
|
+
const headers = { [IDEMPOTENCY_HEADER]: options.idempotencyKey || await newIdempotencyKey() };
|
|
131
175
|
let created;
|
|
132
|
-
if (
|
|
133
|
-
|
|
176
|
+
if (file === void 0) {
|
|
177
|
+
const json = audioUrl !== void 0 ? { audio_url: audioUrl, ...fields } : { ...fields };
|
|
178
|
+
if (test) json.test = true;
|
|
179
|
+
created = await this.request("POST", "/songs", { json, headers });
|
|
134
180
|
} else {
|
|
181
|
+
if (test) fields.test = "true";
|
|
135
182
|
const { blob, name } = await toUpload(file, options.filename);
|
|
136
183
|
created = await this.request("POST", "/songs", {
|
|
184
|
+
headers,
|
|
137
185
|
form: () => {
|
|
138
186
|
const form = new FormData();
|
|
139
187
|
form.append("file", blob, name);
|
|
@@ -177,9 +225,28 @@ var Songbrain = class {
|
|
|
177
225
|
shotPlan(songId) {
|
|
178
226
|
return this.request("GET", `/songs/${encodeURIComponent(songId)}/shot-plan`);
|
|
179
227
|
}
|
|
180
|
-
/**
|
|
228
|
+
/** One page of your songs, newest first: `{ object: "list", data, has_more, next_cursor }`. */
|
|
181
229
|
listSongs(options = {}) {
|
|
182
|
-
return this.request("GET", "/songs", {
|
|
230
|
+
return this.request("GET", "/songs", {
|
|
231
|
+
query: { limit: String(options.limit ?? 20), starting_after: options.startingAfter }
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* All your songs, newest first, fetching pages as needed.
|
|
236
|
+
*
|
|
237
|
+
* ```ts
|
|
238
|
+
* for await (const item of sb.iterSongs()) console.log(item.id, item.status);
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
async *iterSongs(options = {}) {
|
|
242
|
+
let cursor = options.startingAfter;
|
|
243
|
+
for (; ; ) {
|
|
244
|
+
const page = await this.listSongs({ limit: options.pageSize ?? 100, startingAfter: cursor });
|
|
245
|
+
const data = page.data ?? [];
|
|
246
|
+
yield* data;
|
|
247
|
+
cursor = page.next_cursor ?? data[data.length - 1]?.id;
|
|
248
|
+
if (!page.has_more || !cursor) return;
|
|
249
|
+
}
|
|
183
250
|
}
|
|
184
251
|
/** Delete a song's audio and analysis now. 409 `still_processing` while it runs. */
|
|
185
252
|
deleteSong(songId) {
|
|
@@ -194,6 +261,20 @@ var Songbrain = class {
|
|
|
194
261
|
pricing() {
|
|
195
262
|
return this.request("GET", "/pricing");
|
|
196
263
|
}
|
|
264
|
+
// ── webhooks ─────────────────────────────────────────────────────────────
|
|
265
|
+
/**
|
|
266
|
+
* Send a signed `ping` event to `url` right now:
|
|
267
|
+
* `{ delivered, status_code, latency_ms, event_id }`. Checks your receiver before real songs.
|
|
268
|
+
*/
|
|
269
|
+
testWebhook(url) {
|
|
270
|
+
return this.request("POST", "/webhooks/test", { json: { url } });
|
|
271
|
+
}
|
|
272
|
+
/** The last webhook delivery attempts for your account, newest first. */
|
|
273
|
+
webhookDeliveries(options = {}) {
|
|
274
|
+
return this.request("GET", "/webhooks/deliveries", {
|
|
275
|
+
query: { limit: String(options.limit ?? 20) }
|
|
276
|
+
});
|
|
277
|
+
}
|
|
197
278
|
// ── examples (no key) ────────────────────────────────────────────────────
|
|
198
279
|
/** Example analyses of Songbrain's own songs. No key needed. */
|
|
199
280
|
examples() {
|
|
@@ -214,13 +295,15 @@ var Songbrain = class {
|
|
|
214
295
|
async request(method, path, opts = {}) {
|
|
215
296
|
const url = new URL(this.baseUrl + path);
|
|
216
297
|
for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
|
|
217
|
-
const
|
|
298
|
+
const hasKey = Boolean(opts.headers?.[IDEMPOTENCY_HEADER]);
|
|
299
|
+
const idempotent = method !== "POST" || hasKey;
|
|
218
300
|
for (let attempt = 0; ; attempt++) {
|
|
219
301
|
const headers = {
|
|
220
302
|
Accept: "application/json",
|
|
221
303
|
"User-Agent": `songbrain-js/${VERSION}`
|
|
222
304
|
};
|
|
223
305
|
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
306
|
+
Object.assign(headers, opts.headers);
|
|
224
307
|
let body;
|
|
225
308
|
if (opts.json) {
|
|
226
309
|
headers["Content-Type"] = "application/json";
|
|
@@ -239,6 +322,9 @@ var Songbrain = class {
|
|
|
239
322
|
const message = err instanceof Error ? err.message : String(err);
|
|
240
323
|
throw new SongbrainError(0, "connection_error", message);
|
|
241
324
|
}
|
|
325
|
+
this.lastRequestId = res.headers.get(REQUEST_ID_HEADER) || null;
|
|
326
|
+
const rateLimit = rateLimitFrom(res.headers);
|
|
327
|
+
if (rateLimit) this.lastRateLimit = rateLimit;
|
|
242
328
|
if (res.ok) {
|
|
243
329
|
const text = await res.text();
|
|
244
330
|
if (!text) return {};
|
|
@@ -249,10 +335,12 @@ var Songbrain = class {
|
|
|
249
335
|
}
|
|
250
336
|
}
|
|
251
337
|
const error = await errorFrom(res);
|
|
252
|
-
const
|
|
338
|
+
const inProgress = hasKey && res.status === 409 && error.code === "idempotency_in_progress";
|
|
339
|
+
const retryable = res.status === 429 || inProgress || res.status >= 500 && (idempotent || res.status === 502 || res.status === 503);
|
|
253
340
|
if (retryable && attempt < this.maxRetries) {
|
|
254
341
|
const after = error instanceof RateLimited ? error.retryAfter : null;
|
|
255
|
-
|
|
342
|
+
let delayMs = after !== null ? after * 1e3 : this.backoffMs(attempt);
|
|
343
|
+
if (inProgress) delayMs = Math.max(1e3, delayMs);
|
|
256
344
|
if (delayMs <= MAX_RETRY_WAIT_SEC * 1e3) {
|
|
257
345
|
await this._sleep(delayMs);
|
|
258
346
|
continue;
|
|
@@ -271,6 +359,7 @@ async function errorFrom(res) {
|
|
|
271
359
|
let body;
|
|
272
360
|
let code = `http_${res.status}`;
|
|
273
361
|
let message = text.trim().slice(0, 500) || res.statusText || `HTTP ${res.status}`;
|
|
362
|
+
let requestId = res.headers.get(REQUEST_ID_HEADER) || null;
|
|
274
363
|
try {
|
|
275
364
|
body = JSON.parse(text);
|
|
276
365
|
} catch {
|
|
@@ -282,6 +371,7 @@ async function errorFrom(res) {
|
|
|
282
371
|
const e = err;
|
|
283
372
|
if (e.code) code = String(e.code);
|
|
284
373
|
if (e.message) message = String(e.message);
|
|
374
|
+
if (e.request_id) requestId = String(e.request_id);
|
|
285
375
|
} else if ("detail" in body) {
|
|
286
376
|
code = "validation_error";
|
|
287
377
|
message = JSON.stringify(body.detail);
|
|
@@ -289,15 +379,15 @@ async function errorFrom(res) {
|
|
|
289
379
|
}
|
|
290
380
|
switch (res.status) {
|
|
291
381
|
case 401:
|
|
292
|
-
return new AuthenticationError(401, code, message, body);
|
|
382
|
+
return new AuthenticationError(401, code, message, body, requestId);
|
|
293
383
|
case 402:
|
|
294
|
-
return new InsufficientCredits(402, code, message, body);
|
|
384
|
+
return new InsufficientCredits(402, code, message, body, requestId);
|
|
295
385
|
case 404:
|
|
296
|
-
return new NotFound(404, code, message, body);
|
|
386
|
+
return new NotFound(404, code, message, body, requestId);
|
|
297
387
|
case 429:
|
|
298
|
-
return new RateLimited(429, code, message, body, retryAfterSeconds(res.headers.get("Retry-After")));
|
|
388
|
+
return new RateLimited(429, code, message, body, retryAfterSeconds(res.headers.get("Retry-After")), requestId);
|
|
299
389
|
default:
|
|
300
|
-
return new SongbrainError(res.status, code, message, body);
|
|
390
|
+
return new SongbrainError(res.status, code, message, body, requestId);
|
|
301
391
|
}
|
|
302
392
|
}
|
|
303
393
|
async function toUpload(file, filename) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/version.ts","../src/client.ts","../src/webhooks.ts"],"names":["createHmac","timingSafeEqual"],"mappings":"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","file":"index.cjs","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":["createHmac","timingSafeEqual"],"mappings":"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","file":"index.cjs","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"]}
|