@mrgoonie/multix 0.15.2 → 0.15.3

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.15.3](https://github.com/mrgoonie/multix-cli/compare/v0.15.2...v0.15.3) (2026-10-11)
9
+
10
+
11
+ ### Fixed
12
+
13
+ * **core:** never forward API-key headers across origins on redirect ([#101](https://github.com/mrgoonie/multix-cli/issues/101)) ([5fd673d](https://github.com/mrgoonie/multix-cli/commit/5fd673ddac9e30b592348cafd6ab68343fa644d7))
14
+
8
15
  ## [0.15.2](https://github.com/mrgoonie/multix-cli/compare/v0.15.1...v0.15.2) (2026-10-11)
9
16
 
10
17
 
package/dist/cli.js CHANGED
@@ -541,6 +541,113 @@ init_errors();
541
541
 
542
542
  // src/core/fetch-json.ts
543
543
  init_errors();
544
+
545
+ // src/core/redirect-fetch.ts
546
+ init_errors();
547
+ var MAX_REDIRECTS = 5;
548
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
549
+ var BODY_HEADERS = ["content-type", "content-length", "content-encoding", "content-language"];
550
+ function withoutBodyHeaders(headers2) {
551
+ if (headers2 === void 0) return void 0;
552
+ const copy = new Headers(headers2);
553
+ for (const name of BODY_HEADERS) copy.delete(name);
554
+ return copy;
555
+ }
556
+ async function fetchFollowingRedirects(url, init = {}, opts = {}) {
557
+ const doFetch = opts.fetch ?? ((input, i) => globalThis.fetch(input, i));
558
+ const { validateUrl } = opts;
559
+ const maxRedirects = opts.maxRedirects ?? MAX_REDIRECTS;
560
+ let current = url;
561
+ let hopInit = { ...init, redirect: "manual" };
562
+ for (let hop = 0; ; hop++) {
563
+ await validateUrl?.(current);
564
+ let response;
565
+ try {
566
+ response = await doFetch(current, hopInit);
567
+ } catch (cause) {
568
+ if (!opts.wrapNetworkErrors) throw cause;
569
+ const msg = cause instanceof Error ? cause.message : String(cause);
570
+ throw new HttpError(0, msg, current);
571
+ }
572
+ if (validateUrl && response.url && response.url !== current) {
573
+ try {
574
+ await validateUrl(response.url);
575
+ } catch (err2) {
576
+ await response.body?.cancel().catch(() => {
577
+ });
578
+ throw err2;
579
+ }
580
+ }
581
+ const location = response.headers.get("location");
582
+ if (!REDIRECT_STATUSES.has(response.status) || !location) {
583
+ return { response, url: current };
584
+ }
585
+ let next;
586
+ try {
587
+ next = new URL(location, current);
588
+ } catch {
589
+ }
590
+ const crossOrigin = next !== void 0 && next.origin !== new URL(current).origin;
591
+ if (crossOrigin && opts.crossOrigin === "stop") return { response, url: current };
592
+ await response.body?.cancel().catch(() => {
593
+ });
594
+ if (hop >= maxRedirects) {
595
+ throw new HttpError(response.status, `too many redirects (over ${maxRedirects})`, current);
596
+ }
597
+ if (!next) {
598
+ throw new HttpError(response.status, "redirect with a malformed Location header", current);
599
+ }
600
+ if (crossOrigin) {
601
+ if (opts.crossOrigin === "error") {
602
+ throw new HttpError(
603
+ response.status,
604
+ `refusing to follow a redirect to another origin (${next.origin})`,
605
+ current
606
+ );
607
+ }
608
+ hopInit = { ...hopInit, headers: void 0 };
609
+ }
610
+ const method = (hopInit.method ?? "GET").toUpperCase();
611
+ const toGet = response.status === 303 && method !== "GET" && method !== "HEAD" || (response.status === 301 || response.status === 302) && method === "POST";
612
+ if (toGet) {
613
+ hopInit = {
614
+ ...hopInit,
615
+ method: "GET",
616
+ body: void 0,
617
+ headers: withoutBodyHeaders(hopInit.headers)
618
+ };
619
+ }
620
+ current = next.toString();
621
+ }
622
+ }
623
+ async function fetchSameOrigin(url, init = {}, fetchImpl) {
624
+ const { response } = await fetchFollowingRedirects(url, init, {
625
+ fetch: fetchImpl,
626
+ crossOrigin: "stop"
627
+ });
628
+ return response;
629
+ }
630
+ async function fetchWithValidatedRedirects(url, opts = {}) {
631
+ const { signal, headers: headers2, validateUrl } = opts;
632
+ const init = { ...signal ? { signal } : {}, ...headers2 ? { headers: headers2 } : {} };
633
+ if (!validateUrl && !headers2) {
634
+ const doFetch = opts.fetch ?? ((input, i) => globalThis.fetch(input, i));
635
+ try {
636
+ return { response: await doFetch(url, init), url };
637
+ } catch (cause) {
638
+ const msg = cause instanceof Error ? cause.message : String(cause);
639
+ throw new HttpError(0, msg, url);
640
+ }
641
+ }
642
+ return fetchFollowingRedirects(url, init, {
643
+ validateUrl,
644
+ maxRedirects: opts.maxRedirects,
645
+ fetch: opts.fetch,
646
+ wrapNetworkErrors: true
647
+ });
648
+ }
649
+
650
+ // src/core/fetch-json.ts
544
651
  var DEFAULT_TIMEOUT_MS = 12e4;
545
652
  var SNIPPET_MAX = 500;
546
653
  function parseRetryAfter(value, now = Date.now()) {
@@ -566,6 +673,10 @@ async function httpErrorFromResponse(response, url) {
566
673
  parseRetryAfter(response.headers.get("retry-after"))
567
674
  );
568
675
  }
676
+ var NON_CREDENTIAL_HEADERS = /* @__PURE__ */ new Set(["accept", "accept-language", "content-type", "user-agent"]);
677
+ function carriesCredentialHeaders(headers2) {
678
+ return Object.keys(headers2).some((name) => !NON_CREDENTIAL_HEADERS.has(name.toLowerCase()));
679
+ }
569
680
  async function fetchJson(opts) {
570
681
  const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
571
682
  const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
@@ -580,13 +691,11 @@ async function fetchJson(opts) {
580
691
  if (body !== void 0) {
581
692
  init.body = JSON.stringify(body);
582
693
  }
583
- let response;
584
- try {
585
- response = await doFetch(url, init);
586
- } catch (cause) {
587
- const msg = cause instanceof Error ? cause.message : String(cause);
588
- throw new HttpError(0, msg, url);
589
- }
694
+ const { response } = await fetchFollowingRedirects(url, init, {
695
+ fetch: doFetch,
696
+ crossOrigin: carriesCredentialHeaders(headers2) ? "error" : "drop-headers",
697
+ wrapNetworkErrors: true
698
+ });
590
699
  if (!response.ok) {
591
700
  throw await httpErrorFromResponse(response, url);
592
701
  }
@@ -934,13 +1043,18 @@ var AtlasCloudClient = class {
934
1043
  const url = `${this.baseUrl}/model/uploadMedia`;
935
1044
  let response;
936
1045
  try {
937
- response = await this.doFetch(url, {
938
- method: "POST",
939
- headers: this.authHeaders(),
940
- body: form,
941
- signal: AbortSignal.timeout(this.uploadTimeoutMs)
942
- });
1046
+ response = await fetchSameOrigin(
1047
+ url,
1048
+ {
1049
+ method: "POST",
1050
+ headers: this.authHeaders(),
1051
+ body: form,
1052
+ signal: AbortSignal.timeout(this.uploadTimeoutMs)
1053
+ },
1054
+ (input, init) => this.doFetch(input, init)
1055
+ );
943
1056
  } catch (cause) {
1057
+ if (cause instanceof HttpError) throw cause;
944
1058
  throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
945
1059
  }
946
1060
  if (!response.ok) throw await httpErrorFromResponse(response, url);
@@ -1133,7 +1247,6 @@ async function httpJson(opts) {
1133
1247
  }
1134
1248
  var MAX_DOWNLOAD_ENV = "MULTIX_MAX_DOWNLOAD_MB";
1135
1249
  var DEFAULT_MAX_DOWNLOAD_MB = 2048;
1136
- var MAX_REDIRECTS = 5;
1137
1250
  function formatMb2(bytes) {
1138
1251
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1139
1252
  }
@@ -1163,44 +1276,6 @@ function maxDownloadBytes() {
1163
1276
  }
1164
1277
  return bytes;
1165
1278
  }
1166
- async function fetchWithValidatedRedirects(url, opts = {}) {
1167
- const doFetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
1168
- const { validateUrl, signal } = opts;
1169
- const maxRedirects = opts.maxRedirects ?? MAX_REDIRECTS;
1170
- let current = url;
1171
- let headers2 = opts.headers;
1172
- for (let hop = 0; ; hop++) {
1173
- await validateUrl?.(current);
1174
- let response;
1175
- try {
1176
- response = await doFetch(current, {
1177
- ...signal ? { signal } : {},
1178
- ...headers2 ? { headers: headers2 } : {},
1179
- ...validateUrl ? { redirect: "manual" } : {}
1180
- });
1181
- } catch (cause) {
1182
- const msg = cause instanceof Error ? cause.message : String(cause);
1183
- throw new HttpError(0, msg, current);
1184
- }
1185
- const location = response.headers.get("location");
1186
- if (!validateUrl || !location || response.status < 300 || response.status >= 400) {
1187
- return { response, url: current };
1188
- }
1189
- await response.body?.cancel().catch(() => {
1190
- });
1191
- if (hop >= maxRedirects) {
1192
- throw new HttpError(response.status, `too many redirects (over ${maxRedirects})`, current);
1193
- }
1194
- let next;
1195
- try {
1196
- next = new URL(location, current);
1197
- } catch {
1198
- throw new HttpError(response.status, "redirect with a malformed Location header", current);
1199
- }
1200
- if (next.origin !== new URL(current).origin) headers2 = void 0;
1201
- current = next.toString();
1202
- }
1203
- }
1204
1279
  function removeQuietly(file) {
1205
1280
  try {
1206
1281
  fs3.rmSync(file, { force: true });
@@ -1262,7 +1337,7 @@ async function downloadToFile(url, opts) {
1262
1337
  yield chunk;
1263
1338
  }
1264
1339
  }
1265
- const part = `${dest}.part`;
1340
+ const part = `${dest}.${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.part`;
1266
1341
  let fileStream;
1267
1342
  try {
1268
1343
  fileStream = fs3.createWriteStream(part);
@@ -3132,8 +3207,13 @@ init_errors();
3132
3207
  function cloudflareProviderFailure(status) {
3133
3208
  return new ProviderError(`Cloudflare request failed with HTTP ${status}`, "Cloudflare");
3134
3209
  }
3210
+ async function cloudflareResponseFailure(response) {
3211
+ await response.body?.cancel().catch(() => {
3212
+ });
3213
+ return cloudflareProviderFailure(response.status);
3214
+ }
3135
3215
  async function readCloudflareJson(response) {
3136
- if (!response.ok) throw cloudflareProviderFailure(response.status);
3216
+ if (!response.ok) throw await cloudflareResponseFailure(response);
3137
3217
  try {
3138
3218
  return await response.json();
3139
3219
  } catch {
@@ -3153,12 +3233,11 @@ function videoHeaders(config, opts) {
3153
3233
  };
3154
3234
  }
3155
3235
  function send(opts, url, init) {
3156
- const f = opts.fetch;
3157
3236
  const withTimeout = {
3158
3237
  ...init,
3159
3238
  signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
3160
3239
  };
3161
- return f ? f(url, withTimeout) : globalThis.fetch(url, withTimeout);
3240
+ return fetchSameOrigin(url, withTimeout, opts.fetch);
3162
3241
  }
3163
3242
  function parseCloudflareVideoPrediction(value) {
3164
3243
  if (!value || typeof value !== "object") {
@@ -3211,7 +3290,7 @@ function gatewayUrl(accountId) {
3211
3290
  async function runCloudflareJson(model, input) {
3212
3291
  const config = requireCloudflareConfig();
3213
3292
  const useGateway = Boolean(config.gatewayId);
3214
- const response = await globalThis.fetch(
3293
+ const response = await fetchSameOrigin(
3215
3294
  useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
3216
3295
  {
3217
3296
  method: "POST",
@@ -3228,7 +3307,7 @@ async function runCloudflareJson(model, input) {
3228
3307
  async function runCloudflareBytes(model, input) {
3229
3308
  const config = requireCloudflareConfig();
3230
3309
  const useGateway = Boolean(config.gatewayId);
3231
- const response = await globalThis.fetch(
3310
+ const response = await fetchSameOrigin(
3232
3311
  useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
3233
3312
  {
3234
3313
  method: "POST",
@@ -3236,7 +3315,7 @@ async function runCloudflareBytes(model, input) {
3236
3315
  body: JSON.stringify(useGateway ? { model, input } : input)
3237
3316
  }
3238
3317
  );
3239
- if (!response.ok) throw cloudflareProviderFailure(response.status);
3318
+ if (!response.ok) throw await cloudflareResponseFailure(response);
3240
3319
  const contentType = response.headers.get("content-type") ?? "";
3241
3320
  if (!contentType.includes("audio/mpeg")) {
3242
3321
  throw new ProviderError("Cloudflare did not return MPEG audio", "Cloudflare");
@@ -3558,7 +3637,7 @@ function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
3558
3637
  }
3559
3638
  async function apiDelete(endpoint2, apiKey) {
3560
3639
  const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
3561
- const res = await globalThis.fetch(url, {
3640
+ const res = await fetchSameOrigin(url, {
3562
3641
  method: "DELETE",
3563
3642
  headers: authHeaders(apiKey),
3564
3643
  signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
@@ -3592,8 +3671,9 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
3592
3671
  }
3593
3672
  let res;
3594
3673
  try {
3595
- res = await globalThis.fetch(url.toString(), init);
3674
+ res = await fetchSameOrigin(url.toString(), init);
3596
3675
  } catch (cause) {
3676
+ if (cause instanceof HttpError) throw cause;
3597
3677
  const msg = cause instanceof Error ? cause.message : String(cause);
3598
3678
  throw new HttpError(0, msg, url.toString());
3599
3679
  }
@@ -3607,13 +3687,14 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
3607
3687
  const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
3608
3688
  let res;
3609
3689
  try {
3610
- res = await globalThis.fetch(url, {
3690
+ res = await fetchSameOrigin(url, {
3611
3691
  method: "POST",
3612
3692
  headers: authHeaders(apiKey),
3613
3693
  body: form,
3614
3694
  signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
3615
3695
  });
3616
3696
  } catch (cause) {
3697
+ if (cause instanceof HttpError) throw cause;
3617
3698
  const msg = cause instanceof Error ? cause.message : String(cause);
3618
3699
  throw new HttpError(0, msg, url);
3619
3700
  }
@@ -3822,7 +3903,7 @@ Dubbed file: ${dest}`);
3822
3903
  }
3823
3904
  async function downloadDub(url, apiKey, dest) {
3824
3905
  fs15.mkdirSync(path18.dirname(dest), { recursive: true });
3825
- const res = await globalThis.fetch(url, {
3906
+ const { response: res } = await fetchWithValidatedRedirects(url, {
3826
3907
  headers: { "xi-api-key": apiKey },
3827
3908
  signal: AbortSignal.timeout(6e5)
3828
3909
  });
@@ -3831,7 +3912,6 @@ async function downloadDub(url, apiKey, dest) {
3831
3912
  }
3832
3913
  const arr = new Uint8Array(await res.arrayBuffer());
3833
3914
  fs15.writeFileSync(dest, arr);
3834
- void downloadFile;
3835
3915
  }
3836
3916
 
3837
3917
  // src/providers/elevenlabs/commands/isolate.ts
@@ -5814,13 +5894,14 @@ function checkVeoResult(op) {
5814
5894
  return op;
5815
5895
  }
5816
5896
  async function downloadVeoVideo(apiKey, uri, dest) {
5817
- let host = "";
5897
+ let parsed;
5818
5898
  try {
5819
- host = new URL(uri).hostname;
5899
+ parsed = new URL(uri);
5820
5900
  } catch {
5821
5901
  throw new ProviderError("Veo returned an invalid video URI", "gemini");
5822
5902
  }
5823
- await downloadFile(uri, dest, 3e5, host === API_HOST ? headers(apiKey) : void 0);
5903
+ const keyed = parsed.protocol === "https:" && parsed.hostname === API_HOST;
5904
+ await downloadFile(uri, dest, 3e5, keyed ? headers(apiKey) : void 0);
5824
5905
  }
5825
5906
  async function runVeo(model, request, dest, logger) {
5826
5907
  const body = buildVeoRequest(request);
@@ -6316,10 +6397,8 @@ var HEYGEN_OPERATIONS = {
6316
6397
  };
6317
6398
 
6318
6399
  // src/providers/heygen/node-helpers.ts
6319
- import { once } from "events";
6320
6400
  import fs29 from "fs";
6321
6401
  import path32 from "path";
6322
- import { pipeline as pipeline2 } from "stream/promises";
6323
6402
  init_errors();
6324
6403
  init_redact();
6325
6404
 
@@ -6666,13 +6745,18 @@ var HeygenClient = class {
6666
6745
  form.append("file", typed, input.filename);
6667
6746
  let response;
6668
6747
  try {
6669
- response = await doFetch(url, {
6670
- method: "POST",
6671
- headers: this.headers(key),
6672
- body: form,
6673
- signal: AbortSignal.timeout(this.timeoutMs)
6674
- });
6748
+ response = await fetchSameOrigin(
6749
+ url,
6750
+ {
6751
+ method: "POST",
6752
+ headers: this.headers(key),
6753
+ body: form,
6754
+ signal: AbortSignal.timeout(this.timeoutMs)
6755
+ },
6756
+ doFetch
6757
+ );
6675
6758
  } catch (cause) {
6759
+ if (cause instanceof HttpError) throw toHeygenApiError(cause);
6676
6760
  const msg = cause instanceof Error ? cause.message : String(cause);
6677
6761
  const err3 = toHeygenApiError(new HttpError(0, msg, url));
6678
6762
  if (attempt >= this.maxAttempts) throw err3;
@@ -7323,44 +7407,11 @@ function inferExtension(url, fallback) {
7323
7407
  function safeSlug(value) {
7324
7408
  return value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "out";
7325
7409
  }
7326
- var MAX_REDIRECTS2 = 5;
7327
- var isRedirect = (status) => [301, 302, 303, 307, 308].includes(status);
7328
- async function downloadHttpsOnly(url, dest, timeoutMs) {
7329
- const signal = AbortSignal.timeout(timeoutMs);
7330
- let current = url;
7331
- for (let hop = 0; ; hop++) {
7332
- if (!isHttpsUrl(current)) {
7333
- throw new HttpError(0, "refusing to follow a redirect to a non-https URL", current);
7334
- }
7335
- let response;
7336
- try {
7337
- response = await globalThis.fetch(current, { signal, redirect: "manual" });
7338
- } catch (cause) {
7339
- throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), current);
7340
- }
7341
- if (response.url && !isHttpsUrl(response.url)) {
7342
- await response.body?.cancel();
7343
- throw new HttpError(0, "redirected to a non-https URL", response.url);
7344
- }
7345
- const location = response.headers.get("location");
7346
- if (isRedirect(response.status) && location) {
7347
- await response.body?.cancel();
7348
- if (hop >= MAX_REDIRECTS2) throw new HttpError(0, "too many redirects", url);
7349
- current = new URL(location, current).href;
7350
- continue;
7351
- }
7352
- if (!response.ok) throw new HttpError(response.status, "(download failed)", current);
7353
- if (!response.body) throw new HttpError(0, "Response body is null", current);
7354
- fs29.mkdirSync(path32.dirname(dest), { recursive: true });
7355
- const fileStream = fs29.createWriteStream(dest);
7356
- try {
7357
- await pipeline2(response.body, fileStream);
7358
- } catch (err2) {
7359
- if (!fileStream.closed) await once(fileStream, "close");
7360
- throw err2;
7361
- }
7362
- return;
7363
- }
7410
+ function requireHttps2(url) {
7411
+ if (!isHttpsUrl(url)) throw new HttpError(0, "redirected to a non-https URL", url);
7412
+ }
7413
+ function downloadHttps(url, dest, timeoutMs) {
7414
+ return downloadToFile(url, { dest, timeoutMs, validateUrl: requireHttps2 });
7364
7415
  }
7365
7416
  async function downloadOutput(url, dest, resumeHint) {
7366
7417
  if (!url) {
@@ -7371,12 +7422,9 @@ async function downloadOutput(url, dest, resumeHint) {
7371
7422
  `Refusing to download HeyGen output from a non-https URL (${redactUrl(url)})`
7372
7423
  );
7373
7424
  }
7374
- const tmp = `${dest}.${process.pid}-${Date.now()}.part`;
7375
7425
  try {
7376
- await downloadHttpsOnly(url, tmp, 6e5);
7377
- fs29.renameSync(tmp, dest);
7426
+ await downloadHttps(url, dest, 6e5);
7378
7427
  } catch (e) {
7379
- fs29.rmSync(tmp, { force: true });
7380
7428
  const status = e instanceof HttpError ? e.status : 0;
7381
7429
  const expired = status === 403 || status === 404 || status === 410;
7382
7430
  const reason = e instanceof Error ? e.message : String(e);
@@ -7438,7 +7486,7 @@ function captionPathFor(dest, capExt) {
7438
7486
  const capDest = `${stem}.${capExt}`;
7439
7487
  return capDest.toLowerCase() === dest.toLowerCase() ? `${stem}.captions.${capExt}` : capDest;
7440
7488
  }
7441
- var downloadThumbHttpsOnly = (url, dest) => downloadHttpsOnly(url, dest, 12e4);
7489
+ var downloadThumbHttpsOnly = (url, dest) => downloadHttps(url, dest, 12e4);
7442
7490
  function summarizeState(state) {
7443
7491
  const out = {
7444
7492
  kind: state.kind,
@@ -9585,13 +9633,14 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
9585
9633
  const url = endpoint(pathname);
9586
9634
  let res;
9587
9635
  try {
9588
- res = await globalThis.fetch(url, {
9636
+ res = await fetchSameOrigin(url, {
9589
9637
  method: "POST",
9590
9638
  headers: authHeaders4(apiKey),
9591
9639
  body: form,
9592
9640
  signal: AbortSignal.timeout(timeoutMs)
9593
9641
  });
9594
9642
  } catch (cause) {
9643
+ if (cause instanceof HttpError) throw cause;
9595
9644
  throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
9596
9645
  }
9597
9646
  if (!res.ok) {
@@ -9604,13 +9653,14 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
9604
9653
  const url = endpoint(pathname);
9605
9654
  let res;
9606
9655
  try {
9607
- res = await globalThis.fetch(url, {
9656
+ res = await fetchSameOrigin(url, {
9608
9657
  method: "POST",
9609
9658
  headers: authHeaders4(apiKey, { "Content-Type": "application/json" }),
9610
9659
  body: JSON.stringify(body),
9611
9660
  signal: AbortSignal.timeout(timeoutMs)
9612
9661
  });
9613
9662
  } catch (cause) {
9663
+ if (cause instanceof HttpError) throw cause;
9614
9664
  throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
9615
9665
  }
9616
9666
  if (!res.ok) {
@@ -10450,7 +10500,9 @@ async function downloadOpenRouterVideo(opts) {
10450
10500
  const apiKey = requireOpenRouterKey();
10451
10501
  const url = videoContentUrl(jobId);
10452
10502
  const dest = path50.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
10453
- const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
10503
+ const { response: r } = await fetchWithValidatedRedirects(url, {
10504
+ headers: { Authorization: `Bearer ${apiKey}` }
10505
+ });
10454
10506
  if (!r.ok) {
10455
10507
  logger.error(`Download HTTP ${r.status}`);
10456
10508
  process.exit(1);
@@ -10461,8 +10513,8 @@ async function downloadOpenRouterVideo(opts) {
10461
10513
  }
10462
10514
  fs43.mkdirSync(path50.dirname(dest), { recursive: true });
10463
10515
  const ws = fs43.createWriteStream(dest);
10464
- const { pipeline: pipeline3 } = await import("stream/promises");
10465
- await pipeline3(r.body, ws);
10516
+ const { pipeline: pipeline2 } = await import("stream/promises");
10517
+ await pipeline2(r.body, ws);
10466
10518
  logger.success(`Saved ${dest}`);
10467
10519
  if (opts.output) {
10468
10520
  fs43.mkdirSync(path50.dirname(path50.resolve(opts.output)), { recursive: true });
@@ -10924,13 +10976,18 @@ var ReplicateClient = class {
10924
10976
  const subject = `file upload ${opts.filename}`;
10925
10977
  let response;
10926
10978
  try {
10927
- response = await this.doFetch(url, {
10928
- method: "POST",
10929
- headers: this.headers(),
10930
- body: form,
10931
- signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
10932
- });
10979
+ response = await fetchSameOrigin(
10980
+ url,
10981
+ {
10982
+ method: "POST",
10983
+ headers: this.headers(),
10984
+ body: form,
10985
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
10986
+ },
10987
+ this.doFetch
10988
+ );
10933
10989
  } catch (cause) {
10990
+ if (cause instanceof HttpError) throw mapReplicateError(cause, subject);
10934
10991
  const msg = cause instanceof Error ? cause.message : String(cause);
10935
10992
  throw mapReplicateError(new HttpError(0, msg, url), subject);
10936
10993
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrgoonie/multix",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, ImageMagick, doc-to-md",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: multix
3
3
  description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX, Replicate FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models, Replicate direct API models incl. P-Video, HeyGen Video Agent and scripted avatar videos), video translation and lipsync (HeyGen), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, Atlas Cloud, Replicate, replicate.com models, Replicate predictions, HeyGen, avatar video, talking head, Video Agent, video translation, dubbing, lipsync, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
4
- version: 0.15.2 # x-release-please-version
4
+ version: 0.15.3 # x-release-please-version
5
5
  ---
6
6
 
7
7
  # multix — AI Multimodal CLI