From 618a91c873ae12d1d467cb4e18adafcb0d77ee84 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 2 Jun 2026 02:36:38 +0200 Subject: [PATCH 01/12] Add generated assembly feature markers --- packages/node/src/Transloadit.ts | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/node/src/Transloadit.ts b/packages/node/src/Transloadit.ts index 18ad3ef8..2ef5f71f 100644 --- a/packages/node/src/Transloadit.ts +++ b/packages/node/src/Transloadit.ts @@ -575,6 +575,67 @@ export class Transloadit { return result.data } + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + + async createTusAssembly(fileCount: number): Promise { + return await this._remoteJson< + AssemblyStatusWithUploadUrls, + CreateAssemblyParams & Record + >({ + urlSuffix: '/assemblies', + method: 'post', + params: { + await: false, + steps: { + ':original': { + output_meta: true, + result: 'debug', + robot: '/upload/handle', + }, + }, + }, + fields: { + num_expected_upload_files: fileCount, + }, + }) + } + + // + + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + + async waitForAssembly(assemblyUrl: string): Promise { + while (true) { + const result = await this._remoteJson({ + url: assemblyUrl, + isTrustedUrl: true, + method: 'get', + }) + + // Abort polling if the assembly has entered an error state + if (result.error) { + return result + } + + // The polling is done if the assembly is not uploading or executing anymore. + if (result.ok !== 'ASSEMBLY_UPLOADING' && result.ok !== 'ASSEMBLY_EXECUTING') { + return result + } + + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } + + // + async resumeAssemblyUploads( opts: ResumeAssemblyUploadsOptions, ): Promise { From 380d7a269cb5e0a7464acf90151648db762e4e0e Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 2 Jun 2026 02:45:03 +0200 Subject: [PATCH 02/12] Add Node SDK devdock TUS assembly example --- .../api2-devdock-tus-assembly/main.ts | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 packages/node/examples/api2-devdock-tus-assembly/main.ts diff --git a/packages/node/examples/api2-devdock-tus-assembly/main.ts b/packages/node/examples/api2-devdock-tus-assembly/main.ts new file mode 100644 index 00000000..fcdc53f4 --- /dev/null +++ b/packages/node/examples/api2-devdock-tus-assembly/main.ts @@ -0,0 +1,237 @@ +import { readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' + +import { Upload } from 'tus-js-client' + +import { Transloadit } from '../../src/Transloadit.ts' + +type JsonRecord = Record + +function fail(message: string): never { + throw new Error(message) +} + +function requiredEnv(name: string): string { + const value = process.env[name] + if (!value) { + fail(`${name} must be set`) + } + + return value +} + +function isRecord(value: unknown): value is JsonRecord { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function requireRecord(value: unknown, label: string): JsonRecord { + if (!isRecord(value)) { + fail(`${label} must be an object`) + } + + return value +} + +function requireString(value: unknown, label: string): string { + if (typeof value !== 'string') { + fail(`${label} must be a string`) + } + + return value +} + +function requireNumber(value: unknown, label: string): number { + if (typeof value !== 'number' || !Number.isFinite(value)) { + fail(`${label} must be a number`) + } + + return value +} + +function requireArray(value: unknown, label: string): unknown[] { + if (!Array.isArray(value)) { + fail(`${label} must be an array`) + } + + return value +} + +function readPath(value: unknown, pathParts: readonly unknown[], label: string): unknown { + let current = value + for (const part of pathParts) { + if (Array.isArray(current) && Number.isInteger(part)) { + if (part >= current.length) { + fail(`${label} path ${JSON.stringify(pathParts)} index ${part} is out of range`) + } + current = current[part] + continue + } + + if (isRecord(current) && typeof part === 'string') { + if (!Object.hasOwn(current, part)) { + fail(`${label} path ${JSON.stringify(pathParts)} is missing key ${JSON.stringify(part)}`) + } + current = current[part] + continue + } + + fail(`${label} path ${JSON.stringify(pathParts)} cannot read ${JSON.stringify(part)}`) + } + + return current +} + +function resolveValue(valueSpec: unknown, context: JsonRecord, label: string): unknown { + const spec = requireRecord(valueSpec, label) + if (Object.hasOwn(spec, 'value')) { + return spec.value + } + + const source = requireRecord(spec.source, `${label}.source`) + const root = requireString(source.root, `${label}.source.root`) + const pathParts = requireArray(source.path, `${label}.source.path`) + if (!Object.hasOwn(context, root)) { + fail(`${label} value source root ${JSON.stringify(root)} is unavailable`) + } + + return readPath(context[root], pathParts, label) +} + +function scalarString(value: unknown): string { + if (value === null) { + return 'null' + } + + if (typeof value === 'boolean') { + return value ? 'true' : 'false' + } + + return String(value) +} + +async function loadScenario(): Promise { + const scenarioPath = + process.env.API2_SDK_EXAMPLE_SCENARIO ?? path.join(import.meta.dirname, 'api2-scenario.json') + const parsed: unknown = JSON.parse(await readFile(scenarioPath, 'utf8')) + + return requireRecord(parsed, 'scenario') +} + +function scenarioBytes(uploadConfig: JsonRecord): Buffer { + const source = requireRecord(uploadConfig.source, 'upload.source') + const kind = requireString(source.kind, 'upload.source.kind') + const encoding = requireString(source.encoding, 'upload.source.encoding') + if (kind !== 'bytes') { + fail(`unsupported scenario source kind ${JSON.stringify(kind)}`) + } + + if (encoding !== 'utf8') { + fail(`unsupported scenario source encoding ${JSON.stringify(encoding)}`) + } + + return Buffer.from(requireString(source.value, 'upload.source.value'), 'utf8') +} + +function uploadMetadata( + uploadConfig: JsonRecord, + scenario: JsonRecord, + createResponse: JsonRecord, +): Record { + const context = { createResponse, scenario } + const metadata: Record = {} + for (const fieldValue of requireArray(uploadConfig.metadata, 'upload.metadata')) { + const field = requireRecord(fieldValue, 'upload.metadata[]') + const name = requireString(field.name, 'upload.metadata[].name') + metadata[name] = scalarString(resolveValue(field.value, context, `upload.metadata.${name}`)) + } + + return metadata +} + +function retryDelays(retries: unknown): number[] { + const retryCount = requireNumber(retries, 'upload.retries') + if (!Number.isInteger(retryCount) || retryCount < 0) { + fail(`unsupported retry count ${JSON.stringify(retryCount)}`) + } + + return Array.from({ length: retryCount }, () => 0) +} + +async function uploadWithTus(scenario: JsonRecord, createResponse: JsonRecord): Promise { + const uploadConfig = requireRecord(scenario.upload, 'upload') + const context = { createResponse, scenario } + const endpoint = scalarString(resolveValue(uploadConfig.tusUrl, context, 'upload.tusUrl')) + const content = scenarioBytes(uploadConfig) + if (uploadConfig.chunkSize !== 'full-file') { + fail(`unsupported chunk size policy ${JSON.stringify(uploadConfig.chunkSize)}`) + } + + return await new Promise((resolve, reject) => { + let upload: Upload | null = null + upload = new Upload(content, { + endpoint, + chunkSize: content.length, + metadata: uploadMetadata(uploadConfig, scenario, createResponse), + retryDelays: retryDelays(uploadConfig.retries), + onError: reject, + onSuccess: () => { + if (!upload?.url) { + reject(new Error('TUS upload did not expose an upload URL')) + return + } + + resolve(upload.url) + }, + }) + + upload.start() + }) +} + +async function writeResult(result: JsonRecord): Promise { + const resultPath = process.env.API2_SDK_EXAMPLE_RESULT + if (!resultPath) { + return + } + + await writeFile(resultPath, `${JSON.stringify(result, undefined, 2)}\n`) +} + +async function main(): Promise { + const scenario = await loadScenario() + const createTusAssembly = requireRecord(scenario.createTusAssembly, 'createTusAssembly') + const createInput = requireRecord(createTusAssembly.input, 'createTusAssembly.input') + + const client = new Transloadit({ + authKey: requiredEnv('TRANSLOADIT_KEY'), + authSecret: requiredEnv('TRANSLOADIT_SECRET'), + endpoint: requiredEnv('TRANSLOADIT_ENDPOINT'), + }) + + const createResponse = requireRecord( + await client.createTusAssembly(requireNumber(createInput.file_count, 'file_count')), + 'createTusAssembly response', + ) + const uploadUrl = await uploadWithTus(scenario, createResponse) + const status = requireRecord( + await client.waitForAssembly( + requireString(createResponse.assembly_ssl_url, 'createTusAssembly response.assembly_ssl_url'), + ), + 'waitForAssembly response', + ) + + await writeResult({ + createResponse, + uploadUrl, + waitOk: status.ok, + }) + + console.log( + `Node SDK devdock scenario ${requireString(scenario.scenarioId, 'scenarioId')} uploaded to ${uploadUrl}`, + ) +} + +main().catch((err: unknown) => { + console.error(err) + process.exit(1) +}) From 4c3d9724c3baa6d0f0527fb08b35729c3e60185d Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 2 Jun 2026 02:49:53 +0200 Subject: [PATCH 03/12] Mark generated Node SDK endpoint blocks --- packages/node/src/Transloadit.ts | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/packages/node/src/Transloadit.ts b/packages/node/src/Transloadit.ts index 2ef5f71f..f41fa408 100644 --- a/packages/node/src/Transloadit.ts +++ b/packages/node/src/Transloadit.ts @@ -1029,6 +1029,12 @@ export class Transloadit { * @param params optional request options * @returns when the Credential is created */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async createTemplateCredential( params: CreateTemplateCredentialParams, ): Promise { @@ -1039,6 +1045,8 @@ export class Transloadit { }) } + // + /** * Edit a Credential * @@ -1046,6 +1054,12 @@ export class Transloadit { * @param params optional request options * @returns when the Credential is edited */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async editTemplateCredential( credentialId: string, params: CreateTemplateCredentialParams, @@ -1057,12 +1071,20 @@ export class Transloadit { }) } + // + /** * Delete a Credential * * @param credentialId the Credential ID * @returns when the Credential is deleted */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async deleteTemplateCredential(credentialId: string): Promise { return await this._remoteJson({ urlSuffix: `/template_credentials/${credentialId}`, @@ -1070,12 +1092,20 @@ export class Transloadit { }) } + // + /** * Get a Credential * * @param credentialId the Credential ID * @returns when the Credential is retrieved */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async getTemplateCredential(credentialId: string): Promise { return await this._remoteJson({ urlSuffix: `/template_credentials/${credentialId}`, @@ -1083,12 +1113,20 @@ export class Transloadit { }) } + // + /** * List all TemplateCredentials * * @param params optional request options * @returns the list of templates */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async listTemplateCredentials( params?: ListTemplateCredentialsParams, ): Promise { @@ -1099,6 +1137,8 @@ export class Transloadit { }) } + // + streamTemplateCredentials(params: ListTemplateCredentialsParams) { return new PaginationStream(async (page) => ({ items: (await this.listTemplateCredentials({ ...params, page })).credentials, @@ -1111,6 +1151,12 @@ export class Transloadit { * @param params optional request options * @returns when the template is created */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async createTemplate(params: CreateTemplateParams): Promise { return await this._remoteJson({ urlSuffix: '/templates', @@ -1119,6 +1165,8 @@ export class Transloadit { }) } + // + /** * Edit an Assembly Template * @@ -1126,6 +1174,12 @@ export class Transloadit { * @param params optional request options * @returns when the template is edited */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async editTemplate(templateId: string, params: EditTemplateParams): Promise { return await this._remoteJson({ urlSuffix: `/templates/${templateId}`, @@ -1134,12 +1188,20 @@ export class Transloadit { }) } + // + /** * Delete an Assembly Template * * @param templateId the template ID * @returns when the template is deleted */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async deleteTemplate(templateId: string): Promise { return await this._remoteJson({ urlSuffix: `/templates/${templateId}`, @@ -1147,12 +1209,20 @@ export class Transloadit { }) } + // + /** * Get an Assembly Template * * @param templateId the template ID * @returns when the template is retrieved */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async getTemplate(templateId: string): Promise { return await this._remoteJson({ urlSuffix: `/templates/${templateId}`, @@ -1160,12 +1230,20 @@ export class Transloadit { }) } + // + /** * List all Assembly Templates * * @param params optional request options * @returns the list of templates */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async listTemplates( params?: ListTemplatesParams, ): Promise> { @@ -1176,6 +1254,8 @@ export class Transloadit { }) } + // + streamTemplates(params?: ListTemplatesParams): PaginationStream { return new PaginationStream(async (page) => this.listTemplates({ ...params, page })) } @@ -1187,6 +1267,12 @@ export class Transloadit { * @returns with billing data * @see https://transloadit.com/docs/api/bill-date-get/ */ + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + async getBill(month: string): Promise { assert.ok(month, 'month is required') return await this._remoteJson({ @@ -1195,6 +1281,8 @@ export class Transloadit { }) } + // + calcSignature( params: OptionalAuthParams, algorithm?: string, From c647ce0474c02cbe39c38f881695d55163cb6dbc Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 2 Jun 2026 03:36:34 +0200 Subject: [PATCH 04/12] Add devdock template lifecycle example --- .../api2-devdock-template-lifecycle/main.ts | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 packages/node/examples/api2-devdock-template-lifecycle/main.ts diff --git a/packages/node/examples/api2-devdock-template-lifecycle/main.ts b/packages/node/examples/api2-devdock-template-lifecycle/main.ts new file mode 100644 index 00000000..116bb020 --- /dev/null +++ b/packages/node/examples/api2-devdock-template-lifecycle/main.ts @@ -0,0 +1,255 @@ +import { readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' + +import { ApiError, Transloadit } from '../../src/Transloadit.ts' + +type JsonRecord = Record + +interface ScenarioContent { + additionalProperties: JsonRecord + steps: JsonRecord +} + +interface TemplateConfig { + content: ScenarioContent + namePrefix: string + requireSignatureAuth: boolean +} + +interface UpdateConfig { + content: ScenarioContent + nameSuffix: string + requireSignatureAuth: boolean +} + +interface TemplateLifecycleScenario { + delete: { + errorCodeIncludes: string + } + list: { + minimumCount: number + pageSize: number + } + scenarioId: string + template: TemplateConfig + update: UpdateConfig +} + +function fail(message: string): never { + throw new Error(message) +} + +function requiredEnv(name: string): string { + const value = process.env[name] + if (!value) { + fail(`${name} must be set`) + } + + return value +} + +function isRecord(value: unknown): value is JsonRecord { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function requireRecord(value: unknown, label: string): JsonRecord { + if (!isRecord(value)) { + fail(`${label} must be an object`) + } + + return value +} + +function requireString(value: unknown, label: string): string { + if (typeof value !== 'string') { + fail(`${label} must be a string`) + } + + return value +} + +function requireNumber(value: unknown, label: string): number { + if (typeof value !== 'number' || !Number.isFinite(value)) { + fail(`${label} must be a number`) + } + + return value +} + +function requireBoolean(value: unknown, label: string): boolean { + if (typeof value !== 'boolean') { + fail(`${label} must be a boolean`) + } + + return value +} + +function scenarioContent(value: unknown, label: string): ScenarioContent { + const content = requireRecord(value, label) + + return { + additionalProperties: requireRecord( + content.additionalProperties, + `${label}.additionalProperties`, + ), + steps: requireRecord(content.steps, `${label}.steps`), + } +} + +function templateConfig(value: unknown, label: string): TemplateConfig { + const config = requireRecord(value, label) + + return { + content: scenarioContent(config.content, `${label}.content`), + namePrefix: requireString(config.namePrefix, `${label}.namePrefix`), + requireSignatureAuth: requireBoolean( + config.requireSignatureAuth, + `${label}.requireSignatureAuth`, + ), + } +} + +function updateConfig(value: unknown, label: string): UpdateConfig { + const config = requireRecord(value, label) + + return { + content: scenarioContent(config.content, `${label}.content`), + nameSuffix: requireString(config.nameSuffix, `${label}.nameSuffix`), + requireSignatureAuth: requireBoolean( + config.requireSignatureAuth, + `${label}.requireSignatureAuth`, + ), + } +} + +async function loadScenario(): Promise { + const scenarioPath = + process.env.API2_SDK_EXAMPLE_SCENARIO ?? path.join(import.meta.dirname, 'api2-scenario.json') + const scenario = requireRecord(JSON.parse(await readFile(scenarioPath, 'utf8')), 'scenario') + const list = requireRecord(scenario.list, 'scenario.list') + const deleteConfig = requireRecord(scenario.delete, 'scenario.delete') + + return { + delete: { + errorCodeIncludes: requireString( + deleteConfig.errorCodeIncludes, + 'scenario.delete.errorCodeIncludes', + ), + }, + list: { + minimumCount: requireNumber(list.minimumCount, 'scenario.list.minimumCount'), + pageSize: requireNumber(list.pageSize, 'scenario.list.pageSize'), + }, + scenarioId: requireString(scenario.scenarioId, 'scenario.scenarioId'), + template: templateConfig(scenario.template, 'scenario.template'), + update: updateConfig(scenario.update, 'scenario.update'), + } +} + +function templatePayload(name: string, config: TemplateConfig | UpdateConfig): JsonRecord { + return { + name, + require_signature_auth: config.requireSignatureAuth ? 1 : 0, + template: { + ...config.content.additionalProperties, + steps: config.content.steps, + }, + } +} + +function requireTemplateId(value: unknown, label: string): string { + const template = requireRecord(value, label) + + return requireString(template.id, `${label}.id`) +} + +function templateResult(value: unknown): JsonRecord { + const template = requireRecord(value, 'template response') + const content = requireRecord(template.content, 'template response.content') + const requireSignatureAuth = requireNumber( + template.require_signature_auth, + 'template response.require_signature_auth', + ) + + return { + content, + id: requireString(template.id, 'template response.id'), + name: requireString(template.name, 'template response.name'), + requireSignatureAuth: requireSignatureAuth !== 0, + } +} + +async function deletedGetResult(client: Transloadit, templateId: string): Promise { + try { + await client.getTemplate(templateId) + return { + deletedErrorCode: '', + deletedGetSucceeded: true, + } + } catch (err) { + if (!(err instanceof ApiError)) { + throw err + } + + return { + deletedErrorCode: err.code ?? '', + deletedGetSucceeded: false, + } + } +} + +async function writeResult(result: JsonRecord): Promise { + const resultPath = process.env.API2_SDK_EXAMPLE_RESULT + if (!resultPath) { + return + } + + await writeFile(resultPath, `${JSON.stringify(result, undefined, 2)}\n`) +} + +async function main(): Promise { + const scenario = await loadScenario() + const client = new Transloadit({ + authKey: requiredEnv('TRANSLOADIT_KEY'), + authSecret: requiredEnv('TRANSLOADIT_SECRET'), + endpoint: requiredEnv('TRANSLOADIT_ENDPOINT'), + }) + + const templateName = `${scenario.template.namePrefix}-${Date.now()}` + const created = await client.createTemplate(templatePayload(templateName, scenario.template)) + const templateId = requireTemplateId(created, 'createTemplate response') + let deleteTemplate = true + + try { + const fetched = await client.getTemplate(templateId) + const listed = await client.listTemplates({ pagesize: scenario.list.pageSize }) + const updatedTemplateName = `${templateName}${scenario.update.nameSuffix}` + + await client.editTemplate(templateId, templatePayload(updatedTemplateName, scenario.update)) + const updated = await client.getTemplate(templateId) + + await client.deleteTemplate(templateId) + deleteTemplate = false + + await writeResult({ + ...(await deletedGetResult(client, templateId)), + fetched: templateResult(fetched), + listCount: listed.count, + templateId, + templateName, + updated: templateResult(updated), + updatedTemplateName, + }) + } finally { + if (deleteTemplate) { + await client.deleteTemplate(templateId) + } + } + + console.log(`Node SDK devdock scenario ${scenario.scenarioId} passed for ${templateId}`) +} + +main().catch((err: unknown) => { + console.error(err) + process.exit(1) +}) From fbaaf0fc5a783540d77f1e3922bd0b4098fc9310 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 2 Jun 2026 05:37:05 +0200 Subject: [PATCH 05/12] Read TUS scenario preparations generically --- .../api2-devdock-tus-assembly/main.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/node/examples/api2-devdock-tus-assembly/main.ts b/packages/node/examples/api2-devdock-tus-assembly/main.ts index fcdc53f4..1ae288af 100644 --- a/packages/node/examples/api2-devdock-tus-assembly/main.ts +++ b/packages/node/examples/api2-devdock-tus-assembly/main.ts @@ -97,6 +97,28 @@ function resolveValue(valueSpec: unknown, context: JsonRecord, label: string): u return readPath(context[root], pathParts, label) } +function featurePreparation( + scenario: JsonRecord, + featureId: string, +): { label: string; preparation: JsonRecord } { + const preparations = requireArray(scenario.preparations, 'preparations') + for (const [index, rawPreparation] of preparations.entries()) { + const label = `preparations[${index}]` + const preparation = requireRecord(rawPreparation, label) + if (requireString(preparation.featureId, `${label}.featureId`) !== featureId) { + continue + } + + if (requireString(preparation.kind, `${label}.kind`) !== 'feature-call') { + fail(`${label} must be a feature-call preparation`) + } + + return { label, preparation } + } + + fail(`scenario has no preparation for feature ${JSON.stringify(featureId)}`) +} + function scalarString(value: unknown): string { if (value === null) { return 'null' @@ -199,8 +221,11 @@ async function writeResult(result: JsonRecord): Promise { async function main(): Promise { const scenario = await loadScenario() - const createTusAssembly = requireRecord(scenario.createTusAssembly, 'createTusAssembly') - const createInput = requireRecord(createTusAssembly.input, 'createTusAssembly.input') + const { label: createTusAssemblyLabel, preparation: createTusAssembly } = featurePreparation( + scenario, + 'createTusAssembly', + ) + const createInput = requireRecord(createTusAssembly.input, `${createTusAssemblyLabel}.input`) const client = new Transloadit({ authKey: requiredEnv('TRANSLOADIT_KEY'), From 95f7b0e54d5a4a2e344b2d03e5c47e23fc81de27 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 2 Jun 2026 08:03:08 +0200 Subject: [PATCH 06/12] Generate TUS assembly upload helper --- .../api2-devdock-tus-assembly/main.ts | 152 +++--------------- packages/node/src/Transloadit.ts | 103 ++++++++++++ 2 files changed, 126 insertions(+), 129 deletions(-) diff --git a/packages/node/examples/api2-devdock-tus-assembly/main.ts b/packages/node/examples/api2-devdock-tus-assembly/main.ts index 1ae288af..b4d126eb 100644 --- a/packages/node/examples/api2-devdock-tus-assembly/main.ts +++ b/packages/node/examples/api2-devdock-tus-assembly/main.ts @@ -1,8 +1,6 @@ import { readFile, writeFile } from 'node:fs/promises' import path from 'node:path' -import { Upload } from 'tus-js-client' - import { Transloadit } from '../../src/Transloadit.ts' type JsonRecord = Record @@ -56,45 +54,11 @@ function requireArray(value: unknown, label: string): unknown[] { return value } -function readPath(value: unknown, pathParts: readonly unknown[], label: string): unknown { - let current = value - for (const part of pathParts) { - if (Array.isArray(current) && Number.isInteger(part)) { - if (part >= current.length) { - fail(`${label} path ${JSON.stringify(pathParts)} index ${part} is out of range`) - } - current = current[part] - continue - } - - if (isRecord(current) && typeof part === 'string') { - if (!Object.hasOwn(current, part)) { - fail(`${label} path ${JSON.stringify(pathParts)} is missing key ${JSON.stringify(part)}`) - } - current = current[part] - continue - } - - fail(`${label} path ${JSON.stringify(pathParts)} cannot read ${JSON.stringify(part)}`) - } - - return current -} - -function resolveValue(valueSpec: unknown, context: JsonRecord, label: string): unknown { - const spec = requireRecord(valueSpec, label) - if (Object.hasOwn(spec, 'value')) { - return spec.value - } - - const source = requireRecord(spec.source, `${label}.source`) - const root = requireString(source.root, `${label}.source.root`) - const pathParts = requireArray(source.path, `${label}.source.path`) - if (!Object.hasOwn(context, root)) { - fail(`${label} value source root ${JSON.stringify(root)} is unavailable`) - } - - return readPath(context[root], pathParts, label) +function stringRecord(value: unknown, label: string): Record { + const record = requireRecord(value, label) + return Object.fromEntries( + Object.entries(record).map(([key, entryValue]) => [key, String(entryValue)]), + ) } function featurePreparation( @@ -119,18 +83,6 @@ function featurePreparation( fail(`scenario has no preparation for feature ${JSON.stringify(featureId)}`) } -function scalarString(value: unknown): string { - if (value === null) { - return 'null' - } - - if (typeof value === 'boolean') { - return value ? 'true' : 'false' - } - - return String(value) -} - async function loadScenario(): Promise { const scenarioPath = process.env.API2_SDK_EXAMPLE_SCENARIO ?? path.join(import.meta.dirname, 'api2-scenario.json') @@ -139,6 +91,13 @@ async function loadScenario(): Promise { return requireRecord(parsed, 'scenario') } +function fileCount(scenario: JsonRecord): number { + const { label, preparation } = featurePreparation(scenario, 'createTusAssembly') + const input = requireRecord(preparation.input, `${label}.input`) + + return requireNumber(input.file_count, `${label}.input.file_count`) +} + function scenarioBytes(uploadConfig: JsonRecord): Buffer { const source = requireRecord(uploadConfig.source, 'upload.source') const kind = requireString(source.kind, 'upload.source.kind') @@ -154,62 +113,6 @@ function scenarioBytes(uploadConfig: JsonRecord): Buffer { return Buffer.from(requireString(source.value, 'upload.source.value'), 'utf8') } -function uploadMetadata( - uploadConfig: JsonRecord, - scenario: JsonRecord, - createResponse: JsonRecord, -): Record { - const context = { createResponse, scenario } - const metadata: Record = {} - for (const fieldValue of requireArray(uploadConfig.metadata, 'upload.metadata')) { - const field = requireRecord(fieldValue, 'upload.metadata[]') - const name = requireString(field.name, 'upload.metadata[].name') - metadata[name] = scalarString(resolveValue(field.value, context, `upload.metadata.${name}`)) - } - - return metadata -} - -function retryDelays(retries: unknown): number[] { - const retryCount = requireNumber(retries, 'upload.retries') - if (!Number.isInteger(retryCount) || retryCount < 0) { - fail(`unsupported retry count ${JSON.stringify(retryCount)}`) - } - - return Array.from({ length: retryCount }, () => 0) -} - -async function uploadWithTus(scenario: JsonRecord, createResponse: JsonRecord): Promise { - const uploadConfig = requireRecord(scenario.upload, 'upload') - const context = { createResponse, scenario } - const endpoint = scalarString(resolveValue(uploadConfig.tusUrl, context, 'upload.tusUrl')) - const content = scenarioBytes(uploadConfig) - if (uploadConfig.chunkSize !== 'full-file') { - fail(`unsupported chunk size policy ${JSON.stringify(uploadConfig.chunkSize)}`) - } - - return await new Promise((resolve, reject) => { - let upload: Upload | null = null - upload = new Upload(content, { - endpoint, - chunkSize: content.length, - metadata: uploadMetadata(uploadConfig, scenario, createResponse), - retryDelays: retryDelays(uploadConfig.retries), - onError: reject, - onSuccess: () => { - if (!upload?.url) { - reject(new Error('TUS upload did not expose an upload URL')) - return - } - - resolve(upload.url) - }, - }) - - upload.start() - }) -} - async function writeResult(result: JsonRecord): Promise { const resultPath = process.env.API2_SDK_EXAMPLE_RESULT if (!resultPath) { @@ -221,38 +124,29 @@ async function writeResult(result: JsonRecord): Promise { async function main(): Promise { const scenario = await loadScenario() - const { label: createTusAssemblyLabel, preparation: createTusAssembly } = featurePreparation( - scenario, - 'createTusAssembly', - ) - const createInput = requireRecord(createTusAssembly.input, `${createTusAssemblyLabel}.input`) - + const upload = requireRecord(scenario.upload, 'upload') const client = new Transloadit({ authKey: requiredEnv('TRANSLOADIT_KEY'), authSecret: requiredEnv('TRANSLOADIT_SECRET'), endpoint: requiredEnv('TRANSLOADIT_ENDPOINT'), }) - const createResponse = requireRecord( - await client.createTusAssembly(requireNumber(createInput.file_count, 'file_count')), - 'createTusAssembly response', - ) - const uploadUrl = await uploadWithTus(scenario, createResponse) - const status = requireRecord( - await client.waitForAssembly( - requireString(createResponse.assembly_ssl_url, 'createTusAssembly response.assembly_ssl_url'), - ), - 'waitForAssembly response', + const result = await client.uploadTusAssembly( + fileCount(scenario), + scenarioBytes(upload), + requireString(upload.fieldName, 'upload.fieldName'), + requireString(upload.fileName, 'upload.fileName'), + stringRecord(upload.userMeta, 'upload.userMeta'), ) await writeResult({ - createResponse, - uploadUrl, - waitOk: status.ok, + createResponse: result.assembly, + uploadUrl: result.uploadUrl, + waitOk: result.assembly.ok, }) console.log( - `Node SDK devdock scenario ${requireString(scenario.scenarioId, 'scenarioId')} uploaded to ${uploadUrl}`, + `Node SDK devdock scenario ${requireString(scenario.scenarioId, 'scenarioId')} uploaded to ${result.uploadUrl}`, ) } diff --git a/packages/node/src/Transloadit.ts b/packages/node/src/Transloadit.ts index f41fa408..a9f1965c 100644 --- a/packages/node/src/Transloadit.ts +++ b/packages/node/src/Transloadit.ts @@ -108,6 +108,11 @@ export type AssemblyStatusWithUploadUrls = AssemblyStatus & { upload_urls?: Record } +export interface UploadTusAssemblyResult { + assembly: AssemblyStatus + uploadUrl: string +} + const { version } = packageJson export type AssemblyProgress = (assembly: AssemblyStatus) => void @@ -636,6 +641,104 @@ export class Transloadit { // + // + + // This block is generated from Transloadit API2 contracts. If it looks wrong, + // please report the issue instead of editing this block by hand; the source fix + // belongs in the contract generator so all SDKs stay in sync. + + async uploadTusAssembly( + fileCount: number, + content: Buffer | Uint8Array | string, + fieldname: string, + filename: string, + userMeta: Record, + ): Promise { + const createdAssembly = await this.createTusAssembly(fileCount) + + const endpointUrl = createdAssembly.tus_url + if (!endpointUrl) { + throw new Error('TUS singleUploadLifecycle needs input.endpointUrl') + } + + const contentBytes = Buffer.isBuffer(content) ? content : Buffer.from(content) + + const metadataMap = new Map() + for (const [key, value] of Object.entries(userMeta)) { + metadataMap.set(String(key), String(value)) + } + metadataMap.set('assembly_url', String(createdAssembly.assembly_url)) + metadataMap.set('fieldname', String(fieldname)) + metadataMap.set('filename', String(filename)) + + const createHeaders: Record = {} + createHeaders['Tus-Resumable'] = '1.0.0' + createHeaders['Upload-Length'] = String(contentBytes.length) + const createMetadataParts: string[] = [] + for (const [key, value] of metadataMap) { + const encodedValue = Buffer.from(String(value), 'utf8').toString('base64') + createMetadataParts.push(`${key} ${encodedValue}`) + } + createHeaders['Upload-Metadata'] = createMetadataParts.join(',') + const createResponse = await got(endpointUrl, { + method: 'POST', + body: Buffer.alloc(0), + headers: createHeaders, + retry: this._gotRetry, + throwHttpErrors: false, + timeout: { request: this._defaultTimeout }, + }) + + if (createResponse.statusCode !== 201) { + throw new Error(`TUS create returned HTTP ${createResponse.statusCode}, expected 201`) + } + const uploadUrlLocation = createResponse.headers.location + const uploadUrlLocationText = Array.isArray(uploadUrlLocation) + ? uploadUrlLocation[0] + : uploadUrlLocation + if (!uploadUrlLocationText) { + throw new Error('TUS create did not return a Location header') + } + const uploadUrlText = new URL(uploadUrlLocationText, endpointUrl).toString() + + const uploadHeaders: Record = {} + uploadHeaders['Tus-Resumable'] = '1.0.0' + uploadHeaders['Upload-Offset'] = '0' + uploadHeaders['Content-Type'] = 'application/offset+octet-stream' + const uploadResponse = await got(uploadUrlText, { + method: 'PATCH', + body: contentBytes, + headers: uploadHeaders, + retry: this._gotRetry, + throwHttpErrors: false, + timeout: { request: this._defaultTimeout }, + }) + + if (uploadResponse.statusCode !== 204) { + throw new Error(`TUS upload returned HTTP ${uploadResponse.statusCode}, expected 204`) + } + const uploadOffsetHeader = uploadResponse.headers['upload-offset'] + const uploadOffsetText = Array.isArray(uploadOffsetHeader) + ? uploadOffsetHeader[0] + : uploadOffsetHeader + if (!uploadOffsetText) { + throw new Error('TUS upload returned an invalid Upload-Offset header') + } + const remoteOffset = Number(uploadOffsetText) + if (!Number.isInteger(remoteOffset)) { + throw new Error('TUS upload returned an invalid Upload-Offset header') + } + if (remoteOffset !== contentBytes.length) { + throw new Error(`TUS upload offset ${remoteOffset}, expected ${contentBytes.length}`) + } + + const completedAssembly = await this.waitForAssembly(createdAssembly.assembly_ssl_url ?? '') + + return { assembly: completedAssembly, uploadUrl: uploadUrlText } + } + + // + async resumeAssemblyUploads( opts: ResumeAssemblyUploadsOptions, ): Promise { From a5b4c9aa6cb250601e66360227c5cbb876dd68e7 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 3 Jun 2026 01:01:35 +0200 Subject: [PATCH 07/12] Use header-derived TUS offset variable --- packages/node/src/Transloadit.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/node/src/Transloadit.ts b/packages/node/src/Transloadit.ts index a9f1965c..5e1768bc 100644 --- a/packages/node/src/Transloadit.ts +++ b/packages/node/src/Transloadit.ts @@ -724,12 +724,12 @@ export class Transloadit { if (!uploadOffsetText) { throw new Error('TUS upload returned an invalid Upload-Offset header') } - const remoteOffset = Number(uploadOffsetText) - if (!Number.isInteger(remoteOffset)) { + const uploadOffset = Number(uploadOffsetText) + if (!Number.isInteger(uploadOffset)) { throw new Error('TUS upload returned an invalid Upload-Offset header') } - if (remoteOffset !== contentBytes.length) { - throw new Error(`TUS upload offset ${remoteOffset}, expected ${contentBytes.length}`) + if (uploadOffset !== contentBytes.length) { + throw new Error(`TUS upload offset ${uploadOffset}, expected ${contentBytes.length}`) } const completedAssembly = await this.waitForAssembly(createdAssembly.assembly_ssl_url ?? '') From 0a88f25d197324a6a9acc015e2def5de9711b8df Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 3 Jun 2026 05:05:48 +0200 Subject: [PATCH 08/12] Read TUS example input from SDK feature call --- .../api2-devdock-tus-assembly/main.ts | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/node/examples/api2-devdock-tus-assembly/main.ts b/packages/node/examples/api2-devdock-tus-assembly/main.ts index b4d126eb..5b334b53 100644 --- a/packages/node/examples/api2-devdock-tus-assembly/main.ts +++ b/packages/node/examples/api2-devdock-tus-assembly/main.ts @@ -61,26 +61,34 @@ function stringRecord(value: unknown, label: string): Record { ) } -function featurePreparation( +function optionalStringRecord(value: unknown, label: string): Record { + if (value == null) { + return {} + } + + return stringRecord(value, label) +} + +function sdkFeatureCall( scenario: JsonRecord, featureId: string, -): { label: string; preparation: JsonRecord } { - const preparations = requireArray(scenario.preparations, 'preparations') - for (const [index, rawPreparation] of preparations.entries()) { - const label = `preparations[${index}]` - const preparation = requireRecord(rawPreparation, label) - if (requireString(preparation.featureId, `${label}.featureId`) !== featureId) { +): { featureCall: JsonRecord; label: string } { + const featureCalls = requireArray(scenario.sdkFeatureCalls, 'sdkFeatureCalls') + for (const [index, rawFeatureCall] of featureCalls.entries()) { + const label = `sdkFeatureCalls[${index}]` + const featureCall = requireRecord(rawFeatureCall, label) + if (requireString(featureCall.featureId, `${label}.featureId`) !== featureId) { continue } - if (requireString(preparation.kind, `${label}.kind`) !== 'feature-call') { - fail(`${label} must be a feature-call preparation`) + if (requireString(featureCall.kind, `${label}.kind`) !== 'sdk-feature-call') { + fail(`${label} must be an sdk-feature-call`) } - return { label, preparation } + return { featureCall, label } } - fail(`scenario has no preparation for feature ${JSON.stringify(featureId)}`) + fail(`scenario has no SDK feature call for feature ${JSON.stringify(featureId)}`) } async function loadScenario(): Promise { @@ -91,26 +99,17 @@ async function loadScenario(): Promise { return requireRecord(parsed, 'scenario') } -function fileCount(scenario: JsonRecord): number { - const { label, preparation } = featurePreparation(scenario, 'createTusAssembly') - const input = requireRecord(preparation.input, `${label}.input`) +function uploadTusAssemblyInput(scenario: JsonRecord): JsonRecord { + const { featureCall, label } = sdkFeatureCall(scenario, 'uploadTusAssembly') - return requireNumber(input.file_count, `${label}.input.file_count`) + return requireRecord(featureCall.input, `${label}.input`) } function scenarioBytes(uploadConfig: JsonRecord): Buffer { - const source = requireRecord(uploadConfig.source, 'upload.source') - const kind = requireString(source.kind, 'upload.source.kind') - const encoding = requireString(source.encoding, 'upload.source.encoding') - if (kind !== 'bytes') { - fail(`unsupported scenario source kind ${JSON.stringify(kind)}`) - } - - if (encoding !== 'utf8') { - fail(`unsupported scenario source encoding ${JSON.stringify(encoding)}`) - } - - return Buffer.from(requireString(source.value, 'upload.source.value'), 'utf8') + return Buffer.from( + requireString(uploadConfig.content, 'sdkFeatureCalls.uploadTusAssembly.input.upload.content'), + 'utf8', + ) } async function writeResult(result: JsonRecord): Promise { @@ -124,7 +123,8 @@ async function writeResult(result: JsonRecord): Promise { async function main(): Promise { const scenario = await loadScenario() - const upload = requireRecord(scenario.upload, 'upload') + const input = uploadTusAssemblyInput(scenario) + const upload = requireRecord(input.upload, 'sdkFeatureCalls.uploadTusAssembly.input.upload') const client = new Transloadit({ authKey: requiredEnv('TRANSLOADIT_KEY'), authSecret: requiredEnv('TRANSLOADIT_SECRET'), @@ -132,11 +132,14 @@ async function main(): Promise { }) const result = await client.uploadTusAssembly( - fileCount(scenario), + requireNumber(input.file_count, 'sdkFeatureCalls.uploadTusAssembly.input.file_count'), scenarioBytes(upload), - requireString(upload.fieldName, 'upload.fieldName'), - requireString(upload.fileName, 'upload.fileName'), - stringRecord(upload.userMeta, 'upload.userMeta'), + requireString(upload.fieldname, 'sdkFeatureCalls.uploadTusAssembly.input.upload.fieldname'), + requireString(upload.filename, 'sdkFeatureCalls.uploadTusAssembly.input.upload.filename'), + optionalStringRecord( + upload.user_meta, + 'sdkFeatureCalls.uploadTusAssembly.input.upload.user_meta', + ), ) await writeResult({ From 49bde3b8c08a590234487c9f21f9b776e3c5f09d Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 3 Jun 2026 20:51:26 +0200 Subject: [PATCH 09/12] Read SDK example input projection --- .../api2-devdock-tus-assembly/main.ts | 115 ++++++++++-------- 1 file changed, 65 insertions(+), 50 deletions(-) diff --git a/packages/node/examples/api2-devdock-tus-assembly/main.ts b/packages/node/examples/api2-devdock-tus-assembly/main.ts index 5b334b53..25c7d90e 100644 --- a/packages/node/examples/api2-devdock-tus-assembly/main.ts +++ b/packages/node/examples/api2-devdock-tus-assembly/main.ts @@ -5,6 +5,29 @@ import { Transloadit } from '../../src/Transloadit.ts' type JsonRecord = Record +interface ExampleInput { + scenarioId: string + sdkFeatureInputs: { + uploadTusAssembly: UploadTusAssemblyInput + } +} + +interface TusAssemblyScenario { + exampleInput: ExampleInput +} + +interface UploadConfig { + content: string + fieldname: string + filename: string + user_meta: Record +} + +interface UploadTusAssemblyInput { + file_count: number + upload: UploadConfig +} + function fail(message: string): never { throw new Error(message) } @@ -46,14 +69,6 @@ function requireNumber(value: unknown, label: string): number { return value } -function requireArray(value: unknown, label: string): unknown[] { - if (!Array.isArray(value)) { - fail(`${label} must be an array`) - } - - return value -} - function stringRecord(value: unknown, label: string): Record { const record = requireRecord(value, label) return Object.fromEntries( @@ -69,47 +84,50 @@ function optionalStringRecord(value: unknown, label: string): Record { +async function loadScenario(): Promise { const scenarioPath = process.env.API2_SDK_EXAMPLE_SCENARIO ?? path.join(import.meta.dirname, 'api2-scenario.json') const parsed: unknown = JSON.parse(await readFile(scenarioPath, 'utf8')) + const scenario = requireRecord(parsed, 'scenario') - return requireRecord(parsed, 'scenario') -} - -function uploadTusAssemblyInput(scenario: JsonRecord): JsonRecord { - const { featureCall, label } = sdkFeatureCall(scenario, 'uploadTusAssembly') - - return requireRecord(featureCall.input, `${label}.input`) -} - -function scenarioBytes(uploadConfig: JsonRecord): Buffer { - return Buffer.from( - requireString(uploadConfig.content, 'sdkFeatureCalls.uploadTusAssembly.input.upload.content'), - 'utf8', - ) + return { + exampleInput: exampleInput(scenario.exampleInput, 'scenario.exampleInput'), + } } async function writeResult(result: JsonRecord): Promise { @@ -123,8 +141,8 @@ async function writeResult(result: JsonRecord): Promise { async function main(): Promise { const scenario = await loadScenario() - const input = uploadTusAssemblyInput(scenario) - const upload = requireRecord(input.upload, 'sdkFeatureCalls.uploadTusAssembly.input.upload') + const input = scenario.exampleInput.sdkFeatureInputs.uploadTusAssembly + const upload = input.upload const client = new Transloadit({ authKey: requiredEnv('TRANSLOADIT_KEY'), authSecret: requiredEnv('TRANSLOADIT_SECRET'), @@ -132,14 +150,11 @@ async function main(): Promise { }) const result = await client.uploadTusAssembly( - requireNumber(input.file_count, 'sdkFeatureCalls.uploadTusAssembly.input.file_count'), - scenarioBytes(upload), - requireString(upload.fieldname, 'sdkFeatureCalls.uploadTusAssembly.input.upload.fieldname'), - requireString(upload.filename, 'sdkFeatureCalls.uploadTusAssembly.input.upload.filename'), - optionalStringRecord( - upload.user_meta, - 'sdkFeatureCalls.uploadTusAssembly.input.upload.user_meta', - ), + input.file_count, + Buffer.from(upload.content, 'utf8'), + upload.fieldname, + upload.filename, + upload.user_meta, ) await writeResult({ @@ -149,7 +164,7 @@ async function main(): Promise { }) console.log( - `Node SDK devdock scenario ${requireString(scenario.scenarioId, 'scenarioId')} uploaded to ${result.uploadUrl}`, + `Node SDK devdock scenario ${scenario.exampleInput.scenarioId} uploaded to ${result.uploadUrl}`, ) } From feefa6dde506fca7b4f0cba2912a4930a9996984 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 5 Jun 2026 17:07:54 +0200 Subject: [PATCH 10/12] Regenerate contract-owned TUS Assembly surfaces --- packages/node/src/Transloadit.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/node/src/Transloadit.ts b/packages/node/src/Transloadit.ts index 5e1768bc..08d3d1b5 100644 --- a/packages/node/src/Transloadit.ts +++ b/packages/node/src/Transloadit.ts @@ -108,11 +108,19 @@ export type AssemblyStatusWithUploadUrls = AssemblyStatus & { upload_urls?: Record } +// + +// This block is generated from Transloadit API2 contracts. If it looks wrong, +// please report the issue instead of editing this block by hand; the source fix +// belongs in the contract generator so all SDKs stay in sync. + export interface UploadTusAssemblyResult { assembly: AssemblyStatus uploadUrl: string } +// + const { version } = packageJson export type AssemblyProgress = (assembly: AssemblyStatus) => void @@ -586,6 +594,9 @@ export class Transloadit { // please report the issue instead of editing this block by hand; the source fix // belongs in the contract generator so all SDKs stay in sync. + /** + * Creates a TUS-ready Assembly that waits for the requested number of resumable uploads before execution continues. + */ async createTusAssembly(fileCount: number): Promise { return await this._remoteJson< AssemblyStatusWithUploadUrls, @@ -617,6 +628,10 @@ export class Transloadit { // please report the issue instead of editing this block by hand; the source fix // belongs in the contract generator so all SDKs stay in sync. + /** + * Waits for an Assembly to finish uploading and executing. + * Use the returned assembly_ssl_url as the assembly URL. + */ async waitForAssembly(assemblyUrl: string): Promise { while (true) { const result = await this._remoteJson({ @@ -647,6 +662,9 @@ export class Transloadit { // please report the issue instead of editing this block by hand; the source fix // belongs in the contract generator so all SDKs stay in sync. + /** + * Creates a TUS-ready Assembly, uploads one file with the TUS protocol, and waits for the Assembly to finish. + */ async uploadTusAssembly( fileCount: number, content: Buffer | Uint8Array | string, From 8b235a10e195e8ab668922b996b0b37d0abe96fb Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 5 Jun 2026 17:13:48 +0200 Subject: [PATCH 11/12] Skip coverage publish on pull requests --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2d1e9ea..d1b1296c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -229,7 +229,10 @@ jobs: env: COVERAGE_REPO_SSH_PRIVATE_KEY: ${{ secrets.COVERAGE_REPO_SSH_PRIVATE_KEY }} run: | - if [ -n "$COVERAGE_REPO_SSH_PRIVATE_KEY" ]; then + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "Coverage publish skipped for pull request runs." + elif [ -n "$COVERAGE_REPO_SSH_PRIVATE_KEY" ]; then echo "enabled=true" >> "$GITHUB_OUTPUT" else echo "enabled=false" >> "$GITHUB_OUTPUT" From eaa74e8abd4da469465626bcbb0a579f9f34b7bc Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 5 Jun 2026 18:29:58 +0200 Subject: [PATCH 12/12] Regenerate required feature value guards --- packages/node/src/Transloadit.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/node/src/Transloadit.ts b/packages/node/src/Transloadit.ts index 08d3d1b5..0693f3e9 100644 --- a/packages/node/src/Transloadit.ts +++ b/packages/node/src/Transloadit.ts @@ -750,7 +750,11 @@ export class Transloadit { throw new Error(`TUS upload offset ${uploadOffset}, expected ${contentBytes.length}`) } - const completedAssembly = await this.waitForAssembly(createdAssembly.assembly_ssl_url ?? '') + const createdAssemblyAssemblySslUrl = createdAssembly.assembly_ssl_url + if (!createdAssemblyAssemblySslUrl) { + throw new Error('uploadTusAssembly needs createdAssembly.assembly_ssl_url') + } + const completedAssembly = await this.waitForAssembly(createdAssemblyAssemblySslUrl) return { assembly: completedAssembly, uploadUrl: uploadUrlText } }