@mux/ai 0.6.0 → 0.7.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/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // package.json
8
- var version = "0.6.0";
8
+ var version = "0.7.3";
9
9
 
10
10
  // src/env.ts
11
11
  import { z } from "zod";
@@ -60,6 +60,9 @@ var EnvSchema = z.object({
60
60
  S3_BUCKET: optionalString("Bucket used for caption and audio uploads.", "S3 bucket"),
61
61
  S3_ACCESS_KEY_ID: optionalString("Access key ID for S3-compatible uploads.", "S3 access key id"),
62
62
  S3_SECRET_ACCESS_KEY: optionalString("Secret access key for S3-compatible uploads.", "S3 secret access key"),
63
+ S3_ALLOWED_ENDPOINT_HOSTS: optionalString(
64
+ "Comma-separated S3 endpoint allowlist (supports exact hosts and *.suffix patterns)."
65
+ ),
63
66
  EVALITE_RESULTS_ENDPOINT: optionalString(
64
67
  "Full URL for posting Evalite results (e.g., https://example.com/api/evalite-results).",
65
68
  "Evalite results endpoint"
@@ -135,11 +138,15 @@ function bytesToBase64(bytes) {
135
138
  }
136
139
  return output;
137
140
  }
141
+ function normalizeBase64Input(value) {
142
+ const cleaned = value.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
143
+ return cleaned?.length % 4 === 0 ? cleaned : cleaned + "=".repeat(4 - cleaned.length % 4);
144
+ }
138
145
  function base64ToBytes(value, label) {
139
146
  if (!value) {
140
147
  throw new Error(`${label} is missing`);
141
148
  }
142
- const normalized = value.length % 4 === 0 ? value : value + "=".repeat(4 - value.length % 4);
149
+ const normalized = normalizeBase64Input(value);
143
150
  const atobImpl = typeof globalThis.atob === "function" ? globalThis.atob.bind(globalThis) : void 0;
144
151
  if (atobImpl) {
145
152
  let binary;
@@ -157,19 +164,18 @@ function base64ToBytes(value, label) {
157
164
  }
158
165
  return bytes2;
159
166
  }
160
- const cleaned = normalized.replace(/\s+/g, "");
161
- if (cleaned.length % 4 !== 0) {
167
+ if (normalized.length % 4 !== 0) {
162
168
  throw new Error(`${label} is not valid base64`);
163
169
  }
164
- const padding = cleaned.endsWith("==") ? 2 : cleaned.endsWith("=") ? 1 : 0;
165
- const outputLength = cleaned.length / 4 * 3 - padding;
170
+ const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
171
+ const outputLength = normalized.length / 4 * 3 - padding;
166
172
  const bytes = new Uint8Array(outputLength);
167
173
  let offset = 0;
168
- for (let i = 0; i < cleaned.length; i += 4) {
169
- const c0 = cleaned.charCodeAt(i);
170
- const c1 = cleaned.charCodeAt(i + 1);
171
- const c2 = cleaned.charCodeAt(i + 2);
172
- const c3 = cleaned.charCodeAt(i + 3);
174
+ for (let i = 0; i < normalized.length; i += 4) {
175
+ const c0 = normalized.charCodeAt(i);
176
+ const c1 = normalized.charCodeAt(i + 1);
177
+ const c2 = normalized.charCodeAt(i + 2);
178
+ const c3 = normalized.charCodeAt(i + 3);
173
179
  if (c0 === 61 || c1 === 61) {
174
180
  throw new Error(`${label} is not valid base64`);
175
181
  }
@@ -318,7 +324,9 @@ async function shouldEnforceEncryptedCredentials() {
318
324
  function getWorkflowSecretKeyFromEnv() {
319
325
  const key = env_default.MUX_AI_WORKFLOW_SECRET_KEY;
320
326
  if (!key) {
321
- throw new Error("Workflow secret key is required. Set MUX_AI_WORKFLOW_SECRET_KEY environment variable.");
327
+ throw new Error(
328
+ "Workflow secret key is required. Set MUX_AI_WORKFLOW_SECRET_KEY environment variable."
329
+ );
322
330
  }
323
331
  return key;
324
332
  }
@@ -349,36 +357,92 @@ async function resolveWorkflowCredentials(credentials) {
349
357
  );
350
358
  return { ...resolved, ...decrypted };
351
359
  } catch (error) {
352
- const detail = error instanceof Error ? error.message : String(error);
360
+ const detail = error instanceof Error ? error.message : "Unknown error.";
353
361
  throw new Error(`Failed to decrypt workflow credentials. ${detail}`);
354
362
  }
355
363
  }
356
364
  if (await shouldEnforceEncryptedCredentials()) {
357
365
  throw new Error(
358
- "Plaintext workflow credentials are not allowed when using Workflow Dev Kit.Pass encrypted credentials (encryptForWorkflow) or resolve secrets via environment variables."
366
+ "Plaintext workflow credentials are not allowed when using Workflow Dev Kit. Pass encrypted credentials (encryptForWorkflow) or resolve secrets via environment variables."
359
367
  );
360
368
  }
361
369
  return { ...resolved, ...credentials };
362
370
  }
363
- async function resolveMuxCredentials(credentials) {
371
+ function readString(record, key) {
372
+ const value = record?.[key];
373
+ return typeof value === "string" && value.length > 0 ? value : void 0;
374
+ }
375
+ function resolveDirectMuxCredentials(record) {
376
+ const tokenId = readString(record, "muxTokenId");
377
+ const tokenSecret = readString(record, "muxTokenSecret");
378
+ const signingKey = readString(record, "muxSigningKey");
379
+ const privateKey = readString(record, "muxPrivateKey");
380
+ if (!tokenId && !tokenSecret && !signingKey && !privateKey) {
381
+ return void 0;
382
+ }
383
+ if (!tokenId || !tokenSecret) {
384
+ throw new Error(
385
+ "Both muxTokenId and muxTokenSecret are required when passing direct Mux workflow credentials."
386
+ );
387
+ }
388
+ return {
389
+ tokenId,
390
+ tokenSecret,
391
+ signingKey,
392
+ privateKey
393
+ };
394
+ }
395
+ function createWorkflowMuxClient(options) {
396
+ return {
397
+ async createClient() {
398
+ const { default: MuxClient } = await import("@mux/mux-node");
399
+ return new MuxClient({
400
+ tokenId: options.tokenId,
401
+ tokenSecret: options.tokenSecret
402
+ });
403
+ },
404
+ getSigningKey() {
405
+ return options.signingKey;
406
+ },
407
+ getPrivateKey() {
408
+ return options.privateKey;
409
+ }
410
+ };
411
+ }
412
+ async function resolveMuxClient(credentials) {
364
413
  const resolved = await resolveWorkflowCredentials(credentials);
365
- const muxTokenId = resolved.muxTokenId ?? env_default.MUX_TOKEN_ID;
366
- const muxTokenSecret = resolved.muxTokenSecret ?? env_default.MUX_TOKEN_SECRET;
414
+ const resolvedRecord = resolved;
415
+ const resolvedMuxCredentials = resolveDirectMuxCredentials(resolvedRecord);
416
+ if (resolvedMuxCredentials) {
417
+ return createWorkflowMuxClient(resolvedMuxCredentials);
418
+ }
419
+ const muxTokenId = env_default.MUX_TOKEN_ID;
420
+ const muxTokenSecret = env_default.MUX_TOKEN_SECRET;
367
421
  if (!muxTokenId || !muxTokenSecret) {
368
422
  throw new Error(
369
- "Mux credentials are required. Provide encrypted workflow credentials or set MUX_TOKEN_ID and MUX_TOKEN_SECRET environment variables."
423
+ "Mux credentials are required. Provide muxTokenId/muxTokenSecret via workflow credentials, or set MUX_TOKEN_ID and MUX_TOKEN_SECRET environment variables."
370
424
  );
371
425
  }
372
- return { muxTokenId, muxTokenSecret };
426
+ return createWorkflowMuxClient({
427
+ tokenId: muxTokenId,
428
+ tokenSecret: muxTokenSecret,
429
+ signingKey: env_default.MUX_SIGNING_KEY,
430
+ privateKey: env_default.MUX_PRIVATE_KEY
431
+ });
373
432
  }
374
- async function resolveProviderApiKey(provider, credentials) {
375
- const resolved = await resolveWorkflowCredentials(credentials);
433
+ function resolveProviderApiKeyFromCredentials(provider, resolved) {
434
+ const record = resolved;
435
+ const openaiApiKey = readString(record, "openaiApiKey");
436
+ const anthropicApiKey = readString(record, "anthropicApiKey");
437
+ const googleApiKey = readString(record, "googleApiKey");
438
+ const hiveApiKey = readString(record, "hiveApiKey");
439
+ const elevenLabsApiKey = readString(record, "elevenLabsApiKey");
376
440
  const apiKeyMap = {
377
- openai: resolved.openaiApiKey ?? env_default.OPENAI_API_KEY,
378
- anthropic: resolved.anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
379
- google: resolved.googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
380
- hive: resolved.hiveApiKey ?? env_default.HIVE_API_KEY,
381
- elevenlabs: resolved.elevenLabsApiKey ?? env_default.ELEVENLABS_API_KEY
441
+ openai: openaiApiKey ?? env_default.OPENAI_API_KEY,
442
+ anthropic: anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
443
+ google: googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
444
+ hive: hiveApiKey ?? env_default.HIVE_API_KEY,
445
+ elevenlabs: elevenLabsApiKey ?? env_default.ELEVENLABS_API_KEY
382
446
  };
383
447
  const apiKey = apiKeyMap[provider];
384
448
  if (!apiKey) {
@@ -390,21 +454,320 @@ async function resolveProviderApiKey(provider, credentials) {
390
454
  elevenlabs: "ELEVENLABS_API_KEY"
391
455
  };
392
456
  throw new Error(
393
- `${provider} API key is required. Provide encrypted workflow credentials or set ${envVarNames[provider]} environment variable.`
457
+ `${provider} API key is required. Provide ${provider} credentials via workflow credentials or set ${envVarNames[provider]} environment variable.`
394
458
  );
395
459
  }
396
460
  return apiKey;
397
461
  }
462
+ async function resolveProviderApiKey(provider, credentials) {
463
+ const resolved = await resolveWorkflowCredentials(credentials);
464
+ return resolveProviderApiKeyFromCredentials(provider, resolved);
465
+ }
398
466
  async function resolveMuxSigningContext(credentials) {
399
467
  const resolved = await resolveWorkflowCredentials(credentials);
400
- const keyId = resolved.muxSigningKey ?? env_default.MUX_SIGNING_KEY;
401
- const keySecret = resolved.muxPrivateKey ?? env_default.MUX_PRIVATE_KEY;
468
+ const resolvedRecord = resolved;
469
+ const keyId = readString(resolvedRecord, "muxSigningKey") ?? env_default.MUX_SIGNING_KEY;
470
+ const keySecret = readString(resolvedRecord, "muxPrivateKey") ?? env_default.MUX_PRIVATE_KEY;
402
471
  if (!keyId || !keySecret) {
403
472
  return void 0;
404
473
  }
405
474
  return { keyId, keySecret };
406
475
  }
407
476
 
477
+ // node_modules/@workflow/serde/dist/index.js
478
+ var WORKFLOW_SERIALIZE = /* @__PURE__ */ Symbol.for("workflow-serialize");
479
+ var WORKFLOW_DESERIALIZE = /* @__PURE__ */ Symbol.for("workflow-deserialize");
480
+
481
+ // src/lib/s3-sigv4.ts
482
+ var AWS4_ALGORITHM = "AWS4-HMAC-SHA256";
483
+ var AWS4_REQUEST_TERMINATOR = "aws4_request";
484
+ var AWS4_SERVICE = "s3";
485
+ var S3_ALLOWED_ENDPOINT_PATTERNS = parseEndpointAllowlist(
486
+ env_default.S3_ALLOWED_ENDPOINT_HOSTS
487
+ );
488
+ function getCrypto() {
489
+ const webCrypto = globalThis.crypto;
490
+ if (!webCrypto?.subtle) {
491
+ throw new Error("Web Crypto API is required for S3 signing.");
492
+ }
493
+ return webCrypto;
494
+ }
495
+ var textEncoder = new TextEncoder();
496
+ function toBytes(value) {
497
+ return typeof value === "string" ? textEncoder.encode(value) : value;
498
+ }
499
+ function bytesToHex(bytes) {
500
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
501
+ }
502
+ async function sha256Hex(value) {
503
+ const digest = await getCrypto().subtle.digest("SHA-256", toBytes(value));
504
+ return bytesToHex(new Uint8Array(digest));
505
+ }
506
+ async function hmacSha256Raw(key, value) {
507
+ const cryptoKey = await getCrypto().subtle.importKey(
508
+ "raw",
509
+ key,
510
+ { name: "HMAC", hash: "SHA-256" },
511
+ false,
512
+ ["sign"]
513
+ );
514
+ const signature = await getCrypto().subtle.sign("HMAC", cryptoKey, textEncoder.encode(value));
515
+ return new Uint8Array(signature);
516
+ }
517
+ async function deriveSigningKey(secretAccessKey, shortDate, region) {
518
+ const kDate = await hmacSha256Raw(textEncoder.encode(`AWS4${secretAccessKey}`), shortDate);
519
+ const kRegion = await hmacSha256Raw(kDate, region);
520
+ const kService = await hmacSha256Raw(kRegion, AWS4_SERVICE);
521
+ return hmacSha256Raw(kService, AWS4_REQUEST_TERMINATOR);
522
+ }
523
+ function formatAmzDate(date = /* @__PURE__ */ new Date()) {
524
+ const iso = date.toISOString();
525
+ const shortDate = iso.slice(0, 10).replace(/-/g, "");
526
+ const amzDate = `${iso.slice(0, 19).replace(/[-:]/g, "")}Z`;
527
+ return { amzDate, shortDate };
528
+ }
529
+ function encodeRFC3986(value) {
530
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
531
+ }
532
+ function encodePath(path) {
533
+ return path.split("/").map((segment) => encodeRFC3986(segment)).join("/");
534
+ }
535
+ function normalizeEndpoint(endpoint) {
536
+ let url;
537
+ try {
538
+ url = new URL(endpoint);
539
+ } catch {
540
+ throw new Error(`Invalid S3 endpoint: ${endpoint}`);
541
+ }
542
+ if (url.search || url.hash) {
543
+ throw new Error("S3 endpoint must not include query params or hash fragments.");
544
+ }
545
+ enforceEndpointPolicy(url);
546
+ return url;
547
+ }
548
+ function parseEndpointAllowlist(allowlist) {
549
+ if (!allowlist) {
550
+ return [];
551
+ }
552
+ return allowlist.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
553
+ }
554
+ function hostnameMatchesPattern(hostname, pattern) {
555
+ if (pattern.startsWith("*.")) {
556
+ const suffix = pattern.slice(1);
557
+ return hostname.endsWith(suffix) && hostname.length > suffix.length;
558
+ }
559
+ return hostname === pattern;
560
+ }
561
+ function enforceEndpointPolicy(url) {
562
+ const hostname = url.hostname.toLowerCase();
563
+ if (url.protocol !== "https:") {
564
+ throw new Error(
565
+ `Insecure S3 endpoint protocol "${url.protocol}" is not allowed. Use HTTPS.`
566
+ );
567
+ }
568
+ if (S3_ALLOWED_ENDPOINT_PATTERNS.length > 0 && !S3_ALLOWED_ENDPOINT_PATTERNS.some((pattern) => hostnameMatchesPattern(hostname, pattern))) {
569
+ throw new Error(
570
+ `S3 endpoint host "${hostname}" is not in S3_ALLOWED_ENDPOINT_HOSTS.`
571
+ );
572
+ }
573
+ }
574
+ function buildCanonicalUri(endpoint, bucket, key) {
575
+ const endpointPath = endpoint.pathname === "/" ? "" : encodePath(endpoint.pathname.replace(/\/+$/, ""));
576
+ const encodedBucket = encodeRFC3986(bucket);
577
+ const encodedKey = encodePath(key);
578
+ return `${endpointPath}/${encodedBucket}/${encodedKey}`;
579
+ }
580
+ function buildCanonicalQuery(params) {
581
+ return Object.entries(params).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${encodeRFC3986(key)}=${encodeRFC3986(value)}`).join("&");
582
+ }
583
+ async function signString(secretAccessKey, shortDate, region, value) {
584
+ const signingKey = await deriveSigningKey(secretAccessKey, shortDate, region);
585
+ const signatureBytes = await hmacSha256Raw(signingKey, value);
586
+ return bytesToHex(signatureBytes);
587
+ }
588
+ function buildCredentialScope(shortDate, region) {
589
+ return `${shortDate}/${region}/${AWS4_SERVICE}/${AWS4_REQUEST_TERMINATOR}`;
590
+ }
591
+ async function putObjectToS3({
592
+ accessKeyId,
593
+ secretAccessKey,
594
+ endpoint,
595
+ region,
596
+ bucket,
597
+ key,
598
+ body,
599
+ contentType
600
+ }) {
601
+ const resolvedEndpoint = normalizeEndpoint(endpoint);
602
+ const canonicalUri = buildCanonicalUri(resolvedEndpoint, bucket, key);
603
+ const host = resolvedEndpoint.host;
604
+ const normalizedContentType = contentType?.trim();
605
+ const { amzDate, shortDate } = formatAmzDate();
606
+ const payloadHash = await sha256Hex(body);
607
+ const signingHeaders = [
608
+ ["host", host],
609
+ ["x-amz-content-sha256", payloadHash],
610
+ ["x-amz-date", amzDate],
611
+ ...normalizedContentType ? [["content-type", normalizedContentType]] : []
612
+ ].sort(([a], [b]) => a.localeCompare(b));
613
+ const canonicalHeaders = signingHeaders.map(([name, value]) => `${name}:${value}`).join("\n");
614
+ const signedHeaders = signingHeaders.map(([name]) => name).join(";");
615
+ const canonicalRequest = [
616
+ "PUT",
617
+ canonicalUri,
618
+ "",
619
+ `${canonicalHeaders}
620
+ `,
621
+ signedHeaders,
622
+ payloadHash
623
+ ].join("\n");
624
+ const credentialScope = buildCredentialScope(shortDate, region);
625
+ const stringToSign = [
626
+ AWS4_ALGORITHM,
627
+ amzDate,
628
+ credentialScope,
629
+ await sha256Hex(canonicalRequest)
630
+ ].join("\n");
631
+ const signature = await signString(secretAccessKey, shortDate, region, stringToSign);
632
+ const authorization = `${AWS4_ALGORITHM} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
633
+ const requestUrl = `${resolvedEndpoint.origin}${canonicalUri}`;
634
+ const response = await fetch(requestUrl, {
635
+ method: "PUT",
636
+ headers: {
637
+ "Authorization": authorization,
638
+ "x-amz-content-sha256": payloadHash,
639
+ "x-amz-date": amzDate,
640
+ ...normalizedContentType ? { "content-type": normalizedContentType } : {}
641
+ },
642
+ body
643
+ });
644
+ if (!response.ok) {
645
+ const errorBody = await response.text().catch(() => "");
646
+ const detail = errorBody ? ` ${errorBody}` : "";
647
+ throw new Error(`S3 PUT failed (${response.status} ${response.statusText}).${detail}`);
648
+ }
649
+ }
650
+ async function createPresignedGetUrl({
651
+ accessKeyId,
652
+ secretAccessKey,
653
+ endpoint,
654
+ region,
655
+ bucket,
656
+ key,
657
+ expiresInSeconds = 3600
658
+ }) {
659
+ const resolvedEndpoint = normalizeEndpoint(endpoint);
660
+ const canonicalUri = buildCanonicalUri(resolvedEndpoint, bucket, key);
661
+ const host = resolvedEndpoint.host;
662
+ const { amzDate, shortDate } = formatAmzDate();
663
+ const credentialScope = buildCredentialScope(shortDate, region);
664
+ const signedHeaders = "host";
665
+ const queryParams = {
666
+ "X-Amz-Algorithm": AWS4_ALGORITHM,
667
+ "X-Amz-Credential": `${accessKeyId}/${credentialScope}`,
668
+ "X-Amz-Date": amzDate,
669
+ "X-Amz-Expires": `${expiresInSeconds}`,
670
+ "X-Amz-SignedHeaders": signedHeaders
671
+ };
672
+ const canonicalQuery = buildCanonicalQuery(queryParams);
673
+ const canonicalRequest = [
674
+ "GET",
675
+ canonicalUri,
676
+ canonicalQuery,
677
+ `host:${host}
678
+ `,
679
+ signedHeaders,
680
+ "UNSIGNED-PAYLOAD"
681
+ ].join("\n");
682
+ const stringToSign = [
683
+ AWS4_ALGORITHM,
684
+ amzDate,
685
+ credentialScope,
686
+ await sha256Hex(canonicalRequest)
687
+ ].join("\n");
688
+ const signature = await signString(secretAccessKey, shortDate, region, stringToSign);
689
+ const queryWithSignature = `${canonicalQuery}&X-Amz-Signature=${signature}`;
690
+ return `${resolvedEndpoint.origin}${canonicalUri}?${queryWithSignature}`;
691
+ }
692
+
693
+ // src/lib/workflow-serialization.ts
694
+ var WORKFLOW_CLASS_REGISTRY = /* @__PURE__ */ Symbol.for("workflow-class-registry");
695
+ function registerWorkflowSerializationClass(classId, cls) {
696
+ const globalObject = globalThis;
697
+ let registry = globalObject[WORKFLOW_CLASS_REGISTRY];
698
+ if (!registry) {
699
+ registry = /* @__PURE__ */ new Map();
700
+ globalObject[WORKFLOW_CLASS_REGISTRY] = registry;
701
+ }
702
+ registry.set(classId, cls);
703
+ const serializableClass = cls;
704
+ if (serializableClass.classId !== classId) {
705
+ Object.defineProperty(cls, "classId", {
706
+ value: classId,
707
+ writable: false,
708
+ enumerable: false,
709
+ configurable: false
710
+ });
711
+ }
712
+ }
713
+
714
+ // src/lib/workflow-storage-client.ts
715
+ var WorkflowStorageClient = class {
716
+ constructor(options = {}) {
717
+ this.accessKeyId = options.accessKeyId;
718
+ this.secretAccessKey = options.secretAccessKey;
719
+ }
720
+ resolveCredentials(input) {
721
+ const accessKeyId = input.accessKeyId ?? this.accessKeyId;
722
+ const secretAccessKey = input.secretAccessKey ?? this.secretAccessKey;
723
+ if (!accessKeyId || !secretAccessKey) {
724
+ throw new Error(
725
+ "Storage credentials are required. Provide accessKeyId/secretAccessKey in WorkflowStorageClient options or in the storage operation input."
726
+ );
727
+ }
728
+ return { accessKeyId, secretAccessKey };
729
+ }
730
+ async putObject(input) {
731
+ const credentials = this.resolveCredentials(input);
732
+ await putObjectToS3({
733
+ accessKeyId: credentials.accessKeyId,
734
+ secretAccessKey: credentials.secretAccessKey,
735
+ endpoint: input.endpoint,
736
+ region: input.region,
737
+ bucket: input.bucket,
738
+ key: input.key,
739
+ body: input.body,
740
+ contentType: input.contentType
741
+ });
742
+ }
743
+ async createPresignedGetUrl(input) {
744
+ const credentials = this.resolveCredentials(input);
745
+ return createPresignedGetUrl({
746
+ accessKeyId: credentials.accessKeyId,
747
+ secretAccessKey: credentials.secretAccessKey,
748
+ endpoint: input.endpoint,
749
+ region: input.region,
750
+ bucket: input.bucket,
751
+ key: input.key,
752
+ expiresInSeconds: input.expiresInSeconds
753
+ });
754
+ }
755
+ static [WORKFLOW_SERIALIZE](instance) {
756
+ return {
757
+ accessKeyId: instance.accessKeyId,
758
+ secretAccessKey: instance.secretAccessKey
759
+ };
760
+ }
761
+ static [WORKFLOW_DESERIALIZE](value) {
762
+ return new this(value);
763
+ }
764
+ };
765
+ WorkflowStorageClient.classId = "WorkflowStorageClient";
766
+ registerWorkflowSerializationClass(WorkflowStorageClient.classId, WorkflowStorageClient);
767
+ function createWorkflowStorageClient(options = {}) {
768
+ return new WorkflowStorageClient(options);
769
+ }
770
+
408
771
  // src/primitives/index.ts
409
772
  var primitives_exports = {};
410
773
  __export(primitives_exports, {
@@ -418,6 +781,12 @@ __export(primitives_exports, {
418
781
  extractTimestampedTranscript: () => extractTimestampedTranscript,
419
782
  fetchTranscriptForAsset: () => fetchTranscriptForAsset,
420
783
  findCaptionTrack: () => findCaptionTrack,
784
+ getHeatmapForAsset: () => getHeatmapForAsset,
785
+ getHeatmapForPlaybackId: () => getHeatmapForPlaybackId,
786
+ getHeatmapForVideo: () => getHeatmapForVideo,
787
+ getHotspotsForAsset: () => getHotspotsForAsset,
788
+ getHotspotsForPlaybackId: () => getHotspotsForPlaybackId,
789
+ getHotspotsForVideo: () => getHotspotsForVideo,
421
790
  getReadyTextTracks: () => getReadyTextTracks,
422
791
  getStoryboardUrl: () => getStoryboardUrl,
423
792
  getThumbnailUrls: () => getThumbnailUrls,
@@ -426,10 +795,166 @@ __export(primitives_exports, {
426
795
  vttTimestampToSeconds: () => vttTimestampToSeconds
427
796
  });
428
797
 
798
+ // src/lib/providers.ts
799
+ import { createAnthropic } from "@ai-sdk/anthropic";
800
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
801
+ import { createOpenAI } from "@ai-sdk/openai";
802
+ var DEFAULT_LANGUAGE_MODELS = {
803
+ openai: "gpt-5.1",
804
+ anthropic: "claude-sonnet-4-5",
805
+ google: "gemini-3-flash-preview"
806
+ };
807
+ var DEFAULT_EMBEDDING_MODELS = {
808
+ openai: "text-embedding-3-small",
809
+ google: "gemini-embedding-001"
810
+ };
811
+ function resolveLanguageModelConfig(options = {}) {
812
+ const provider = options.provider || "openai";
813
+ const modelId = options.model || DEFAULT_LANGUAGE_MODELS[provider];
814
+ return { provider, modelId };
815
+ }
816
+ function resolveEmbeddingModelConfig(options = {}) {
817
+ const provider = options.provider || "openai";
818
+ const modelId = options.model || DEFAULT_EMBEDDING_MODELS[provider];
819
+ return { provider, modelId };
820
+ }
821
+ async function createLanguageModelFromConfig(provider, modelId, credentials) {
822
+ switch (provider) {
823
+ case "openai": {
824
+ const apiKey = await resolveProviderApiKey("openai", credentials);
825
+ const openai = createOpenAI({ apiKey });
826
+ return openai(modelId);
827
+ }
828
+ case "anthropic": {
829
+ const apiKey = await resolveProviderApiKey("anthropic", credentials);
830
+ const anthropic = createAnthropic({ apiKey });
831
+ return anthropic(modelId);
832
+ }
833
+ case "google": {
834
+ const apiKey = await resolveProviderApiKey("google", credentials);
835
+ const google = createGoogleGenerativeAI({ apiKey });
836
+ return google(modelId);
837
+ }
838
+ default: {
839
+ const exhaustiveCheck = provider;
840
+ throw new Error(`Unsupported provider: ${exhaustiveCheck}`);
841
+ }
842
+ }
843
+ }
844
+ async function createEmbeddingModelFromConfig(provider, modelId, credentials) {
845
+ switch (provider) {
846
+ case "openai": {
847
+ const apiKey = await resolveProviderApiKey("openai", credentials);
848
+ const openai = createOpenAI({ apiKey });
849
+ return openai.embedding(modelId);
850
+ }
851
+ case "google": {
852
+ const apiKey = await resolveProviderApiKey("google", credentials);
853
+ const google = createGoogleGenerativeAI({ apiKey });
854
+ return google.textEmbeddingModel(modelId);
855
+ }
856
+ default: {
857
+ const exhaustiveCheck = provider;
858
+ throw new Error(`Unsupported embedding provider: ${exhaustiveCheck}`);
859
+ }
860
+ }
861
+ }
862
+
863
+ // src/lib/client-factory.ts
864
+ async function getMuxClientFromEnv(credentials) {
865
+ return resolveMuxClient(credentials);
866
+ }
867
+ async function getApiKeyFromEnv(provider, credentials) {
868
+ return resolveProviderApiKey(provider, credentials);
869
+ }
870
+
871
+ // src/primitives/heatmap.ts
872
+ async function getHeatmapForAsset(assetId, options = {}) {
873
+ "use step";
874
+ return fetchHeatmap("assets", assetId, options);
875
+ }
876
+ async function getHeatmapForVideo(videoId, options = {}) {
877
+ "use step";
878
+ return fetchHeatmap("videos", videoId, options);
879
+ }
880
+ async function getHeatmapForPlaybackId(playbackId, options = {}) {
881
+ "use step";
882
+ return fetchHeatmap("playback-ids", playbackId, options);
883
+ }
884
+ function transformHeatmapResponse(response) {
885
+ return {
886
+ assetId: response.asset_id,
887
+ videoId: response.video_id,
888
+ playbackId: response.playback_id,
889
+ heatmap: response.heatmap,
890
+ timeframe: response.timeframe
891
+ };
892
+ }
893
+ async function fetchHeatmap(identifierType, id, options) {
894
+ "use step";
895
+ const { timeframe = "[24:hours]", credentials } = options;
896
+ const muxClient = await getMuxClientFromEnv(credentials);
897
+ const mux = await muxClient.createClient();
898
+ const queryParams = new URLSearchParams();
899
+ queryParams.append("timeframe[]", timeframe);
900
+ const path = `/data/v1/engagement/${identifierType}/${id}/heatmap?${queryParams.toString()}`;
901
+ const response = await mux.get(path);
902
+ return transformHeatmapResponse(response);
903
+ }
904
+
905
+ // src/primitives/hotspots.ts
906
+ async function getHotspotsForAsset(assetId, options = {}) {
907
+ "use step";
908
+ const response = await fetchHotspots("assets", assetId, options);
909
+ return response.hotspots;
910
+ }
911
+ async function getHotspotsForVideo(videoId, options = {}) {
912
+ "use step";
913
+ const response = await fetchHotspots("videos", videoId, options);
914
+ return response.hotspots;
915
+ }
916
+ async function getHotspotsForPlaybackId(playbackId, options = {}) {
917
+ "use step";
918
+ const response = await fetchHotspots("playback-ids", playbackId, options);
919
+ return response.hotspots;
920
+ }
921
+ function transformHotspotResponse(response) {
922
+ return {
923
+ assetId: response.data.asset_id,
924
+ videoId: response.data.video_id,
925
+ playbackId: response.data.playback_id,
926
+ hotspots: response.data.hotspots.map((h) => ({
927
+ startMs: h.start_ms,
928
+ endMs: h.end_ms,
929
+ score: h.score
930
+ }))
931
+ };
932
+ }
933
+ async function fetchHotspots(identifierType, id, options) {
934
+ "use step";
935
+ const {
936
+ limit = 5,
937
+ orderDirection = "desc",
938
+ orderBy = "score",
939
+ timeframe = "[24:hours]",
940
+ credentials
941
+ } = options;
942
+ const muxClient = await getMuxClientFromEnv(credentials);
943
+ const mux = await muxClient.createClient();
944
+ const queryParams = new URLSearchParams();
945
+ queryParams.append("limit", String(limit));
946
+ queryParams.append("order_direction", orderDirection);
947
+ queryParams.append("order_by", orderBy);
948
+ queryParams.append("timeframe[]", timeframe);
949
+ const path = `/data/v1/engagement/${identifierType}/${id}/hotspots?${queryParams.toString()}`;
950
+ const response = await mux.get(path);
951
+ return transformHotspotResponse(response);
952
+ }
953
+
429
954
  // src/lib/url-signing.ts
430
- import Mux from "@mux/mux-node";
431
- function createSigningClient(context) {
432
- return new Mux({
955
+ async function createSigningClient(context) {
956
+ const { default: MuxClient } = await import("@mux/mux-node");
957
+ return new MuxClient({
433
958
  // These are not needed for signing, but the SDK requires them
434
959
  // Using empty strings as we only need the jwt functionality
435
960
  tokenId: env_default.MUX_TOKEN_ID || "",
@@ -440,7 +965,7 @@ function createSigningClient(context) {
440
965
  }
441
966
  async function signPlaybackId(playbackId, context, type = "video", params) {
442
967
  "use step";
443
- const client = createSigningClient(context);
968
+ const client = await createSigningClient(context);
444
969
  const stringParams = params ? Object.fromEntries(
445
970
  Object.entries(params).map(([key, value]) => [key, String(value)])
446
971
  ) : void 0;
@@ -455,7 +980,7 @@ async function signUrl(url, playbackId, context, type = "video", params, credent
455
980
  const resolvedContext = context ?? await resolveMuxSigningContext(credentials);
456
981
  if (!resolvedContext) {
457
982
  throw new Error(
458
- "Signed playback ID requires signing credentials. Provide muxSigningKey and muxPrivateKey in credentials or set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
983
+ "Signed playback ID requires signing credentials. Provide muxSigningKey and muxPrivateKey via workflow credentials or set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
459
984
  );
460
985
  }
461
986
  const token = await signPlaybackId(playbackId, resolvedContext, type, params);
@@ -906,82 +1431,6 @@ async function downloadImagesAsBase64(urls, options = {}, maxConcurrent = 5) {
906
1431
  return results;
907
1432
  }
908
1433
 
909
- // src/lib/mux-assets.ts
910
- import Mux2 from "@mux/mux-node";
911
-
912
- // src/lib/providers.ts
913
- import { createAnthropic } from "@ai-sdk/anthropic";
914
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
915
- import { createOpenAI } from "@ai-sdk/openai";
916
- var DEFAULT_LANGUAGE_MODELS = {
917
- openai: "gpt-5.1",
918
- anthropic: "claude-sonnet-4-5",
919
- google: "gemini-3-flash-preview"
920
- };
921
- var DEFAULT_EMBEDDING_MODELS = {
922
- openai: "text-embedding-3-small",
923
- google: "gemini-embedding-001"
924
- };
925
- function resolveLanguageModelConfig(options = {}) {
926
- const provider = options.provider || "openai";
927
- const modelId = options.model || DEFAULT_LANGUAGE_MODELS[provider];
928
- return { provider, modelId };
929
- }
930
- function resolveEmbeddingModelConfig(options = {}) {
931
- const provider = options.provider || "openai";
932
- const modelId = options.model || DEFAULT_EMBEDDING_MODELS[provider];
933
- return { provider, modelId };
934
- }
935
- async function createLanguageModelFromConfig(provider, modelId, credentials) {
936
- switch (provider) {
937
- case "openai": {
938
- const apiKey = await resolveProviderApiKey("openai", credentials);
939
- const openai = createOpenAI({ apiKey });
940
- return openai(modelId);
941
- }
942
- case "anthropic": {
943
- const apiKey = await resolveProviderApiKey("anthropic", credentials);
944
- const anthropic = createAnthropic({ apiKey });
945
- return anthropic(modelId);
946
- }
947
- case "google": {
948
- const apiKey = await resolveProviderApiKey("google", credentials);
949
- const google = createGoogleGenerativeAI({ apiKey });
950
- return google(modelId);
951
- }
952
- default: {
953
- const exhaustiveCheck = provider;
954
- throw new Error(`Unsupported provider: ${exhaustiveCheck}`);
955
- }
956
- }
957
- }
958
- async function createEmbeddingModelFromConfig(provider, modelId, credentials) {
959
- switch (provider) {
960
- case "openai": {
961
- const apiKey = await resolveProviderApiKey("openai", credentials);
962
- const openai = createOpenAI({ apiKey });
963
- return openai.embedding(modelId);
964
- }
965
- case "google": {
966
- const apiKey = await resolveProviderApiKey("google", credentials);
967
- const google = createGoogleGenerativeAI({ apiKey });
968
- return google.textEmbeddingModel(modelId);
969
- }
970
- default: {
971
- const exhaustiveCheck = provider;
972
- throw new Error(`Unsupported embedding provider: ${exhaustiveCheck}`);
973
- }
974
- }
975
- }
976
-
977
- // src/lib/client-factory.ts
978
- async function getMuxCredentialsFromEnv(credentials) {
979
- return resolveMuxCredentials(credentials);
980
- }
981
- async function getApiKeyFromEnv(provider, credentials) {
982
- return resolveProviderApiKey(provider, credentials);
983
- }
984
-
985
1434
  // src/lib/mux-assets.ts
986
1435
  function getPlaybackId(asset) {
987
1436
  const playbackIds = asset.playback_ids || [];
@@ -1002,25 +1451,35 @@ function isAudioOnlyAsset(asset) {
1002
1451
  const hasVideoTrack = asset.tracks?.some((track) => track.type === "video") ?? false;
1003
1452
  return hasAudioTrack && !hasVideoTrack;
1004
1453
  }
1454
+ function toPlaybackAsset(asset) {
1455
+ const { id: playbackId, policy } = getPlaybackId(asset);
1456
+ return { asset, playbackId, policy };
1457
+ }
1005
1458
  async function getPlaybackIdForAsset(assetId, credentials) {
1006
1459
  "use step";
1007
1460
  const asset = await getMuxAsset(assetId, credentials);
1008
- const { id: playbackId, policy } = getPlaybackId(asset);
1009
- return { asset, playbackId, policy };
1461
+ return toPlaybackAsset(asset);
1010
1462
  }
1011
1463
  async function getMuxAsset(assetId, credentials) {
1012
1464
  "use step";
1013
- const { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
1014
- const mux = new Mux2({
1015
- tokenId: muxTokenId,
1016
- tokenSecret: muxTokenSecret
1017
- });
1465
+ const muxClient = await getMuxClientFromEnv(credentials);
1466
+ const mux = await muxClient.createClient();
1018
1467
  return mux.video.assets.retrieve(assetId);
1019
1468
  }
1020
1469
  function getAssetDurationSecondsFromAsset(asset) {
1021
1470
  const duration = asset.duration;
1022
1471
  return typeof duration === "number" && Number.isFinite(duration) ? duration : void 0;
1023
1472
  }
1473
+ function getVideoTrackDurationSecondsFromAsset(asset) {
1474
+ const videoTrack = asset.tracks?.find((track) => track.type === "video");
1475
+ const duration = videoTrack?.duration;
1476
+ return typeof duration === "number" && Number.isFinite(duration) ? duration : void 0;
1477
+ }
1478
+ function getVideoTrackMaxFrameRateFromAsset(asset) {
1479
+ const videoTrack = asset.tracks?.find((track) => track.type === "video");
1480
+ const maxFrameRate = videoTrack?.max_frame_rate;
1481
+ return typeof maxFrameRate === "number" && Number.isFinite(maxFrameRate) && maxFrameRate > 0 ? maxFrameRate : void 0;
1482
+ }
1024
1483
 
1025
1484
  // src/lib/prompt-builder.ts
1026
1485
  function renderSection(section) {
@@ -1560,10 +2019,7 @@ async function hasBurnedInCaptions(assetId, options = {}) {
1560
2019
  model,
1561
2020
  provider
1562
2021
  });
1563
- const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
1564
- assetId,
1565
- credentials
1566
- );
2022
+ const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
1567
2023
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
1568
2024
  const imageUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", credentials);
1569
2025
  let analysisResponse;
@@ -1761,10 +2217,7 @@ async function generateChapters(assetId, languageCode, options = {}) {
1761
2217
  model,
1762
2218
  provider
1763
2219
  });
1764
- const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
1765
- assetId,
1766
- credentials
1767
- );
2220
+ const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
1768
2221
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
1769
2222
  const isAudioOnly = isAudioOnlyAsset(assetData);
1770
2223
  const signingContext = await resolveMuxSigningContext(credentials);
@@ -2003,6 +2456,66 @@ async function generateVideoEmbeddings(assetId, options = {}) {
2003
2456
  return generateEmbeddingsInternal(assetId, options);
2004
2457
  }
2005
2458
 
2459
+ // src/lib/sampling-plan.ts
2460
+ var DEFAULT_FPS = 30;
2461
+ function roundToNearestFrameMs(tsMs, fps = DEFAULT_FPS) {
2462
+ const frameMs = 1e3 / fps;
2463
+ return Math.round(Math.round(tsMs / frameMs) * frameMs * 100) / 100;
2464
+ }
2465
+ function planSamplingTimestamps(options) {
2466
+ const DEFAULT_MIN_CANDIDATES = 10;
2467
+ const DEFAULT_MAX_CANDIDATES = 30;
2468
+ const {
2469
+ duration_sec,
2470
+ min_candidates = DEFAULT_MIN_CANDIDATES,
2471
+ max_candidates = DEFAULT_MAX_CANDIDATES,
2472
+ trim_start_sec = 1,
2473
+ trim_end_sec = 1,
2474
+ fps = DEFAULT_FPS,
2475
+ base_cadence_hz,
2476
+ anchor_percents = [0.2, 0.5, 0.8],
2477
+ anchor_window_sec = 1.5
2478
+ } = options;
2479
+ const usableSec = Math.max(0, duration_sec - (trim_start_sec + trim_end_sec));
2480
+ if (usableSec <= 0)
2481
+ return [];
2482
+ const cadenceHz = base_cadence_hz ?? (duration_sec < 15 ? 3 : duration_sec < 60 ? 2 : duration_sec < 180 ? 1.5 : 1);
2483
+ let target = Math.round(usableSec * cadenceHz);
2484
+ target = Math.max(min_candidates, Math.min(max_candidates, target));
2485
+ const stepSec = usableSec / target;
2486
+ const t0 = trim_start_sec;
2487
+ const base = [];
2488
+ for (let i = 0; i < target; i++) {
2489
+ const tsSec = t0 + (i + 0.5) * stepSec;
2490
+ base.push(tsSec * 1e3);
2491
+ }
2492
+ const slack = Math.max(0, max_candidates - base.length);
2493
+ const extra = [];
2494
+ if (slack > 0 && anchor_percents.length > 0) {
2495
+ const perAnchor = Math.max(1, Math.min(5, Math.floor(slack / anchor_percents.length)));
2496
+ for (const p of anchor_percents) {
2497
+ const centerSec = Math.min(
2498
+ t0 + usableSec - 1e-3,
2499
+ // nudge just inside the end bound
2500
+ Math.max(t0 + 1e-3, duration_sec * p)
2501
+ // nudge just inside the start bound
2502
+ );
2503
+ const startSec = Math.max(t0, centerSec - anchor_window_sec / 2);
2504
+ const endSec = Math.min(t0 + usableSec, centerSec + anchor_window_sec / 2);
2505
+ if (endSec <= startSec)
2506
+ continue;
2507
+ const wStep = (endSec - startSec) / perAnchor;
2508
+ for (let i = 0; i < perAnchor; i++) {
2509
+ const tsSec = startSec + (i + 0.5) * wStep;
2510
+ extra.push(tsSec * 1e3);
2511
+ }
2512
+ }
2513
+ }
2514
+ const all = base.concat(extra).map((ms) => roundToNearestFrameMs(ms, fps)).filter((ms) => ms >= trim_start_sec * 1e3 && ms <= (duration_sec - trim_end_sec) * 1e3);
2515
+ const uniqSorted = Array.from(new Set(all)).sort((a, b) => a - b);
2516
+ return uniqSorted.slice(0, max_candidates);
2517
+ }
2518
+
2006
2519
  // src/workflows/moderation.ts
2007
2520
  var DEFAULT_THRESHOLDS = {
2008
2521
  sexual: 0.7,
@@ -2014,31 +2527,19 @@ var HIVE_SEXUAL_CATEGORIES = [
2014
2527
  "general_nsfw",
2015
2528
  "general_suggestive",
2016
2529
  "yes_sexual_activity",
2017
- "female_underwear",
2018
- "male_underwear",
2019
- "bra",
2020
- "panties",
2021
2530
  "sex_toys",
2022
2531
  "nudity_female",
2023
- "nudity_male",
2024
- "cleavage",
2025
- "swimwear"
2532
+ "nudity_male"
2026
2533
  ];
2027
2534
  var HIVE_VIOLENCE_CATEGORIES = [
2028
2535
  "gun_in_hand",
2029
2536
  "gun_not_in_hand",
2030
- "animated_gun",
2031
2537
  "knife_in_hand",
2032
- "knife_not_in_hand",
2033
- "culinary_knife_not_in_hand",
2034
- "culinary_knife_in_hand",
2035
2538
  "very_bloody",
2036
- "a_little_bloody",
2037
2539
  "other_blood",
2038
2540
  "hanging",
2039
2541
  "noose",
2040
2542
  "human_corpse",
2041
- "animated_corpse",
2042
2543
  "emaciated_body",
2043
2544
  "self_harm",
2044
2545
  "animal_abuse",
@@ -2097,7 +2598,8 @@ async function moderateImageWithOpenAI(entry) {
2097
2598
  url: entry.url,
2098
2599
  sexual: 0,
2099
2600
  violence: 0,
2100
- error: true
2601
+ error: true,
2602
+ errorMessage: error instanceof Error ? error.message : String(error)
2101
2603
  };
2102
2604
  }
2103
2605
  }
@@ -2142,7 +2644,8 @@ async function requestOpenAITextModeration(text, model, url, credentials) {
2142
2644
  url,
2143
2645
  sexual: 0,
2144
2646
  violence: 0,
2145
- error: true
2647
+ error: true,
2648
+ errorMessage: error instanceof Error ? error.message : String(error)
2146
2649
  };
2147
2650
  }
2148
2651
  }
@@ -2167,7 +2670,7 @@ async function requestOpenAITranscriptModeration(transcriptText, model, maxConcu
2167
2670
  const chunks = chunkTextByUtf16CodeUnits(transcriptText, 1e4);
2168
2671
  if (!chunks.length) {
2169
2672
  return [
2170
- { url: "transcript:0", sexual: 0, violence: 0, error: true }
2673
+ { url: "transcript:0", sexual: 0, violence: 0, error: true, errorMessage: "No transcript chunks to moderate" }
2171
2674
  ];
2172
2675
  }
2173
2676
  const targets = chunks.map((chunk, idx) => ({
@@ -2201,34 +2704,59 @@ async function moderateImageWithHive(entry) {
2201
2704
  });
2202
2705
  formData.append("media", blob, `thumbnail.${extension}`);
2203
2706
  }
2204
- const res = await fetch(HIVE_ENDPOINT, {
2205
- method: "POST",
2206
- headers: {
2207
- Accept: "application/json",
2208
- Authorization: `Token ${apiKey}`
2209
- },
2210
- body: formData
2211
- });
2707
+ const controller = new AbortController();
2708
+ const timeout = setTimeout(() => controller.abort(), 15e3);
2709
+ let res;
2710
+ try {
2711
+ res = await fetch(HIVE_ENDPOINT, {
2712
+ method: "POST",
2713
+ headers: {
2714
+ Accept: "application/json",
2715
+ Authorization: `Token ${apiKey}`
2716
+ },
2717
+ body: formData,
2718
+ signal: controller.signal
2719
+ });
2720
+ } catch (err) {
2721
+ if (err?.name === "AbortError") {
2722
+ throw new Error("Hive request timed out after 15s");
2723
+ }
2724
+ throw err;
2725
+ } finally {
2726
+ clearTimeout(timeout);
2727
+ }
2212
2728
  const json = await res.json().catch(() => void 0);
2213
2729
  if (!res.ok) {
2214
2730
  throw new Error(
2215
2731
  `Hive moderation error: ${res.status} ${res.statusText} - ${JSON.stringify(json)}`
2216
2732
  );
2217
2733
  }
2218
- const classes = json?.status?.[0]?.response?.output?.[0]?.classes || [];
2734
+ if (json?.return_code != null && json.return_code !== 0) {
2735
+ throw new Error(
2736
+ `Hive API error (return_code ${json.return_code}): ${json.message || "Unknown error"}`
2737
+ );
2738
+ }
2739
+ const classes = json?.status?.[0]?.response?.output?.[0]?.classes;
2740
+ if (!Array.isArray(classes)) {
2741
+ throw new TypeError(
2742
+ `Unexpected Hive response structure: ${JSON.stringify(json)}`
2743
+ );
2744
+ }
2745
+ const sexual = getHiveCategoryScores(classes, HIVE_SEXUAL_CATEGORIES);
2746
+ const violence = getHiveCategoryScores(classes, HIVE_VIOLENCE_CATEGORIES);
2219
2747
  return {
2220
2748
  url: entry.url,
2221
- sexual: getHiveCategoryScores(classes, HIVE_SEXUAL_CATEGORIES),
2222
- violence: getHiveCategoryScores(classes, HIVE_VIOLENCE_CATEGORIES),
2749
+ sexual,
2750
+ violence,
2223
2751
  error: false
2224
2752
  };
2225
2753
  } catch (error) {
2226
- console.error("Hive moderation failed:", error);
2227
2754
  return {
2228
2755
  url: entry.url,
2229
2756
  sexual: 0,
2230
2757
  violence: 0,
2231
- error: true
2758
+ error: true,
2759
+ errorMessage: error instanceof Error ? error.message : String(error)
2232
2760
  };
2233
2761
  }
2234
2762
  }
@@ -2247,7 +2775,20 @@ async function requestHiveModeration(imageUrls, maxConcurrent = 5, submissionMod
2247
2775
  source: { kind: "url", value: url },
2248
2776
  credentials
2249
2777
  }));
2250
- return processConcurrently(targets, moderateImageWithHive, maxConcurrent);
2778
+ return await processConcurrently(targets, moderateImageWithHive, maxConcurrent);
2779
+ }
2780
+ async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options) {
2781
+ "use step";
2782
+ const { width, shouldSign, credentials } = options;
2783
+ const baseUrl = `https://image.mux.com/${playbackId}/thumbnail.png`;
2784
+ const urlPromises = timestampsMs.map(async (tsMs) => {
2785
+ const time = Number((tsMs / 1e3).toFixed(2));
2786
+ if (shouldSign) {
2787
+ return signUrl(baseUrl, playbackId, void 0, "thumbnail", { time, width }, credentials);
2788
+ }
2789
+ return `${baseUrl}?time=${time}&width=${width}`;
2790
+ });
2791
+ return Promise.all(urlPromises);
2251
2792
  }
2252
2793
  async function getModerationScores(assetId, options = {}) {
2253
2794
  "use workflow";
@@ -2262,11 +2803,17 @@ async function getModerationScores(assetId, options = {}) {
2262
2803
  maxConcurrent = 5,
2263
2804
  imageSubmissionMode = "url",
2264
2805
  imageDownloadOptions,
2265
- credentials
2806
+ credentials: providedCredentials
2266
2807
  } = options;
2808
+ const credentials = providedCredentials;
2267
2809
  const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
2810
+ const videoTrackDurationSeconds = getVideoTrackDurationSecondsFromAsset(asset);
2811
+ const videoTrackFps = getVideoTrackMaxFrameRateFromAsset(asset);
2268
2812
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
2269
- const duration = assetDurationSeconds ?? 0;
2813
+ const candidateDurations = [videoTrackDurationSeconds, assetDurationSeconds].filter(
2814
+ (d) => d != null
2815
+ );
2816
+ const duration = candidateDurations.length > 0 ? Math.min(...candidateDurations) : 0;
2270
2817
  const isAudioOnly = isAudioOnlyAsset(asset);
2271
2818
  const signingContext = await resolveMuxSigningContext(credentials);
2272
2819
  if (policy === "signed" && !signingContext) {
@@ -2308,13 +2855,35 @@ async function getModerationScores(assetId, options = {}) {
2308
2855
  throw new Error(`Unsupported moderation provider: ${provider}`);
2309
2856
  }
2310
2857
  } else {
2311
- const thumbnailUrls = await getThumbnailUrls(playbackId, duration, {
2312
- interval: thumbnailInterval,
2313
- width: thumbnailWidth,
2314
- shouldSign: policy === "signed",
2315
- maxSamples,
2316
- credentials
2317
- });
2858
+ const thumbnailUrls = maxSamples === void 0 ? (
2859
+ // Generate thumbnail URLs (signed if needed) using existing interval-based logic.
2860
+ await getThumbnailUrls(playbackId, duration, {
2861
+ interval: thumbnailInterval,
2862
+ width: thumbnailWidth,
2863
+ shouldSign: policy === "signed",
2864
+ credentials
2865
+ })
2866
+ ) : (
2867
+ // In maxSamples mode, sample valid timestamps over the trimmed usable span.
2868
+ // Use proportional trims (≈ duration/6, capped at 5s) to stay well inside the
2869
+ // renderable range — Mux can't always serve thumbnails at the very edges.
2870
+ await getThumbnailUrlsFromTimestamps(
2871
+ playbackId,
2872
+ planSamplingTimestamps({
2873
+ duration_sec: duration,
2874
+ max_candidates: maxSamples,
2875
+ trim_start_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
2876
+ trim_end_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
2877
+ fps: videoTrackFps,
2878
+ base_cadence_hz: thumbnailInterval > 0 ? 1 / thumbnailInterval : void 0
2879
+ }),
2880
+ {
2881
+ width: thumbnailWidth,
2882
+ shouldSign: policy === "signed",
2883
+ credentials
2884
+ }
2885
+ )
2886
+ );
2318
2887
  thumbnailCount = thumbnailUrls.length;
2319
2888
  if (provider === "openai") {
2320
2889
  thumbnailScores = await requestOpenAIModeration(
@@ -2337,6 +2906,13 @@ async function getModerationScores(assetId, options = {}) {
2337
2906
  throw new Error(`Unsupported moderation provider: ${provider}`);
2338
2907
  }
2339
2908
  }
2909
+ const failed = thumbnailScores.filter((s) => s.error);
2910
+ if (failed.length > 0) {
2911
+ const details = failed.map((s) => `${s.url}: ${s.errorMessage || "Unknown error"}`).join("; ");
2912
+ throw new Error(
2913
+ `Moderation failed for ${failed.length}/${thumbnailScores.length} thumbnail(s): ${details}`
2914
+ );
2915
+ }
2340
2916
  const maxSexual = Math.max(...thumbnailScores.map((s) => s.sexual));
2341
2917
  const maxViolence = Math.max(...thumbnailScores.map((s) => s.violence));
2342
2918
  const finalThresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };
@@ -2347,7 +2923,7 @@ async function getModerationScores(assetId, options = {}) {
2347
2923
  thumbnailScores,
2348
2924
  usage: {
2349
2925
  metadata: {
2350
- assetDurationSeconds,
2926
+ assetDurationSeconds: duration,
2351
2927
  ...thumbnailCount === void 0 ? {} : { thumbnailCount }
2352
2928
  }
2353
2929
  },
@@ -2708,7 +3284,8 @@ async function getSummaryAndTags(assetId, options) {
2708
3284
  model,
2709
3285
  provider
2710
3286
  });
2711
- const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3287
+ const workflowCredentials = credentials;
3288
+ const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, workflowCredentials);
2712
3289
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
2713
3290
  const isAudioOnly = isAudioOnlyAsset(assetData);
2714
3291
  if (isAudioOnly && !includeTranscript) {
@@ -2716,7 +3293,7 @@ async function getSummaryAndTags(assetId, options) {
2716
3293
  "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles)."
2717
3294
  );
2718
3295
  }
2719
- const signingContext = await resolveMuxSigningContext(credentials);
3296
+ const signingContext = await resolveMuxSigningContext(workflowCredentials);
2720
3297
  if (policy === "signed" && !signingContext) {
2721
3298
  throw new Error(
2722
3299
  "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
@@ -2725,7 +3302,7 @@ async function getSummaryAndTags(assetId, options) {
2725
3302
  const transcriptText = includeTranscript ? (await fetchTranscriptForAsset(assetData, playbackId, {
2726
3303
  cleanTranscript,
2727
3304
  shouldSign: policy === "signed",
2728
- credentials,
3305
+ credentials: workflowCredentials,
2729
3306
  required: isAudioOnly
2730
3307
  })).transcriptText : "";
2731
3308
  const userPrompt = buildUserPrompt4({
@@ -2745,10 +3322,10 @@ async function getSummaryAndTags(assetId, options) {
2745
3322
  modelConfig.modelId,
2746
3323
  userPrompt,
2747
3324
  systemPrompt,
2748
- credentials
3325
+ workflowCredentials
2749
3326
  );
2750
3327
  } else {
2751
- const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", credentials);
3328
+ const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", workflowCredentials);
2752
3329
  imageUrl = storyboardUrl;
2753
3330
  if (imageSubmissionMode === "base64") {
2754
3331
  const downloadResult = await downloadImageAsBase64(storyboardUrl, imageDownloadOptions);
@@ -2758,7 +3335,7 @@ async function getSummaryAndTags(assetId, options) {
2758
3335
  modelConfig.modelId,
2759
3336
  userPrompt,
2760
3337
  systemPrompt,
2761
- credentials
3338
+ workflowCredentials
2762
3339
  );
2763
3340
  } else {
2764
3341
  analysisResponse = await withRetry(() => analyzeStoryboard2(
@@ -2767,7 +3344,7 @@ async function getSummaryAndTags(assetId, options) {
2767
3344
  modelConfig.modelId,
2768
3345
  userPrompt,
2769
3346
  systemPrompt,
2770
- credentials
3347
+ workflowCredentials
2771
3348
  ));
2772
3349
  }
2773
3350
  }
@@ -2803,9 +3380,6 @@ async function getSummaryAndTags(assetId, options) {
2803
3380
  };
2804
3381
  }
2805
3382
 
2806
- // src/workflows/translate-audio.ts
2807
- import Mux3 from "@mux/mux-node";
2808
-
2809
3383
  // src/lib/language-codes.ts
2810
3384
  var ISO639_1_TO_3 = {
2811
3385
  // Major world languages
@@ -2964,6 +3538,48 @@ function getLanguageName(code) {
2964
3538
  }
2965
3539
  }
2966
3540
 
3541
+ // src/lib/storage-adapter.ts
3542
+ function requireCredentials(accessKeyId, secretAccessKey) {
3543
+ if (!accessKeyId || !secretAccessKey) {
3544
+ throw new Error(
3545
+ "S3 credentials are required for default storage operations. Provide S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY or pass options.storageAdapter."
3546
+ );
3547
+ }
3548
+ return { accessKeyId, secretAccessKey };
3549
+ }
3550
+ async function putObjectWithStorageAdapter(input, adapter) {
3551
+ if (adapter) {
3552
+ await adapter.putObject(input);
3553
+ return;
3554
+ }
3555
+ const credentials = requireCredentials(input.accessKeyId, input.secretAccessKey);
3556
+ await putObjectToS3({
3557
+ accessKeyId: credentials.accessKeyId,
3558
+ secretAccessKey: credentials.secretAccessKey,
3559
+ endpoint: input.endpoint,
3560
+ region: input.region,
3561
+ bucket: input.bucket,
3562
+ key: input.key,
3563
+ body: input.body,
3564
+ contentType: input.contentType
3565
+ });
3566
+ }
3567
+ async function createPresignedGetUrlWithStorageAdapter(input, adapter) {
3568
+ if (adapter) {
3569
+ return adapter.createPresignedGetUrl(input);
3570
+ }
3571
+ const credentials = requireCredentials(input.accessKeyId, input.secretAccessKey);
3572
+ return createPresignedGetUrl({
3573
+ accessKeyId: credentials.accessKeyId,
3574
+ secretAccessKey: credentials.secretAccessKey,
3575
+ endpoint: input.endpoint,
3576
+ region: input.region,
3577
+ bucket: input.bucket,
3578
+ key: input.key,
3579
+ expiresInSeconds: input.expiresInSeconds
3580
+ });
3581
+ }
3582
+
2967
3583
  // src/workflows/translate-audio.ts
2968
3584
  var STATIC_RENDITION_POLL_INTERVAL_MS = 5e3;
2969
3585
  var STATIC_RENDITION_MAX_ATTEMPTS = 36;
@@ -2983,11 +3599,8 @@ function getReadyAudioStaticRendition(asset) {
2983
3599
  var hasReadyAudioStaticRendition = (asset) => Boolean(getReadyAudioStaticRendition(asset));
2984
3600
  async function requestStaticRenditionCreation(assetId, credentials) {
2985
3601
  "use step";
2986
- const { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
2987
- const mux = new Mux3({
2988
- tokenId: muxTokenId,
2989
- tokenSecret: muxTokenSecret
2990
- });
3602
+ const muxClient = await resolveMuxClient(credentials);
3603
+ const mux = await muxClient.createClient();
2991
3604
  try {
2992
3605
  await mux.video.assets.createStaticRendition(assetId, {
2993
3606
  resolution: "audio-only"
@@ -3009,11 +3622,8 @@ async function waitForAudioStaticRendition({
3009
3622
  credentials
3010
3623
  }) {
3011
3624
  "use step";
3012
- const { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
3013
- const mux = new Mux3({
3014
- tokenId: muxTokenId,
3015
- tokenSecret: muxTokenSecret
3016
- });
3625
+ const muxClient = await resolveMuxClient(credentials);
3626
+ const mux = await muxClient.createClient();
3017
3627
  let currentAsset = initialAsset;
3018
3628
  if (hasReadyAudioStaticRendition(currentAsset)) {
3019
3629
  return currentAsset;
@@ -3126,53 +3736,40 @@ async function uploadDubbedAudioToS3({
3126
3736
  toLanguageCode,
3127
3737
  s3Endpoint,
3128
3738
  s3Region,
3129
- s3Bucket
3739
+ s3Bucket,
3740
+ storageAdapter
3130
3741
  }) {
3131
3742
  "use step";
3132
- const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
3133
- const { Upload } = await import("@aws-sdk/lib-storage");
3134
- const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
3135
3743
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
3136
3744
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
3137
- const s3Client = new S3Client({
3745
+ const audioKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.m4a`;
3746
+ await putObjectWithStorageAdapter({
3747
+ accessKeyId: s3AccessKeyId,
3748
+ secretAccessKey: s3SecretAccessKey,
3749
+ endpoint: s3Endpoint,
3138
3750
  region: s3Region,
3751
+ bucket: s3Bucket,
3752
+ key: audioKey,
3753
+ body: new Uint8Array(dubbedAudioBuffer),
3754
+ contentType: "audio/mp4"
3755
+ }, storageAdapter);
3756
+ const presignedUrl = await createPresignedGetUrlWithStorageAdapter({
3757
+ accessKeyId: s3AccessKeyId,
3758
+ secretAccessKey: s3SecretAccessKey,
3139
3759
  endpoint: s3Endpoint,
3140
- credentials: {
3141
- accessKeyId: s3AccessKeyId,
3142
- secretAccessKey: s3SecretAccessKey
3143
- },
3144
- forcePathStyle: true
3145
- });
3146
- const audioKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.m4a`;
3147
- const upload = new Upload({
3148
- client: s3Client,
3149
- params: {
3150
- Bucket: s3Bucket,
3151
- Key: audioKey,
3152
- Body: new Uint8Array(dubbedAudioBuffer),
3153
- ContentType: "audio/mp4"
3154
- }
3155
- });
3156
- await upload.done();
3157
- const getObjectCommand = new GetObjectCommand({
3158
- Bucket: s3Bucket,
3159
- Key: audioKey
3160
- });
3161
- const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, {
3162
- expiresIn: 3600
3163
- // 1 hour
3164
- });
3760
+ region: s3Region,
3761
+ bucket: s3Bucket,
3762
+ key: audioKey,
3763
+ expiresInSeconds: 3600
3764
+ }, storageAdapter);
3165
3765
  console.warn(`\u2705 Audio uploaded successfully to: ${audioKey}`);
3166
3766
  console.warn(`\u{1F517} Generated presigned URL (expires in 1 hour)`);
3167
3767
  return presignedUrl;
3168
3768
  }
3169
3769
  async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
3170
3770
  "use step";
3171
- const { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
3172
- const mux = new Mux3({
3173
- tokenId: muxTokenId,
3174
- tokenSecret: muxTokenSecret
3175
- });
3771
+ const muxClient = await resolveMuxClient(credentials);
3772
+ const mux = await muxClient.createClient();
3176
3773
  const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
3177
3774
  const trackName = `${languageName} (auto-dubbed)`;
3178
3775
  const trackResponse = await mux.video.assets.createTrack(assetId, {
@@ -3192,34 +3789,24 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
3192
3789
  provider = "elevenlabs",
3193
3790
  numSpeakers = 0,
3194
3791
  // 0 = auto-detect
3195
- elevenLabsApiKey,
3196
3792
  uploadToMux = true,
3793
+ storageAdapter,
3197
3794
  credentials: providedCredentials
3198
3795
  } = options;
3199
3796
  if (provider !== "elevenlabs") {
3200
3797
  throw new Error("Only ElevenLabs provider is currently supported for audio translation");
3201
3798
  }
3202
- let credentials;
3203
- if (isEncryptedPayload(providedCredentials)) {
3204
- credentials = providedCredentials;
3205
- } else if (providedCredentials || elevenLabsApiKey) {
3206
- credentials = {
3207
- ...providedCredentials ?? {},
3208
- ...elevenLabsApiKey ? { elevenLabsApiKey } : {}
3209
- };
3210
- }
3799
+ const credentials = providedCredentials;
3800
+ const effectiveStorageAdapter = storageAdapter;
3211
3801
  const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
3212
3802
  const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
3213
3803
  const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
3214
3804
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
3215
3805
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
3216
- if (uploadToMux && (!s3Endpoint || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey)) {
3217
- throw new Error("S3 configuration is required for uploading to Mux. Provide s3Endpoint, s3Bucket, s3AccessKeyId, and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
3806
+ if (uploadToMux && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
3807
+ throw new Error("Storage configuration is required for uploading to Mux. Provide s3Endpoint and s3Bucket. If no storageAdapter is supplied, also provide s3AccessKeyId and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
3218
3808
  }
3219
- const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(
3220
- assetId,
3221
- credentials
3222
- );
3809
+ const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3223
3810
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
3224
3811
  let currentAsset = initialAsset;
3225
3812
  if (!hasReadyAudioStaticRendition(currentAsset)) {
@@ -3333,7 +3920,8 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
3333
3920
  toLanguageCode,
3334
3921
  s3Endpoint,
3335
3922
  s3Region,
3336
- s3Bucket
3923
+ s3Bucket,
3924
+ storageAdapter: effectiveStorageAdapter
3337
3925
  });
3338
3926
  } catch (error) {
3339
3927
  throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -3369,7 +3957,6 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
3369
3957
  }
3370
3958
 
3371
3959
  // src/workflows/translate-captions.ts
3372
- import Mux4 from "@mux/mux-node";
3373
3960
  import { generateText as generateText5, Output as Output5 } from "ai";
3374
3961
  import { z as z6 } from "zod";
3375
3962
  var translationSchema = z6.object({
@@ -3423,51 +4010,37 @@ async function uploadVttToS3({
3423
4010
  toLanguageCode,
3424
4011
  s3Endpoint,
3425
4012
  s3Region,
3426
- s3Bucket
4013
+ s3Bucket,
4014
+ storageAdapter
3427
4015
  }) {
3428
4016
  "use step";
3429
- const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
3430
- const { Upload } = await import("@aws-sdk/lib-storage");
3431
- const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
3432
4017
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
3433
4018
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
3434
- const s3Client = new S3Client({
4019
+ const vttKey = `translations/${assetId}/${fromLanguageCode}-to-${toLanguageCode}-${Date.now()}.vtt`;
4020
+ await putObjectWithStorageAdapter({
4021
+ accessKeyId: s3AccessKeyId,
4022
+ secretAccessKey: s3SecretAccessKey,
4023
+ endpoint: s3Endpoint,
3435
4024
  region: s3Region,
4025
+ bucket: s3Bucket,
4026
+ key: vttKey,
4027
+ body: translatedVtt,
4028
+ contentType: "text/vtt"
4029
+ }, storageAdapter);
4030
+ return createPresignedGetUrlWithStorageAdapter({
4031
+ accessKeyId: s3AccessKeyId,
4032
+ secretAccessKey: s3SecretAccessKey,
3436
4033
  endpoint: s3Endpoint,
3437
- credentials: {
3438
- accessKeyId: s3AccessKeyId,
3439
- secretAccessKey: s3SecretAccessKey
3440
- },
3441
- forcePathStyle: true
3442
- });
3443
- const vttKey = `translations/${assetId}/${fromLanguageCode}-to-${toLanguageCode}-${Date.now()}.vtt`;
3444
- const upload = new Upload({
3445
- client: s3Client,
3446
- params: {
3447
- Bucket: s3Bucket,
3448
- Key: vttKey,
3449
- Body: translatedVtt,
3450
- ContentType: "text/vtt"
3451
- }
3452
- });
3453
- await upload.done();
3454
- const getObjectCommand = new GetObjectCommand({
3455
- Bucket: s3Bucket,
3456
- Key: vttKey
3457
- });
3458
- const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, {
3459
- expiresIn: 3600
3460
- // 1 hour
3461
- });
3462
- return presignedUrl;
4034
+ region: s3Region,
4035
+ bucket: s3Bucket,
4036
+ key: vttKey,
4037
+ expiresInSeconds: 3600
4038
+ }, storageAdapter);
3463
4039
  }
3464
4040
  async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
3465
4041
  "use step";
3466
- const { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
3467
- const mux = new Mux4({
3468
- tokenId: muxTokenId,
3469
- tokenSecret: muxTokenSecret
3470
- });
4042
+ const muxClient = await resolveMuxClient(credentials);
4043
+ const mux = await muxClient.createClient();
3471
4044
  const trackResponse = await mux.video.assets.createTrack(assetId, {
3472
4045
  type: "text",
3473
4046
  text_type: "subtitles",
@@ -3489,8 +4062,11 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
3489
4062
  s3Region: providedS3Region,
3490
4063
  s3Bucket: providedS3Bucket,
3491
4064
  uploadToMux: uploadToMuxOption,
3492
- credentials
4065
+ storageAdapter,
4066
+ credentials: providedCredentials
3493
4067
  } = options;
4068
+ const credentials = providedCredentials;
4069
+ const effectiveStorageAdapter = storageAdapter;
3494
4070
  const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
3495
4071
  const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
3496
4072
  const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
@@ -3502,13 +4078,10 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
3502
4078
  model,
3503
4079
  provider
3504
4080
  });
3505
- if (uploadToMux && (!s3Endpoint || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey)) {
3506
- throw new Error("S3 configuration is required for uploading to Mux. Provide s3Endpoint, s3Bucket, s3AccessKeyId, and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
4081
+ if (uploadToMux && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
4082
+ throw new Error("Storage configuration is required for uploading to Mux. Provide s3Endpoint and s3Bucket. If no storageAdapter is supplied, also provide s3AccessKeyId and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
3507
4083
  }
3508
- const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
3509
- assetId,
3510
- credentials
3511
- );
4084
+ const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3512
4085
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
3513
4086
  const isAudioOnly = isAudioOnlyAsset(assetData);
3514
4087
  const signingContext = await resolveMuxSigningContext(credentials);
@@ -3593,7 +4166,8 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
3593
4166
  toLanguageCode,
3594
4167
  s3Endpoint,
3595
4168
  s3Region,
3596
- s3Bucket
4169
+ s3Bucket,
4170
+ storageAdapter: effectiveStorageAdapter
3597
4171
  });
3598
4172
  } catch (error) {
3599
4173
  throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -3626,6 +4200,8 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
3626
4200
  };
3627
4201
  }
3628
4202
  export {
4203
+ WorkflowStorageClient,
4204
+ createWorkflowStorageClient,
3629
4205
  decryptFromWorkflow,
3630
4206
  encryptForWorkflow,
3631
4207
  primitives_exports as primitives,