@transloadit/node 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,7 +52,7 @@ import type {
52
52
  import { lintAssemblyInstructions as lintAssemblyInstructionsInternal } from './lintAssemblyInstructions.ts'
53
53
  import PaginationStream from './PaginationStream.ts'
54
54
  import PollingTimeoutError from './PollingTimeoutError.ts'
55
- import type { Stream } from './tus.ts'
55
+ import type { Stream, UploadBehavior } from './tus.ts'
56
56
  import { sendTusRequest } from './tus.ts'
57
57
 
58
58
  // See https://github.com/sindresorhus/got/tree/v11.8.6?tab=readme-ov-file#errors
@@ -66,11 +66,30 @@ export {
66
66
  TimeoutError,
67
67
  UploadError,
68
68
  } from 'got'
69
-
69
+ export { goldenTemplates } from './alphalib/goldenTemplates.ts'
70
70
  export type { AssemblyStatus } from './alphalib/types/assemblyStatus.ts'
71
71
  export * from './apiTypes.ts'
72
72
  export { InconsistentResponseError, ApiError }
73
+ export { mergeTemplateContent } from './alphalib/templateMerge.ts'
74
+ export type {
75
+ Base64Strategy,
76
+ InputFile,
77
+ PrepareInputFilesOptions,
78
+ PrepareInputFilesResult,
79
+ UploadInput,
80
+ UrlStrategy,
81
+ } from './inputFiles.ts'
82
+ export { prepareInputFiles } from './inputFiles.ts'
73
83
  export type { LintAssemblyInstructionsResult, LintFatalLevel } from './lintAssemblyInstructions.ts'
84
+ export type {
85
+ RobotHelp,
86
+ RobotHelpOptions,
87
+ RobotListItem,
88
+ RobotListOptions,
89
+ RobotListResult,
90
+ RobotParamHelp,
91
+ } from './robots.ts'
92
+ export { getRobotHelp, listRobots } from './robots.ts'
74
93
 
75
94
  const log = debug('transloadit')
76
95
  const logWarn = debug('transloadit:warn')
@@ -80,6 +99,12 @@ export interface UploadProgress {
80
99
  totalBytes?: number | undefined
81
100
  }
82
101
 
102
+ export type { UploadBehavior }
103
+
104
+ export type AssemblyStatusWithUploadUrls = AssemblyStatus & {
105
+ upload_urls?: Record<string, string>
106
+ }
107
+
83
108
  const { version } = packageJson
84
109
 
85
110
  export type AssemblyProgress = (assembly: AssemblyStatus) => void
@@ -157,6 +182,7 @@ interface AssemblyUploadOptions {
157
182
  uploads?: {
158
183
  [name: string]: Readable | IntoStreamInput
159
184
  }
185
+ uploadBehavior?: UploadBehavior
160
186
  waitForCompletion?: boolean
161
187
  chunkSize?: number
162
188
  uploadConcurrency?: number
@@ -173,6 +199,10 @@ interface AssemblyUploadOptions {
173
199
  export interface CreateAssemblyOptions extends AssemblyUploadOptions {
174
200
  params?: CreateAssemblyParams
175
201
  assemblyId?: string
202
+ /**
203
+ * Expected number of tus uploads when files will be uploaded separately.
204
+ */
205
+ expectedUploads?: number
176
206
  }
177
207
 
178
208
  export interface ResumeAssemblyUploadsOptions extends AssemblyUploadOptions {
@@ -237,7 +267,7 @@ export interface SmartCDNUrlOptions {
237
267
  export type Fields = Record<string, string | number>
238
268
 
239
269
  // A special promise that lets the user immediately get the assembly ID (synchronously before the request is sent)
240
- interface CreateAssemblyPromise extends Promise<AssemblyStatus> {
270
+ interface CreateAssemblyPromise extends Promise<AssemblyStatusWithUploadUrls> {
241
271
  assemblyId: string
242
272
  }
243
273
 
@@ -273,9 +303,19 @@ function checkResult<T>(result: T | { error: string }): asserts result is T {
273
303
  }
274
304
  }
275
305
 
276
- export interface Options {
306
+ type AuthKeySecret = {
277
307
  authKey: string
278
308
  authSecret: string
309
+ authToken?: undefined
310
+ }
311
+
312
+ type AuthToken = {
313
+ authToken: string
314
+ authKey?: string
315
+ authSecret?: string
316
+ }
317
+
318
+ type BaseOptions = {
279
319
  endpoint?: string
280
320
  maxRetries?: number
281
321
  timeout?: number
@@ -283,11 +323,15 @@ export interface Options {
283
323
  validateResponses?: boolean
284
324
  }
285
325
 
326
+ export type Options = BaseOptions & (AuthKeySecret | AuthToken)
327
+
286
328
  export class Transloadit {
287
329
  private _authKey: string
288
330
 
289
331
  private _authSecret: string
290
332
 
333
+ private _authToken: string | null
334
+
291
335
  private _endpoint: string
292
336
 
293
337
  private _maxRetries: number
@@ -301,20 +345,26 @@ export class Transloadit {
301
345
  private _validateResponses = false
302
346
 
303
347
  constructor(opts: Options) {
304
- if (opts?.authKey == null) {
305
- throw new Error('Please provide an authKey')
306
- }
307
-
308
- if (opts.authSecret == null) {
309
- throw new Error('Please provide an authSecret')
310
- }
348
+ const rawToken = typeof opts?.authToken === 'string' ? opts.authToken.trim() : ''
349
+ const hasToken = rawToken.length > 0
311
350
 
312
351
  if (opts.endpoint?.endsWith('/')) {
313
352
  throw new Error('Trailing slash in endpoint is not allowed')
314
353
  }
315
354
 
316
- this._authKey = opts.authKey
317
- this._authSecret = opts.authSecret
355
+ if (!hasToken) {
356
+ if (opts?.authKey == null) {
357
+ throw new Error('Please provide an authKey')
358
+ }
359
+
360
+ if (opts.authSecret == null) {
361
+ throw new Error('Please provide an authSecret')
362
+ }
363
+ }
364
+
365
+ this._authKey = opts.authKey ?? ''
366
+ this._authSecret = opts.authSecret ?? ''
367
+ this._authToken = hasToken ? rawToken : null
318
368
  this._endpoint = opts.endpoint || 'https://api2.transloadit.com'
319
369
  this._maxRetries = opts.maxRetries != null ? opts.maxRetries : 5
320
370
  this._defaultTimeout = opts.timeout != null ? opts.timeout : 60000
@@ -350,7 +400,9 @@ export class Transloadit {
350
400
  files = {},
351
401
  uploads = {},
352
402
  assemblyId,
403
+ expectedUploads,
353
404
  signal,
405
+ uploadBehavior = 'await',
354
406
  } = opts
355
407
 
356
408
  // Keep track of how long the request took
@@ -406,30 +458,38 @@ export class Transloadit {
406
458
  const streamErrorPromise = createStreamErrorPromise(allStreamsMap)
407
459
 
408
460
  const createAssemblyAndUpload = async () => {
409
- const result: AssemblyStatus = await this._remoteJson({
461
+ const totalExpectedUploads =
462
+ expectedUploads == null ? allStreams.length : Math.max(expectedUploads, allStreams.length)
463
+
464
+ const result: AssemblyStatusWithUploadUrls = await this._remoteJson({
410
465
  urlSuffix,
411
466
  method: 'post',
412
467
  timeout: { request: timeout },
413
468
  params,
414
469
  fields: {
415
- tus_num_expected_upload_files: allStreams.length,
470
+ tus_num_expected_upload_files: totalExpectedUploads,
416
471
  },
417
472
  signal,
418
473
  })
419
474
  checkResult(result)
420
475
 
421
476
  if (Object.keys(allStreamsMap).length > 0) {
422
- await sendTusRequest({
477
+ const { uploadUrls } = await sendTusRequest({
423
478
  streamsMap: allStreamsMap,
424
479
  assembly: result,
425
480
  onProgress: onUploadProgress,
426
481
  requestedChunkSize,
427
482
  uploadConcurrency,
428
483
  signal,
484
+ uploadBehavior,
429
485
  })
486
+ if (uploadBehavior !== 'await' && Object.keys(uploadUrls).length > 0) {
487
+ result.upload_urls = uploadUrls
488
+ }
430
489
  }
431
490
 
432
- if (!waitForCompletion) return result
491
+ const shouldWaitForCompletion = waitForCompletion && uploadBehavior === 'await'
492
+ if (!shouldWaitForCompletion) return result
433
493
 
434
494
  if (result.assembly_id == null) {
435
495
  throw new InconsistentResponseError(
@@ -478,7 +538,9 @@ export class Transloadit {
478
538
  })
479
539
  }
480
540
 
481
- async resumeAssemblyUploads(opts: ResumeAssemblyUploadsOptions): Promise<AssemblyStatus> {
541
+ async resumeAssemblyUploads(
542
+ opts: ResumeAssemblyUploadsOptions,
543
+ ): Promise<AssemblyStatusWithUploadUrls> {
482
544
  const {
483
545
  assemblyUrl,
484
546
  files = {},
@@ -490,12 +552,16 @@ export class Transloadit {
490
552
  onUploadProgress = () => {},
491
553
  onAssemblyProgress = () => {},
492
554
  signal,
555
+ uploadBehavior = 'await',
493
556
  } = opts
494
557
 
495
558
  const startTimeMs = getHrTimeMs()
496
559
 
497
560
  getAssemblyIdFromUrl(assemblyUrl)
498
- const assembly = await this._fetchAssemblyStatus({ url: assemblyUrl, signal })
561
+ const assembly: AssemblyStatusWithUploadUrls = await this._fetchAssemblyStatus({
562
+ url: assemblyUrl,
563
+ signal,
564
+ })
499
565
  const statusUrl = assembly.assembly_ssl_url ?? assembly.assembly_url ?? assemblyUrl
500
566
 
501
567
  const finishedKeys = new Set<string>()
@@ -571,13 +637,25 @@ export class Transloadit {
571
637
  onProgress: onUploadProgress,
572
638
  signal,
573
639
  uploadUrls: uploadUrlsByLabel,
640
+ uploadBehavior,
574
641
  })
575
642
 
576
643
  await Promise.race([uploadPromise, streamErrorPromise])
644
+ const { uploadUrls } = await uploadPromise
645
+ if (uploadBehavior !== 'await' && Object.keys(uploadUrls).length > 0) {
646
+ assembly.upload_urls = uploadUrls
647
+ }
577
648
  }
578
649
 
579
- const latestAssembly = await this._fetchAssemblyStatus({ url: statusUrl, signal })
580
- if (!waitForCompletion) return latestAssembly
650
+ const latestAssembly: AssemblyStatusWithUploadUrls = await this._fetchAssemblyStatus({
651
+ url: statusUrl,
652
+ signal,
653
+ })
654
+ if (uploadBehavior !== 'await' && assembly.upload_urls) {
655
+ latestAssembly.upload_urls = assembly.upload_urls
656
+ }
657
+ const shouldWaitForCompletion = waitForCompletion && uploadBehavior === 'await'
658
+ if (!shouldWaitForCompletion) return latestAssembly
581
659
 
582
660
  if (latestAssembly.assembly_id == null) {
583
661
  throw new InconsistentResponseError(
@@ -701,6 +779,7 @@ export class Transloadit {
701
779
  const { assembly_ssl_url: url } = await this.getAssembly(assemblyId)
702
780
  const rawResult = await this._remoteJson<Record<string, unknown>, OptionalAuthParams>({
703
781
  url,
782
+ isTrustedUrl: true,
704
783
  method: 'delete',
705
784
  })
706
785
 
@@ -829,6 +908,7 @@ export class Transloadit {
829
908
  const rawResult = await this._remoteJson<Record<string, unknown>, OptionalAuthParams>({
830
909
  url,
831
910
  urlSuffix: url ? undefined : `/assemblies/${assemblyId}`,
911
+ isTrustedUrl: Boolean(url),
832
912
  signal,
833
913
  })
834
914
 
@@ -1021,6 +1101,9 @@ export class Transloadit {
1021
1101
  params: OptionalAuthParams,
1022
1102
  algorithm?: string,
1023
1103
  ): { signature: string; params: string } {
1104
+ if (!this._authKey || !this._authSecret) {
1105
+ throw new Error('Cannot sign params without authKey and authSecret.')
1106
+ }
1024
1107
  const jsonParams = this._prepareParams(params)
1025
1108
  const signature = this._calcSignature(jsonParams, algorithm)
1026
1109
 
@@ -1031,6 +1114,9 @@ export class Transloadit {
1031
1114
  * Construct a signed Smart CDN URL. See https://transloadit.com/docs/topics/signature-authentication/#smart-cdn.
1032
1115
  */
1033
1116
  getSignedSmartCDNUrl(opts: SmartCDNUrlOptions): string {
1117
+ if (!this._authKey || !this._authSecret) {
1118
+ throw new Error('authKey and authSecret are required to sign Smart CDN URLs.')
1119
+ }
1034
1120
  return getSignedSmartCdnUrl({
1035
1121
  ...opts,
1036
1122
  authKey: this._authKey,
@@ -1039,15 +1125,24 @@ export class Transloadit {
1039
1125
  }
1040
1126
 
1041
1127
  private _calcSignature(toSign: string, algorithm = 'sha384'): string {
1128
+ if (!this._authSecret) {
1129
+ throw new Error('Cannot sign params without authSecret.')
1130
+ }
1042
1131
  return signParamsSync(toSign, this._authSecret, algorithm)
1043
1132
  }
1044
1133
 
1045
1134
  // Sets the multipart/form-data for POST, PUT and DELETE requests, including
1046
1135
  // the streams, the signed params, and any additional fields.
1047
1136
  private _appendForm(form: FormData, params: OptionalAuthParams, fields?: Fields): void {
1048
- const sigData = this.calcSignature(params)
1049
- const jsonParams = sigData.params
1050
- const { signature } = sigData
1137
+ const shouldSign = Boolean(this._authKey && this._authSecret)
1138
+ let jsonParams = JSON.stringify(params ?? {})
1139
+ let signature: string | undefined
1140
+
1141
+ if (shouldSign) {
1142
+ const sigData = this.calcSignature(params)
1143
+ jsonParams = sigData.params
1144
+ signature = sigData.signature
1145
+ }
1051
1146
 
1052
1147
  form.append('params', jsonParams)
1053
1148
 
@@ -1057,16 +1152,24 @@ export class Transloadit {
1057
1152
  }
1058
1153
  }
1059
1154
 
1060
- form.append('signature', signature)
1155
+ if (signature) {
1156
+ form.append('signature', signature)
1157
+ }
1061
1158
  }
1062
1159
 
1063
1160
  // Implements HTTP GET query params, handling the case where the url already
1064
1161
  // has params.
1065
1162
  private _appendParamsToUrl(url: string, params: OptionalAuthParams): string {
1066
- const { signature, params: jsonParams } = this.calcSignature(params)
1067
-
1068
1163
  const prefix = url.indexOf('?') === -1 ? '?' : '&'
1069
1164
 
1165
+ const shouldSign = Boolean(this._authKey && this._authSecret)
1166
+ if (!shouldSign) {
1167
+ const jsonParams = JSON.stringify(params ?? {})
1168
+ return `${url}${prefix}params=${encodeURIComponent(jsonParams)}`
1169
+ }
1170
+
1171
+ const { signature, params: jsonParams } = this.calcSignature(params)
1172
+
1070
1173
  return `${url}${prefix}signature=${signature}&params=${encodeURIComponent(jsonParams)}`
1071
1174
  }
1072
1175
 
@@ -1102,6 +1205,7 @@ export class Transloadit {
1102
1205
  private async _remoteJson<TRet, TParams extends OptionalAuthParams>(opts: {
1103
1206
  urlSuffix?: string
1104
1207
  url?: string
1208
+ isTrustedUrl?: boolean
1105
1209
  timeout?: Delays
1106
1210
  method?: 'delete' | 'get' | 'post' | 'put'
1107
1211
  params?: TParams
@@ -1112,6 +1216,7 @@ export class Transloadit {
1112
1216
  const {
1113
1217
  urlSuffix,
1114
1218
  url: urlInput,
1219
+ isTrustedUrl = false,
1115
1220
  timeout = { request: this._defaultTimeout },
1116
1221
  method = 'get',
1117
1222
  params = {},
@@ -1123,6 +1228,13 @@ export class Transloadit {
1123
1228
  // Allow providing either a `urlSuffix` or a full `url`
1124
1229
  if (!urlSuffix && !urlInput) throw new Error('No URL provided')
1125
1230
  let url = urlInput || `${this._endpoint}${urlSuffix}`
1231
+ if (urlInput && !isTrustedUrl) {
1232
+ const allowed = new URL(this._endpoint)
1233
+ const candidate = new URL(urlInput)
1234
+ if (allowed.origin !== candidate.origin) {
1235
+ throw new Error(`Untrusted URL: ${candidate.origin}`)
1236
+ }
1237
+ }
1126
1238
 
1127
1239
  if (method === 'get') {
1128
1240
  url = this._appendParamsToUrl(url, params)
@@ -1147,6 +1259,7 @@ export class Transloadit {
1147
1259
  headers: {
1148
1260
  'Transloadit-Client': `node-sdk:${version}`,
1149
1261
  'User-Agent': undefined, // Remove got's user-agent
1262
+ ...(this._authToken ? { Authorization: `Bearer ${this._authToken}` } : {}),
1150
1263
  ...headers,
1151
1264
  },
1152
1265
  responseType: 'json',
@@ -0,0 +1,53 @@
1
+ export type GoldenTemplate = {
2
+ slug: string
3
+ version: string
4
+ description: string
5
+ steps: Record<string, unknown>
6
+ }
7
+
8
+ export const goldenTemplates = {
9
+ '~transloadit/encode-hls-video@0.0.1': {
10
+ slug: '~transloadit/encode-hls-video@0.0.1',
11
+ version: '0.0.1',
12
+ description:
13
+ 'Encode an input video into HLS renditions (270p, 360p, 540p) with an adaptive playlist.',
14
+ steps: {
15
+ ':original': {
16
+ robot: '/upload/handle',
17
+ },
18
+ low: {
19
+ robot: '/video/encode',
20
+ use: ':original',
21
+ ffmpeg_stack: 'v7.0.0',
22
+ preset: 'hls-270p',
23
+ result: true,
24
+ turbo: true,
25
+ },
26
+ mid: {
27
+ robot: '/video/encode',
28
+ use: ':original',
29
+ ffmpeg_stack: 'v7.0.0',
30
+ preset: 'hls-360p',
31
+ result: true,
32
+ turbo: true,
33
+ },
34
+ high: {
35
+ robot: '/video/encode',
36
+ use: ':original',
37
+ ffmpeg_stack: 'v7.0.0',
38
+ preset: 'hls-540p',
39
+ result: true,
40
+ turbo: true,
41
+ },
42
+ adaptive: {
43
+ robot: '/video/adaptive',
44
+ use: {
45
+ steps: ['low', 'mid', 'high'],
46
+ bundle_steps: true,
47
+ },
48
+ technique: 'hls',
49
+ playlist_name: 'my_playlist.m3u8',
50
+ },
51
+ },
52
+ },
53
+ } satisfies Record<string, GoldenTemplate>
@@ -25,6 +25,7 @@ import {
25
25
  TemplatesModifyCommand,
26
26
  TemplatesSyncCommand,
27
27
  } from './templates.ts'
28
+ import { UploadCommand } from './upload.ts'
28
29
 
29
30
  export function createCli(): Cli {
30
31
  const cli = new Cli({
@@ -63,5 +64,8 @@ export function createCli(): Cli {
63
64
  // Notifications commands
64
65
  cli.register(NotificationsReplayCommand)
65
66
 
67
+ // Uploads commands
68
+ cli.register(UploadCommand)
69
+
66
70
  return cli
67
71
  }
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs'
2
+ import { Command, Option } from 'clipanion'
3
+ import type { AssemblyStatus } from '../../alphalib/types/assemblyStatus.ts'
4
+ import { sendTusRequest } from '../../tus.ts'
5
+ import type { IOutputCtl } from '../OutputCtl.ts'
6
+ import { UnauthenticatedCommand } from './BaseCommand.ts'
7
+
8
+ export interface UploadOptions {
9
+ file: string
10
+ createUploadEndpoint?: string
11
+ resumeUploadEndpoint?: string
12
+ assemblyUrl: string
13
+ field?: string
14
+ }
15
+
16
+ const deriveEndpointFromUploadUrl = (uploadUrl: string): string => {
17
+ const url = new URL(uploadUrl)
18
+ url.pathname = url.pathname.replace(/\/[^/]*$/, '/')
19
+ return url.toString()
20
+ }
21
+
22
+ export async function upload(
23
+ output: IOutputCtl,
24
+ {
25
+ file,
26
+ createUploadEndpoint,
27
+ resumeUploadEndpoint,
28
+ assemblyUrl,
29
+ field = ':original',
30
+ }: UploadOptions,
31
+ ): Promise<void> {
32
+ const tusEndpoint =
33
+ createUploadEndpoint ??
34
+ (resumeUploadEndpoint ? deriveEndpointFromUploadUrl(resumeUploadEndpoint) : undefined)
35
+
36
+ if (!tusEndpoint) {
37
+ throw new Error('Provide --create-upload-endpoint or --resume-upload-endpoint.')
38
+ }
39
+
40
+ const stream = fs.createReadStream(file)
41
+ const streamsMap = {
42
+ [field]: { path: file, stream },
43
+ }
44
+
45
+ const assembly: AssemblyStatus = {
46
+ tus_url: tusEndpoint,
47
+ assembly_ssl_url: assemblyUrl,
48
+ } as AssemblyStatus
49
+
50
+ const { uploadUrls } = await sendTusRequest({
51
+ streamsMap,
52
+ assembly,
53
+ requestedChunkSize: Number.POSITIVE_INFINITY,
54
+ uploadConcurrency: 1,
55
+ onProgress: () => {},
56
+ uploadUrls: resumeUploadEndpoint ? { [field]: resumeUploadEndpoint } : undefined,
57
+ })
58
+
59
+ const uploadUrl = uploadUrls[field]
60
+
61
+ output.print(`Uploaded ${file}`, {
62
+ status: 'ok',
63
+ file,
64
+ field,
65
+ assembly_url: assemblyUrl,
66
+ tus_endpoint: tusEndpoint,
67
+ resume_upload_endpoint: resumeUploadEndpoint,
68
+ upload_url: uploadUrl,
69
+ })
70
+ }
71
+
72
+ export class UploadCommand extends UnauthenticatedCommand {
73
+ static override paths = [['upload']]
74
+
75
+ static override usage = Command.Usage({
76
+ category: 'Uploads',
77
+ description: 'Upload a local file to a tus endpoint for an Assembly',
78
+ details: `
79
+ Upload a local file to a tus endpoint and attach it to an existing Assembly.
80
+ Use --create-upload-endpoint for new uploads or --resume-upload-endpoint to resume.
81
+ `,
82
+ examples: [
83
+ [
84
+ 'Upload a file to an Assembly',
85
+ 'transloadit upload ./video.mp4 --create-upload-endpoint https://api2.transloadit.com/resumable/files/ --assembly https://api2.transloadit.com/assemblies/ASSEMBLY_ID',
86
+ ],
87
+ [
88
+ 'Resume a file upload',
89
+ 'transloadit upload ./video.mp4 --resume-upload-endpoint https://api2.transloadit.com/resumable/files/UPLOAD_ID --assembly https://api2.transloadit.com/assemblies/ASSEMBLY_ID',
90
+ ],
91
+ ],
92
+ })
93
+
94
+ file = Option.String({ required: true })
95
+ tusEndpoint = Option.String({ required: false })
96
+
97
+ assemblyUrl = Option.String('--assembly', {
98
+ description: 'Assembly URL to attach this upload to',
99
+ required: true,
100
+ })
101
+
102
+ createUploadEndpoint = Option.String('--create-upload-endpoint', {
103
+ description: 'Tus create endpoint (e.g. https://api2.transloadit.com/resumable/files/)',
104
+ })
105
+
106
+ resumeUploadEndpoint = Option.String('--resume-upload-endpoint', {
107
+ description: 'Tus upload URL to resume (e.g. https://.../resumable/files/<id>)',
108
+ })
109
+
110
+ field = Option.String('--field', {
111
+ description: 'Field name for the upload (default: :original)',
112
+ })
113
+
114
+ protected async run(): Promise<number | undefined> {
115
+ try {
116
+ await upload(this.output, {
117
+ file: this.file,
118
+ createUploadEndpoint: this.createUploadEndpoint ?? this.tusEndpoint,
119
+ resumeUploadEndpoint: this.resumeUploadEndpoint,
120
+ assemblyUrl: this.assemblyUrl,
121
+ field: this.field,
122
+ })
123
+ return undefined
124
+ } catch (err) {
125
+ this.output.error(err instanceof Error ? err.message : String(err))
126
+ return 1
127
+ }
128
+ }
129
+ }