├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── eslint.config.mjs ├── lib ├── CloudConvert.ts ├── JobsResource.ts ├── SignedUrlResource.ts ├── TasksResource.ts ├── UsersResource.ts ├── WebhooksResource.ts └── index.ts ├── package.json ├── tests ├── .eslintrc.js ├── integration │ ├── ApiKey.ts │ ├── JobsResourceTest.ts │ ├── TasksResourceTest.ts │ ├── UsersResourceTest.ts │ └── files │ │ ├── input.pdf │ │ └── input.png └── unit │ ├── JobsResourceTest.ts │ ├── SignedUrlResourceTest.ts │ ├── TasksResourceTest.ts │ ├── UsersResourceTest.ts │ ├── WebhooksResourceTest.ts │ ├── requests │ └── webhook_job_finished_payload.json │ └── responses │ ├── job.json │ ├── job_created.json │ ├── job_finished.json │ ├── jobs.json │ ├── task.json │ ├── task_created.json │ ├── tasks.json │ ├── upload_task_created.json │ └── user.json └── tsconfig.json /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | max-parallel: 2 18 | matrix: 19 | node-version: [20, 22, 23] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm install -d 29 | - run: npm run lint 30 | - run: npm test 31 | - run: npm run test-integration 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules/ 4 | package-lock.json 5 | built/ 6 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "tsx", 3 | "spec": [ 4 | "tests/**/*.ts" 5 | ] 6 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .travis.yml 4 | .eslintrc 5 | .eslintignore 6 | .editorconfig 7 | .babelrc 8 | .gitignore 9 | .git 10 | .github/ 11 | lib/ 12 | tests/ 13 | built/node_modules/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | built/ 3 | package-lock.json 4 | package.json 5 | tests/unit/requests/ 6 | tests/unit/responses/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "htmlWhitespaceSensitivity": "ignore", 5 | "tabWidth": 4, 6 | "semi": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": "explicit" 5 | }, 6 | "[md]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[typescript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The License (MIT) 2 | 3 | Copyright (c) 2017 Josias Montag 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloudconvert-node 2 | 3 | This is the official Node.js SDK for the [CloudConvert](https://cloudconvert.com/api/v2) API v2. 4 | 5 | [![Node.js Run Tests](https://github.com/cloudconvert/cloudconvert-node/actions/workflows/run-tests.yml/badge.svg)](https://github.com/cloudconvert/cloudconvert-node/actions/workflows/run-tests.yml) 6 | [![npm](https://img.shields.io/npm/v/cloudconvert.svg)](https://www.npmjs.com/package/cloudconvert) 7 | [![npm](https://img.shields.io/npm/dt/cloudconvert.svg)](https://www.npmjs.com/package/cloudconvert) 8 | 9 | ## Installation 10 | 11 | npm install --save cloudconvert 12 | 13 | Load as ESM module: 14 | 15 | ```js 16 | import CloudConvert from 'cloudconvert'; 17 | ``` 18 | 19 | ... or via require: 20 | 21 | ```js 22 | const CloudConvert = require('cloudconvert'); 23 | ``` 24 | 25 | ## Creating Jobs 26 | 27 | ```js 28 | import CloudConvert from 'cloudconvert'; 29 | 30 | const cloudConvert = new CloudConvert('api_key'); 31 | 32 | let job = await cloudConvert.jobs.create({ 33 | tasks: { 34 | 'import-my-file': { 35 | operation: 'import/url', 36 | url: 'https://my-url' 37 | }, 38 | 'convert-my-file': { 39 | operation: 'convert', 40 | input: 'import-my-file', 41 | output_format: 'pdf', 42 | some_other_option: 'value' 43 | }, 44 | 'export-my-file': { 45 | operation: 'export/url', 46 | input: 'convert-my-file' 47 | } 48 | } 49 | }); 50 | ``` 51 | 52 | You can use the [CloudConvert Job Builder](https://cloudconvert.com/api/v2/jobs/builder) to see the available options for the various task types. 53 | 54 | ## Downloading Files 55 | 56 | CloudConvert can generate public URLs for using `export/url` tasks. You can use these URLs to download output files. 57 | 58 | ```js 59 | job = await cloudConvert.jobs.wait(job.id); // Wait for job completion 60 | 61 | const file = this.cloudConvert.jobs.getExportUrls(job)[0]; 62 | 63 | const writeStream = fs.createWriteStream('./out/' + file.filename); 64 | 65 | https.get(file.url, function (response) { 66 | response.pipe(writeStream); 67 | }); 68 | 69 | await new Promise((resolve, reject) => { 70 | writeStream.on('finish', resolve); 71 | writeStream.on('error', reject); 72 | }); 73 | ``` 74 | 75 | ## Uploading Files 76 | 77 | Uploads to CloudConvert are done via `import/upload` tasks (see the [docs](https://cloudconvert.com/api/v2/import#import-upload-tasks)). This SDK offers a convenient upload method: 78 | 79 | ```js 80 | const job = await cloudConvert.jobs.create({ 81 | tasks: { 82 | 'upload-my-file': { 83 | operation: 'import/upload' 84 | } 85 | // ... 86 | } 87 | }); 88 | 89 | const uploadTask = job.tasks.find(task => task.name === 'upload-my-file'); 90 | 91 | const inputFile = fs.createReadStream('./file.pdf'); 92 | 93 | await cloudConvert.tasks.upload(uploadTask, inputFile, 'file.pdf'); 94 | ``` 95 | 96 | > **Note on custom streams**: 97 | The length of the stream needs to be known prior to uploading. The SDK tries to automatically detect the file size of file-based read streams. If you are using a custom stream, you might need to pass a `fileSize` as fourth parameter to the `upload()` method. 98 | 99 | ## Websocket Events 100 | 101 | The node SDK can subscribe to events of the [CloudConvert socket.io API](https://cloudconvert.com/api/v2/socket#socket). 102 | 103 | ```js 104 | const job = await cloudConvert.jobs.create({ ... }); 105 | 106 | // Events for the job 107 | // Available events: created, updated, finished, failed 108 | cloudConvert.jobs.subscribeEvent(job.id, 'finished', event => { 109 | // Job has finished 110 | console.log(event.job); 111 | }); 112 | 113 | // Events for all tasks of the job 114 | // Available events: created, updated, finished, failed 115 | cloudConvert.jobs.subscribeTaskEvent(job.id, 'finished', event => { 116 | // Task has finished 117 | console.log(event.task); 118 | }); 119 | ``` 120 | 121 | When you don't want to receive any events any more you should close the socket: 122 | 123 | ```js 124 | cloudConvert.closeSocket(); 125 | ``` 126 | 127 | ## Webhook Signing 128 | 129 | The node SDK allows to verify webhook requests received from CloudConvert. 130 | 131 | ```js 132 | const payloadString = '...'; // The JSON string from the raw request body. 133 | const signature = '...'; // The value of the "CloudConvert-Signature" header. 134 | const signingSecret = '...'; // You can find it in your webhook settings. 135 | 136 | const isValid = cloudConvert.webhooks.verify( 137 | payloadString, 138 | signature, 139 | signingSecret 140 | ); // returns true or false 141 | ``` 142 | 143 | ## Signed URLs 144 | 145 | Signed URLs allow converting files on demand only using URL query parameters. The node.js SDK allows to generate such URLs. Therefore, you need to obtain a signed URL base and a signing secret on the [CloudConvert Dashboard](https://cloudconvert.com/dashboard/api/v2/signed-urls). 146 | 147 | ```js 148 | const signedUrlBase = 'https://s.cloudconvert.com/...'; // You can find it in your signed URL settings. 149 | const signingSecret = '...'; // You can find it in your signed URL settings. 150 | const cacheKey = 'cache-key'; // Allows caching of the result file for 24h 151 | 152 | const job = { 153 | tasks: { 154 | 'import-it': { 155 | operation: 'import/url', 156 | url: 'https://some.url', 157 | filename: 'logo.png' 158 | }, 159 | 'export-it': { 160 | operation: 'export/url', 161 | input: 'import-it', 162 | inline: true 163 | } 164 | } 165 | }; 166 | 167 | const url = cloudConvert.signedUrls.sign( 168 | signedUrlBase, 169 | signingSecret, 170 | job, 171 | cacheKey 172 | ); // returns the generated URL 173 | ``` 174 | 175 | ## Using the Sandbox 176 | 177 | You can use the Sandbox to avoid consuming your quota while testing your application. The node SDK allows you to do that. 178 | 179 | ```js 180 | // Pass `true` to the constructor 181 | const cloudConvert = new CloudConvert('api_key', true); 182 | ``` 183 | 184 | > Don't forget to generate MD5 Hashes for the files you will use for testing. 185 | 186 | ## Setting a Region 187 | 188 | By default, the region in your [account settings](https://cloudconvert.com/dashboard/region) is used. Alternatively, you can set a fixed region: 189 | 190 | ```js 191 | // Pass the region as third argument to the constructor 192 | const cloudConvert = new CloudConvert('api_key', false, 'us-east'); 193 | ``` 194 | 195 | ## Contributing 196 | 197 | This section is intended for people who want to contribute to the development of this library. 198 | 199 | ### Getting started 200 | 201 | Begin with installing the necessary dependencies by running 202 | 203 | npm install 204 | 205 | in the root directory of this repository. 206 | 207 | ### Building 208 | 209 | This project is written in TypeScript so it needs to be compiled first: 210 | 211 | npm run build 212 | 213 | This will compile the code in the `lib` directory and generate a `built` directory containing the JS files and the type declarations. 214 | 215 | ### Unit Tests 216 | 217 | Tests are based on mocha: 218 | 219 | npm run test 220 | 221 | ### Integration Tests 222 | 223 | npm run test-integration 224 | 225 | By default, this runs the integration tests against the Sandbox API with an official CloudConvert account. If you would like to use your own account, you can set your API key using the `CLOUDCONVERT_API_KEY` enviroment variable. In this case you need to whitelist the following MD5 hashes for Sandbox API (using the CloudConvert dashboard). 226 | 227 | 53d6fe6b688c31c565907c81de625046 input.pdf 228 | 99d4c165f77af02015aa647770286cf9 input.png 229 | 230 | ### Linting 231 | 232 | The project is linted by ESLint+Prettier. 233 | 234 | If you're using VSCode, all files will be linted automatically upon saving. 235 | Otherwise, you can lint the project by running 236 | 237 | npm run lint 238 | 239 | and even auto-fix as many things as possible by running 240 | 241 | npm run lint -- --fix 242 | 243 | ## Resources 244 | 245 | - [API v2 Documentation](https://cloudconvert.com/api/v2) 246 | - [CloudConvert Blog](https://cloudconvert.com/blog) 247 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from 'eslint/config'; 2 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 3 | import globals from 'globals'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import path from 'node:path'; 6 | import { fileURLToPath } from 'node:url'; 7 | import js from '@eslint/js'; 8 | import { FlatCompat } from '@eslint/eslintrc'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all 16 | }); 17 | 18 | export default defineConfig([ 19 | globalIgnores([ 20 | '**/node_modules/', 21 | '**/built/', 22 | '**/package-lock.json', 23 | '**/package.json', 24 | 'tests/unit/requests/', 25 | 'tests/unit/responses/', 26 | '.vscode/', 27 | 'tsconfig.json', 28 | '.mocharc.json' 29 | ]), 30 | { 31 | extends: compat.extends( 32 | 'eslint:recommended', 33 | 'plugin:@typescript-eslint/recommended' 34 | ), 35 | 36 | plugins: { 37 | '@typescript-eslint': typescriptEslint 38 | }, 39 | 40 | languageOptions: { 41 | globals: { 42 | ...globals.browser, 43 | ...globals.node 44 | }, 45 | 46 | parser: tsParser, 47 | ecmaVersion: 'latest', 48 | sourceType: 'module' 49 | }, 50 | 51 | rules: { 52 | '@typescript-eslint/no-explicit-any': 'off' 53 | } 54 | } 55 | ]); 56 | -------------------------------------------------------------------------------- /lib/CloudConvert.ts: -------------------------------------------------------------------------------- 1 | import { statSync } from 'node:fs'; 2 | import { basename } from 'node:path'; 3 | import { Readable } from 'node:stream'; 4 | import { io, type Socket } from 'socket.io-client'; 5 | import { version } from '../package.json'; 6 | import JobsResource, { type JobEventData } from './JobsResource'; 7 | import SignedUrlResource from './SignedUrlResource'; 8 | import TasksResource, { 9 | type JobTaskEventData, 10 | type TaskEventData 11 | } from './TasksResource'; 12 | import UsersResource from './UsersResource'; 13 | import WebhooksResource from './WebhooksResource'; 14 | 15 | export type UploadFileSource = 16 | | Blob 17 | | Uint8Array 18 | | Iterable 19 | | AsyncIterable 20 | | NodeJS.ReadableStream; 21 | 22 | async function* unifySources( 23 | data: UploadFileSource 24 | ): AsyncIterable { 25 | if (data instanceof Uint8Array) { 26 | yield data; 27 | return; 28 | } 29 | 30 | if (data instanceof Blob) { 31 | yield data.bytes(); 32 | return; 33 | } 34 | 35 | if (Symbol.iterator in data) { 36 | yield* data; 37 | return; 38 | } 39 | 40 | if (Symbol.asyncIterator in data) { 41 | for await (const chunk of data) { 42 | if (typeof chunk === 'string') 43 | throw new Error( 44 | 'bad file data, received string but expected Uint8Array' 45 | ); 46 | yield chunk; 47 | } 48 | return; 49 | } 50 | } 51 | 52 | function guessNameAndSize( 53 | source: UploadFileSource, 54 | fileName?: string, 55 | fileSize?: number 56 | ): { name: string; size: number } { 57 | const path = 58 | 'path' in source && typeof source.path === 'string' 59 | ? source.path 60 | : undefined; 61 | const name = 62 | fileName ?? (path !== undefined ? basename(path) : undefined) ?? 'file'; 63 | const size = 64 | fileSize ?? 65 | (source instanceof Uint8Array ? source.byteLength : undefined) ?? 66 | (source instanceof Blob ? source.size : undefined) ?? 67 | (path !== undefined 68 | ? statSync(path, { throwIfNoEntry: false })?.size 69 | : undefined); 70 | if (size === undefined) { 71 | throw new Error( 72 | 'Could not determine the number of bytes, specify it explicitly when calling `upload`' 73 | ); 74 | } 75 | return { name, size }; 76 | } 77 | 78 | export class UploadFile { 79 | private readonly attributes: Array<[key: string, value: unknown]> = []; 80 | private readonly data: AsyncIterable; 81 | private readonly filename?: string; 82 | private readonly fileSize: number; 83 | constructor(data: UploadFileSource, filename?: string, fileSize?: number) { 84 | this.data = unifySources(data); 85 | const { name, size } = guessNameAndSize(data, filename, fileSize); 86 | this.filename = name; 87 | this.fileSize = size; 88 | } 89 | add(key: string, value: unknown) { 90 | this.attributes.push([key, value]); 91 | } 92 | toMultiPart(boundary: string): { 93 | size: number; 94 | stream: AsyncIterable; 95 | } { 96 | const enc = new TextEncoder(); 97 | const prefix: Uint8Array[] = []; 98 | const suffix: Uint8Array[] = []; 99 | 100 | // Start multipart/form-data protocol 101 | prefix.push(enc.encode(`--${boundary}\r\n`)); 102 | // Send all attributes 103 | const separator = enc.encode(`\r\n--${boundary}\r\n`); 104 | let first = true; 105 | for (const [key, value] of this.attributes) { 106 | if (value == null) continue; 107 | if (!first) prefix.push(separator); 108 | prefix.push( 109 | enc.encode( 110 | `content-disposition:form-data;name="${key}"\r\n\r\n${value}` 111 | ) 112 | ); 113 | first = false; 114 | } 115 | // Send file 116 | if (!first) prefix.push(separator); 117 | prefix.push( 118 | enc.encode( 119 | `content-disposition:form-data;name="file";filename=${this.filename}\r\ncontent-type:application/octet-stream\r\n\r\n` 120 | ) 121 | ); 122 | const data = this.data; 123 | // End multipart/form-data protocol 124 | suffix.push(enc.encode(`\r\n--${boundary}--\r\n`)); 125 | 126 | const size = 127 | prefix.reduce((sum, arr) => sum + arr.byteLength, 0) + 128 | this.fileSize + 129 | suffix.reduce((sum, arr) => sum + arr.byteLength, 0); 130 | async function* concat() { 131 | yield* prefix; 132 | yield* data; 133 | yield* suffix; 134 | } 135 | return { size, stream: concat() }; 136 | } 137 | } 138 | 139 | export default class CloudConvert { 140 | private socket: Socket | undefined; 141 | private subscribedChannels: Map | undefined; 142 | 143 | public readonly apiKey: string; 144 | public readonly useSandbox: boolean; 145 | public readonly region: string | null; 146 | 147 | public tasks!: TasksResource; 148 | public jobs!: JobsResource; 149 | public users!: UsersResource; 150 | public webhooks!: WebhooksResource; 151 | public signedUrls!: SignedUrlResource; 152 | 153 | constructor(apiKey: string, useSandbox = false, region = null) { 154 | this.apiKey = apiKey; 155 | this.useSandbox = useSandbox; 156 | this.region = region; 157 | 158 | this.tasks = new TasksResource(this); 159 | this.jobs = new JobsResource(this); 160 | this.users = new UsersResource(this); 161 | this.webhooks = new WebhooksResource(); 162 | this.signedUrls = new SignedUrlResource(); 163 | } 164 | 165 | async call( 166 | method: 'GET' | 'POST' | 'DELETE', 167 | route: string, 168 | parameters?: UploadFile | object, 169 | options?: { presigned?: boolean; flat?: boolean } 170 | ) { 171 | const baseURL = this.useSandbox 172 | ? 'https://api.sandbox.cloudconvert.com/v2/' 173 | : `https://${ 174 | this.region ? this.region + '.' : '' 175 | }api.cloudconvert.com/v2/`; 176 | return await this.callWithBase( 177 | baseURL, 178 | method, 179 | route, 180 | parameters, 181 | options 182 | ); 183 | } 184 | 185 | async callWithBase( 186 | baseURL: string, 187 | method: 'GET' | 'POST' | 'DELETE', 188 | route: string, 189 | parameters?: UploadFile | object, 190 | options?: { presigned?: boolean; flat?: boolean } 191 | ) { 192 | const presigned = options?.presigned ?? false; 193 | const flat = options?.flat ?? false; 194 | const url = new URL(route, baseURL); 195 | const { contentLength, contentType, search, body } = prepareParameters( 196 | method, 197 | parameters 198 | ); 199 | if (search !== undefined) { 200 | url.search = search; 201 | } 202 | const headers = { 203 | 'User-Agent': `cloudconvert-node/v${version} (https://github.com/cloudconvert/cloudconvert-node)`, 204 | ...(!presigned ? { Authorization: `Bearer ${this.apiKey}` } : {}), 205 | ...(contentLength ? { 'Content-Length': contentLength } : {}), 206 | ...(contentType ? { 'Content-Type': contentType } : {}) 207 | }; 208 | const res = await fetch(url, { 209 | method, 210 | headers, 211 | body, 212 | // @ts-expect-error incorrect types in @types/node@20 213 | duplex: 'half' 214 | }); 215 | if (!res.ok) { 216 | // @ts-expect-error cause not present in types yet 217 | throw new Error(res.statusText, { cause: res }); 218 | } 219 | 220 | if ( 221 | !res.headers 222 | .get('content-type') 223 | ?.toLowerCase() 224 | .includes('application/json') 225 | ) { 226 | return undefined; 227 | } 228 | const json = await res.json(); 229 | return flat ? json : json.data; 230 | } 231 | 232 | subscribe( 233 | channel: string, 234 | event: string, 235 | callback: 236 | | ((event: JobEventData) => void) 237 | | ((event: TaskEventData) => void) 238 | | ((event: JobTaskEventData) => void) 239 | ): void { 240 | if (!this.socket) { 241 | this.socket = io( 242 | this.useSandbox 243 | ? 'https://socketio.sandbox.cloudconvert.com' 244 | : 'https://socketio.cloudconvert.com', 245 | { transports: ['websocket'] } 246 | ); 247 | this.subscribedChannels = new Map(); 248 | } 249 | 250 | if (!this.subscribedChannels?.get(channel)) { 251 | this.socket.emit('subscribe', { 252 | channel, 253 | auth: { headers: { Authorization: `Bearer ${this.apiKey}` } } 254 | }); 255 | this.subscribedChannels?.set(channel, true); 256 | } 257 | 258 | this.socket.on( 259 | event, 260 | function (eventChannel: string, eventData: any): void { 261 | if (channel !== eventChannel) { 262 | return; 263 | } 264 | callback(eventData); 265 | } 266 | ); 267 | } 268 | 269 | closeSocket(): void { 270 | this.socket?.close(); 271 | } 272 | } 273 | 274 | function prepareParameters( 275 | method: 'GET' | 'POST' | 'DELETE', 276 | data?: UploadFile | object 277 | ): { 278 | contentLength?: string; 279 | contentType?: string; 280 | body?: string | ReadableStream; 281 | search?: string; 282 | } { 283 | if (data === undefined) { 284 | return {}; 285 | } 286 | 287 | if (method === 'GET') { 288 | // abort early if all data needs to go into the search params 289 | const entries = Object.entries(data ?? {}); 290 | return { search: new URLSearchParams(entries).toString() }; 291 | } 292 | 293 | if (data instanceof UploadFile) { 294 | const boundary = `----------${Array.from(Array(32)) 295 | .map(() => Math.random().toString(36)[2] || 0) 296 | .join('')}`; 297 | const { size, stream } = data.toMultiPart(boundary); 298 | return { 299 | contentLength: size.toString(), 300 | contentType: `multipart/form-data; boundary=${boundary}`, 301 | body: asyncIterableToReadableStream(stream) 302 | }; 303 | } 304 | 305 | return { contentType: 'application/json', body: JSON.stringify(data) }; 306 | } 307 | 308 | function asyncIterableToReadableStream( 309 | it: AsyncIterable 310 | ): ReadableStream { 311 | const r = Readable.from(it); 312 | return Readable.toWeb(r) as ReadableStream; 313 | } 314 | -------------------------------------------------------------------------------- /lib/JobsResource.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from './CloudConvert'; 2 | import { 3 | type FileResult, 4 | type Operation, 5 | type Task, 6 | type TaskEventData, 7 | type TaskStatus 8 | } from './TasksResource'; 9 | 10 | export type JobEvent = 'created' | 'updated' | 'finished' | 'failed'; 11 | export type JobStatus = 'processing' | 'finished' | 'error'; 12 | export type JobTaskStatus = Task['status'] | 'queued'; 13 | export interface JobEventData { 14 | job: Job; 15 | } 16 | 17 | export interface Job { 18 | id: string; 19 | tag: string | null; 20 | status: TaskStatus; 21 | created_at: string; 22 | started_at: string | null; 23 | ended_at: string | null; 24 | tasks: JobTask[]; 25 | } 26 | type NotPresentWhenInsideJob = 'job_id' | 'status'; 27 | export interface JobTask extends Omit { 28 | name: string; 29 | status: JobTaskStatus; 30 | } 31 | 32 | export default class JobsResource { 33 | private readonly cloudConvert: CloudConvert; 34 | 35 | constructor(cloudConvert: CloudConvert) { 36 | this.cloudConvert = cloudConvert; 37 | } 38 | 39 | async get(id: string, query?: object): Promise { 40 | return await this.cloudConvert.call('GET', `jobs/${id}`, query); 41 | } 42 | 43 | async wait(id: string): Promise { 44 | const baseURL = this.cloudConvert.useSandbox 45 | ? 'https://sync.api.sandbox.cloudconvert.com/v2/' 46 | : `https://${ 47 | this.cloudConvert.region ? this.cloudConvert.region + '.' : '' 48 | }sync.api.cloudconvert.com/v2/`; 49 | return await this.cloudConvert.callWithBase( 50 | baseURL, 51 | 'GET', 52 | `jobs/${id}` 53 | ); 54 | } 55 | 56 | async all(query?: { 57 | 'filter[status]'?: JobStatus; 58 | 'filter[tag]'?: string; 59 | include?: string; 60 | per_page?: number; 61 | page?: number; 62 | }): Promise { 63 | return await this.cloudConvert.call('GET', 'jobs', query); 64 | } 65 | 66 | // See below for an explanation on how this type signature works 67 | async create(data?: JobTemplate): Promise { 68 | return await this.cloudConvert.call('POST', 'jobs', data); 69 | } 70 | 71 | async delete(id: string): Promise { 72 | await this.cloudConvert.call('DELETE', `jobs/${id}`); 73 | } 74 | 75 | async subscribeEvent( 76 | id: string, 77 | event: string, 78 | callback: (event: JobEventData) => void 79 | ) { 80 | this.cloudConvert.subscribe( 81 | `private-job.${id}`, 82 | `job.${event}`, 83 | callback 84 | ); 85 | } 86 | 87 | subscribeTaskEvent( 88 | id: string, 89 | event: string, 90 | callback: (event: TaskEventData) => void 91 | ) { 92 | this.cloudConvert.subscribe( 93 | `private-job.${id}.tasks`, 94 | `task.${event}`, 95 | callback 96 | ); 97 | } 98 | 99 | getExportUrls(job: Job): FileResult[] { 100 | return job.tasks 101 | .filter( 102 | task => 103 | task.operation === 'export/url' && 104 | task.status === 'finished' 105 | ) 106 | .flatMap(task => task.result?.files ?? []); 107 | } 108 | } 109 | 110 | // We need to map the types from the large Operation union type 111 | // to the template syntax from the API specs (confer the README) 112 | // that is used to create a job with a number of tasks. While this 113 | // is possible to write in just two lines of code, we divide this 114 | // up in many small steps in order to explain what's happening: 115 | 116 | // All possible operation strings ("import/url" etc) 117 | type PossibleOperationStrings = Operation['operation']; 118 | // Every argument in the tasks object should be assignable to this (for some operation string O) 119 | interface NamedOperation { 120 | operation: O; 121 | } 122 | // Given an operation string O, get the operation for it 123 | type OperationByName = Extract>; 124 | // Given an operation string O, get the operation data for it 125 | type OperationData = OperationByName['data']; 126 | // Add all properties to task that can only occur in tasks that are inside jobs 127 | interface TaskExtras extends NamedOperation { 128 | ignore_error?: boolean; 129 | } 130 | // Every argument in the tasks object is typed by this (for some operation string O) 131 | type TaskTemplate = TaskExtras & OperationData; 132 | // Given a union type U of operation strings, turn each operation string into its TaskTemplate 133 | type Distribute = U extends unknown ? TaskTemplate : never; 134 | // Create a union of all possible tasks 135 | type PossibleOperations = Distribute; 136 | // Allow any number of names, each typed by a possible operation 137 | interface TaskContainer { 138 | [name: string]: PossibleOperations; 139 | } 140 | // Add the other properties that are required for job creation 141 | export interface JobTemplate { 142 | tasks: TaskContainer; 143 | tag?: string; 144 | webhook_url?: string; 145 | } 146 | -------------------------------------------------------------------------------- /lib/SignedUrlResource.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import { type JobTemplate } from './JobsResource'; 3 | 4 | export default class SignedUrlResource { 5 | sign( 6 | base: string, 7 | signingSecret: string, 8 | job: JobTemplate, 9 | cacheKey: string | null 10 | ): string { 11 | const json = JSON.stringify(job); 12 | const base64 = Buffer.from(json || '').toString('base64'); 13 | const base64UrlSafe = base64 14 | .replace('+', '-') 15 | .replace('/', '_') 16 | .replace(/=+$/, ''); 17 | 18 | let url = `${base}?job=${base64UrlSafe}`; 19 | 20 | if (cacheKey) { 21 | url += `&cache_key=${cacheKey}`; 22 | } 23 | 24 | const hmac = crypto.createHmac('sha256', signingSecret); 25 | const signature = hmac.update(Buffer.from(url, 'utf-8')).digest('hex'); 26 | 27 | url += `&s=${signature}`; 28 | 29 | return url; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/TasksResource.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert, { 2 | UploadFile, 3 | type UploadFileSource 4 | } from './CloudConvert'; 5 | import { type JobTask } from './JobsResource'; 6 | 7 | export type TaskEvent = 'created' | 'updated' | 'finished' | 'failed'; 8 | export type TaskStatus = 'waiting' | 'processing' | 'finished' | 'error'; 9 | 10 | export interface TaskEventData { 11 | task: Task; 12 | } 13 | 14 | export interface JobTaskEventData { 15 | task: JobTask; 16 | } 17 | 18 | export type Operation = ImportOperation | TaskOperation | ExportOperation; 19 | export type ImportOperation = 20 | | ImportUrl 21 | | ImportUpload 22 | | ImportBase64 23 | | ImportRaw 24 | | ImportS3 25 | | ImportAzureBlob 26 | | ImportGoogleCloudStorage 27 | | ImportOpenStack 28 | | ImportSFTP; 29 | export type TaskOperation = 30 | | TaskConvert 31 | | TaskOptimize 32 | | TaskWaterMark 33 | | TaskCapture 34 | | TaskThumbnail 35 | | TaskMerge 36 | | TaskArchive 37 | | TaskCommand 38 | | TaskMetadata 39 | | TaskMetadataWrite; 40 | export type ExportOperation = 41 | | ExportUrl 42 | | ExportS3 43 | | ExportAzureBlob 44 | | ExportGoogleCloudStorage 45 | | ExportOpenStack 46 | | ExportSFTP; 47 | 48 | interface ImportUrl { 49 | operation: 'import/url'; 50 | data: ImportUrlData; 51 | } 52 | 53 | export interface ImportUrlData { 54 | url: string; 55 | filename?: string; 56 | headers?: { [key: string]: string }; 57 | } 58 | 59 | interface ImportUpload { 60 | operation: 'import/upload'; 61 | data: ImportUploadData; 62 | } 63 | 64 | export interface ImportUploadData { 65 | redirect?: string; 66 | } 67 | 68 | interface ImportBase64 { 69 | operation: 'import/base64'; 70 | data: ImportBase64Data; 71 | } 72 | 73 | export interface ImportBase64Data { 74 | file: string; 75 | filename: string; 76 | } 77 | 78 | interface ImportRaw { 79 | operation: 'import/raw'; 80 | data: ImportRawData; 81 | } 82 | 83 | export interface ImportRawData { 84 | file: string; 85 | filename: string; 86 | } 87 | 88 | interface ImportS3 { 89 | operation: 'import/s3'; 90 | data: ImportS3Data; 91 | } 92 | 93 | export interface ImportS3Data { 94 | bucket: string; 95 | region: string; 96 | endpoint?: string; 97 | key?: string; 98 | key_prefix?: string; 99 | access_key_id: string; 100 | secret_access_key: string; 101 | session_token?: string; 102 | filename?: string; 103 | } 104 | 105 | interface ImportAzureBlob { 106 | operation: 'import/azure/blob'; 107 | data: ImportAzureBlobData; 108 | } 109 | 110 | export interface ImportAzureBlobData { 111 | storage_account: string; 112 | storage_access_key?: string; 113 | sas_token?: string; 114 | container: string; 115 | blob?: string; 116 | blob_prefix?: string; 117 | filename?: string; 118 | } 119 | 120 | interface ImportGoogleCloudStorage { 121 | operation: 'import/google-cloud-storage'; 122 | data: ImportGoogleCloudStorageData; 123 | } 124 | 125 | export interface ImportGoogleCloudStorageData { 126 | project_id: string; 127 | bucket: string; 128 | client_email: string; 129 | private_key: string; 130 | file?: string; 131 | file_prefix?: string; 132 | filename?: string; 133 | } 134 | 135 | interface ImportOpenStack { 136 | operation: 'import/openstack'; 137 | data: ImportOpenStackData; 138 | } 139 | 140 | export interface ImportOpenStackData { 141 | auth_url: string; 142 | username: string; 143 | password: string; 144 | region: string; 145 | container: string; 146 | file?: string; 147 | file_prefix?: string; 148 | filename?: string; 149 | } 150 | 151 | interface ImportSFTP { 152 | operation: 'import/sftp'; 153 | data: ImportSFTPData; 154 | } 155 | 156 | export interface ImportSFTPData { 157 | host: string; 158 | port?: number; 159 | username: string; 160 | password?: string; 161 | private_key?: string; 162 | file?: string; 163 | path?: string; 164 | filename?: string; 165 | } 166 | 167 | interface TaskConvert { 168 | operation: 'convert'; 169 | data: TaskConvertData; 170 | } 171 | 172 | export interface TaskConvertData { 173 | input: string | string[]; 174 | input_format?: string; 175 | output_format: string; 176 | engine?: string; 177 | engine_version?: string; 178 | filename?: string; 179 | timeout?: number; 180 | 181 | [option: string]: any; 182 | } 183 | 184 | interface TaskOptimize { 185 | operation: 'optimize'; 186 | data: TaskOptimizeData; 187 | } 188 | 189 | export interface TaskOptimizeData { 190 | input: string | string[]; 191 | input_format?: 'jpg' | 'png' | 'pdf'; 192 | engine?: string; 193 | engine_version?: string; 194 | filename?: string; 195 | timeout?: number; 196 | quality?: number; 197 | profile?: 'web' | 'print' | 'archive' | 'mrc' | 'max'; 198 | 199 | [option: string]: any; 200 | } 201 | 202 | interface TaskWaterMark { 203 | operation: 'watermark'; 204 | data: TaskWaterMarkData; 205 | } 206 | 207 | export interface TaskWaterMarkData { 208 | input?: string | string[]; 209 | input_format?: string; 210 | pages?: string; 211 | layer?: 'above' | 'below'; 212 | text?: string; 213 | font_size?: number; 214 | font_width_percent?: number; 215 | font_color?: string; 216 | font_name?: 217 | | 'Andale Mono' 218 | | 'Arial' 219 | | 'Arial Black' 220 | | 'Arial Bold' 221 | | 'Arial Bold Italic' 222 | | 'Arial Italic' 223 | | 'Courier New' 224 | | 'Courier New Bold' 225 | | 'Courier New Bold Italic' 226 | | 'Courier New Italic' 227 | | 'Georgia' 228 | | 'Georgia Bold' 229 | | 'Georgia Bold Italic' 230 | | 'Georgia Italic' 231 | | 'Helvetica' 232 | | 'Helvetica Bold' 233 | | 'Helvetica BoldOblique' 234 | | 'Helvetica Narrow Bold' 235 | | 'Helvetica Narrow BoldOblique' 236 | | 'Helvetica Oblique' 237 | | 'Impact' 238 | | 'Times New Roman' 239 | | 'Times New Roman Bold' 240 | | 'Times New Roman Bold Italic' 241 | | 'Times New Roman Italic' 242 | | 'Trebuchet MS' 243 | | 'Trebuchet MS Bold' 244 | | 'Trebuchet MS Bold Italic' 245 | | 'Trebuchet MS Italic' 246 | | 'Verdana' 247 | | 'Verdana Bold' 248 | | 'Verdana Bold Italic' 249 | | 'Verdana Italic'; 250 | font_align?: 'left' | 'center' | 'right'; 251 | image?: string; 252 | image_width?: number; 253 | image_height?: number; 254 | image_width_percent?: number; 255 | position_vertical?: 'top' | 'center' | 'bottom'; 256 | position_horizontal?: 'left' | 'center' | 'right'; 257 | margin_vertical?: number; 258 | margin_horizontal?: number; 259 | opacity?: number; 260 | rotation?: number; 261 | filename?: string; 262 | engine?: string; 263 | engine_version?: string; 264 | timeout?: number; 265 | 266 | [option: string]: any; 267 | } 268 | 269 | interface TaskCapture { 270 | operation: 'capture-website'; 271 | data: TaskCaptureData; 272 | } 273 | 274 | export interface TaskCaptureData { 275 | url: string; 276 | output_format: string; 277 | engine?: string; 278 | engine_version?: string; 279 | filename?: string; 280 | timeout?: number; 281 | pages?: string; 282 | zoom?: number; 283 | page_width?: number; 284 | page_height?: number; 285 | margin_top?: number; 286 | margin_bottom?: number; 287 | margin_left?: number; 288 | margin_right?: number; 289 | print_background?: boolean; 290 | display_header_footer?: boolean; 291 | header_template?: string; 292 | footer_template?: string; 293 | wait_until?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; 294 | wait_for_element?: string; 295 | wait_time?: number; 296 | headers?: { [header: string]: string }; 297 | } 298 | 299 | interface TaskThumbnail { 300 | operation: 'thumbnail'; 301 | data: TaskThumbnailData; 302 | } 303 | 304 | export interface TaskThumbnailData { 305 | input: string | string[]; 306 | input_format?: string; 307 | output_format: 'png' | 'webp' | 'jpg'; 308 | width?: number; 309 | height?: number; 310 | fit?: 'max' | 'crop' | 'scale'; 311 | count?: number; 312 | timestamp?: string; 313 | filename?: string; 314 | engine?: string; 315 | engine_version?: string; 316 | timeout?: number; 317 | 318 | [option: string]: any; 319 | } 320 | 321 | interface TaskMetadata { 322 | operation: 'metadata'; 323 | data: TaskMetadataData; 324 | } 325 | 326 | export interface TaskMetadataData { 327 | input: string | string[]; 328 | input_format?: string; 329 | engine?: string; 330 | engine_version?: string; 331 | timeout?: number; 332 | 333 | [option: string]: any; 334 | } 335 | 336 | interface TaskMetadataWrite { 337 | operation: 'metadata/write'; 338 | data: TaskMetadataWriteData; 339 | } 340 | 341 | export interface TaskMetadataWriteData { 342 | input: string | string[]; 343 | input_format?: string; 344 | engine?: string; 345 | engine_version?: string; 346 | metadata: Record; 347 | filename?: string; 348 | timeout?: number; 349 | 350 | [option: string]: any; 351 | } 352 | 353 | interface TaskMerge { 354 | operation: 'merge'; 355 | data: TaskMergeData; 356 | } 357 | 358 | export interface TaskMergeData { 359 | input: string | string[]; 360 | output_format: 'pdf'; 361 | engine?: string; 362 | engine_version?: string; 363 | filename?: string; 364 | timeout?: number; 365 | } 366 | 367 | interface TaskArchive { 368 | operation: 'archive'; 369 | data: TaskArchiveData; 370 | } 371 | 372 | export interface TaskArchiveData { 373 | input: string | string[]; 374 | output_format: string; 375 | engine?: string; 376 | engine_version?: string; 377 | filename?: string; 378 | timeout?: number; 379 | } 380 | 381 | interface TaskCommand { 382 | operation: 'command'; 383 | data: TaskCommandData; 384 | } 385 | 386 | interface TaskCommandBaseData { 387 | input: string | string[]; 388 | engine_version?: string; 389 | capture_output?: boolean; 390 | timeout?: number; 391 | arguments: string; 392 | } 393 | 394 | interface TaskCommandFfmpegData extends TaskCommandBaseData { 395 | engine: 'ffmpeg'; 396 | command: 'ffmpeg' | 'ffprobe'; 397 | } 398 | 399 | interface TaskCommandGraphicsmagickData extends TaskCommandBaseData { 400 | engine: 'graphicsmagick'; 401 | command: 'gm'; 402 | } 403 | 404 | interface TaskCommandImagemagickData extends TaskCommandBaseData { 405 | engine: 'imagemagick'; 406 | command: 'convert' | 'identify'; 407 | } 408 | 409 | export type TaskCommandData = 410 | | TaskCommandFfmpegData 411 | | TaskCommandGraphicsmagickData 412 | | TaskCommandImagemagickData; 413 | 414 | interface ExportUrl { 415 | operation: 'export/url'; 416 | data: ExportUrlData; 417 | } 418 | 419 | export interface ExportUrlData { 420 | input: string | string[]; 421 | inline?: boolean; 422 | archive_multiple_files?: boolean; 423 | } 424 | 425 | interface ExportS3 { 426 | operation: 'export/s3'; 427 | data: ExportS3Data; 428 | } 429 | 430 | export interface ExportS3Data { 431 | input: string | string[]; 432 | bucket: string; 433 | region: string; 434 | endpoint?: string; 435 | key?: string; 436 | key_prefix?: string; 437 | access_key_id: string; 438 | secret_access_key: string; 439 | session_token?: string; 440 | acl?: 441 | | 'private' 442 | | 'public-read' 443 | | 'public-read-write' 444 | | 'authenticated-read' 445 | | 'bucket-owner-read' 446 | | 'bucket-owner-full-control'; 447 | cache_control?: string; 448 | metadata?: Record; 449 | server_side_encryption?: string; 450 | tagging?: Record; 451 | } 452 | 453 | interface ExportAzureBlob { 454 | operation: 'export/azure/blob'; 455 | data: ExportAzureBlobData; 456 | } 457 | 458 | export interface ExportAzureBlobData { 459 | input: string | string[]; 460 | storage_account: string; 461 | storage_access_key?: string; 462 | sas_token?: string; 463 | container: string; 464 | blob?: string; 465 | blob_prefix?: string; 466 | } 467 | 468 | interface ExportGoogleCloudStorage { 469 | operation: 'export/google-cloud-storage'; 470 | data: ExportGoogleCloudStorageData; 471 | } 472 | 473 | export interface ExportGoogleCloudStorageData { 474 | input: string | string[]; 475 | project_id: string; 476 | bucket: string; 477 | client_email: string; 478 | private_key: string; 479 | file?: string; 480 | file_prefix?: string; 481 | } 482 | 483 | interface ExportOpenStack { 484 | operation: 'export/openstack'; 485 | data: ExportOpenStackData; 486 | } 487 | 488 | export interface ExportOpenStackData { 489 | input: string | string[]; 490 | auth_url: string; 491 | username: string; 492 | password: string; 493 | region: string; 494 | container: string; 495 | file?: string; 496 | file_prefix?: string; 497 | } 498 | 499 | interface ExportSFTP { 500 | operation: 'export/sftp'; 501 | data: ExportSFTPData; 502 | } 503 | 504 | export interface ExportSFTPData { 505 | input: string | string[]; 506 | host: string; 507 | port?: number; 508 | username: string; 509 | password?: string; 510 | private_key?: string; 511 | file?: string; 512 | path?: string; 513 | } 514 | 515 | export interface Task { 516 | id: string; 517 | job_id: string; 518 | operation: Operation['operation']; 519 | status: TaskStatus; 520 | message: string | null; 521 | code: string | null; 522 | credits: number | null; 523 | created_at: string; 524 | started_at: string | null; 525 | ended_at: string | null; 526 | depends_on_tasks: { [task: string]: string }; 527 | retry_of_task_id?: string | null; 528 | retries?: string[] | null; 529 | engine: string; 530 | engine_version: string; 531 | payload: any; 532 | result?: { files?: FileResult[]; [key: string]: any }; 533 | } 534 | 535 | export interface FileResult { 536 | dir?: string; 537 | filename: string; 538 | url?: string; 539 | size?: number; 540 | } 541 | 542 | export default class TasksResource { 543 | private readonly cloudConvert: CloudConvert; 544 | 545 | constructor(cloudConvert: CloudConvert) { 546 | this.cloudConvert = cloudConvert; 547 | } 548 | 549 | async get(id: string, query?: { include?: string }): Promise { 550 | return await this.cloudConvert.call('GET', `tasks/${id}`, query); 551 | } 552 | 553 | async wait(id: string): Promise { 554 | const baseURL = this.cloudConvert.useSandbox 555 | ? 'https://sync.api.sandbox.cloudconvert.com/v2/' 556 | : `https://${ 557 | this.cloudConvert.region ? this.cloudConvert.region + '.' : '' 558 | }sync.api.cloudconvert.com/v2/`; 559 | return await this.cloudConvert.callWithBase( 560 | baseURL, 561 | 'GET', 562 | `tasks/${id}` 563 | ); 564 | } 565 | 566 | async cancel(id: string): Promise { 567 | return await this.cloudConvert.call('POST', `tasks/${id}/cancel`); 568 | } 569 | 570 | async all(query?: { 571 | 'filter[job_id]'?: string; 572 | 'filter[status]'?: TaskStatus; 573 | 'filter[operation]'?: Operation['operation']; 574 | per_page?: number; 575 | page?: number; 576 | }): Promise { 577 | return await this.cloudConvert.call('GET', 'tasks', query); 578 | } 579 | 580 | async create( 581 | operation: O, 582 | data?: Extract['data'] 583 | ): Promise { 584 | return await this.cloudConvert.call('POST', operation, data); 585 | } 586 | 587 | async delete(id: string): Promise { 588 | await this.cloudConvert.call('DELETE', `tasks/${id}`); 589 | } 590 | 591 | async upload( 592 | task: Task | JobTask, 593 | stream: UploadFileSource, 594 | filename?: string, 595 | fileSize?: number 596 | ): Promise { 597 | if (task.operation !== 'import/upload') { 598 | throw new Error('The task operation is not import/upload'); 599 | } 600 | 601 | if (task.status !== 'waiting' || !task.result || !task.result.form) { 602 | throw new Error('The task is not ready for uploading'); 603 | } 604 | 605 | const uploadFile = new UploadFile(stream, filename, fileSize); 606 | for (const parameter in task.result.form.parameters) { 607 | uploadFile.add(parameter, task.result.form.parameters[parameter]); 608 | } 609 | 610 | return await this.cloudConvert.call( 611 | 'POST', 612 | task.result.form.url, 613 | uploadFile, 614 | { presigned: true, flat: true } 615 | ); 616 | } 617 | 618 | async subscribeEvent( 619 | id: string, 620 | event: TaskEvent, 621 | callback: (event: TaskEventData) => void 622 | ): Promise { 623 | this.cloudConvert.subscribe( 624 | `private-task.${id}`, 625 | `task.${event}`, 626 | callback 627 | ); 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /lib/UsersResource.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from './CloudConvert'; 2 | import { type JobEvent, type JobEventData } from './JobsResource'; 3 | import { type TaskEvent, type TaskEventData } from './TasksResource'; 4 | 5 | export interface User { 6 | id: string; 7 | username: string; 8 | email: string; 9 | credits: number; 10 | created_at: string; 11 | } 12 | 13 | export default class UsersResource { 14 | private readonly cloudConvert: CloudConvert; 15 | 16 | constructor(cloudConvert: CloudConvert) { 17 | this.cloudConvert = cloudConvert; 18 | } 19 | 20 | async me(): Promise { 21 | return await this.cloudConvert.call('GET', 'users/me'); 22 | } 23 | 24 | async subscribeJobEvent( 25 | id: string, 26 | event: JobEvent, 27 | callback: (event: JobEventData) => void 28 | ): Promise { 29 | this.cloudConvert.subscribe( 30 | `private-user.${id}.jobs`, 31 | `job.${event}`, 32 | callback 33 | ); 34 | } 35 | 36 | async subscribeTaskEvent( 37 | id: string, 38 | event: TaskEvent, 39 | callback: (event: TaskEventData) => void 40 | ): Promise { 41 | this.cloudConvert.subscribe( 42 | `private-user.${id}.tasks`, 43 | `task.${event}`, 44 | callback 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/WebhooksResource.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | 3 | export default class WebhooksResource { 4 | verify( 5 | payloadString: string, 6 | signature: string, 7 | signingSecret: string 8 | ): boolean { 9 | const hmac = crypto.createHmac('sha256', signingSecret); 10 | const signed = hmac 11 | .update(Buffer.from(payloadString, 'utf-8')) 12 | .digest('hex'); 13 | 14 | return signature === signed; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from './CloudConvert'; 2 | export = CloudConvert; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudconvert", 3 | "version": "3.0.0", 4 | "license": "MIT", 5 | "description": "Official Node.js SDK for the CloudConvert API", 6 | "homepage": "https://github.com/cloudconvert/cloudconvert-node", 7 | "author": "Josias Montag ", 8 | "main": "built/lib/index.js", 9 | "types": "built/lib/index.d.ts", 10 | "module": "built/lib/CloudConvert.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/cloudconvert/cloudconvert-node.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/cloudconvert/cloudconvert-node/issues" 17 | }, 18 | "engines": { 19 | "node": ">=20.11.19" 20 | }, 21 | "dependencies": { 22 | "socket.io-client": "^4.7.4" 23 | }, 24 | "devDependencies": { 25 | "@eslint/eslintrc": "^3.3.1", 26 | "@eslint/js": "^9.24.0", 27 | "@types/chai": "^5.2.1", 28 | "@types/mocha": "^10.0.10", 29 | "@types/node": "^20.11.19", 30 | "@types/socket.io-client": "^3.0.0", 31 | "@typescript-eslint/eslint-plugin": "^8.30.1", 32 | "@typescript-eslint/parser": "^8.30.1", 33 | "chai": "^5.0.3", 34 | "eslint": "^9.24.0", 35 | "eslint-config-prettier": "^10.1.2", 36 | "eslint-config-typescript": "^3.0.0", 37 | "eslint-plugin-prettier": "^5.1.3", 38 | "globals": "^16.0.0", 39 | "mocha": "^11.1.0", 40 | "nock": "^14.0.3", 41 | "prettier": "3.5.3", 42 | "tsx": "^4.19.3", 43 | "typescript": "^5.3.3" 44 | }, 45 | "scripts": { 46 | "prepare": "npm run build", 47 | "build": "tsc", 48 | "test": "mocha --require tsx tests/unit/*.ts", 49 | "test-integration": "mocha --require tsx tests/integration/*.ts", 50 | "fmt": "prettier . --write", 51 | "lint": "eslint --ext .ts --ext .js --ext .json ." 52 | } 53 | } -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | }, 5 | rules: { 6 | '@typescript-eslint/no-explicit-any': 0 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tests/integration/ApiKey.ts: -------------------------------------------------------------------------------- 1 | export default process.env.CLOUDCONVERT_API_KEY || 2 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjI4YmE3OGQyZjc1NWM5ZGE3Yjg1NDRhMWRkMjg2NWM4N2U0YzI5NWI0NzQ0Zjc4ZDNmMzA3OWM2NjU3ZjI0MjVhOTMyYjIxMjU5ZGU2NWQ4In0.eyJhdWQiOiIxIiwianRpIjoiMjhiYTc4ZDJmNzU1YzlkYTdiODU0NGExZGQyODY1Yzg3ZTRjMjk1YjQ3NDRmNzhkM2YzMDc5YzY2NTdmMjQyNWE5MzJiMjEyNTlkZTY1ZDgiLCJpYXQiOjE1NTkwNjc3NzcsIm5iZiI6MTU1OTA2Nzc3NywiZXhwIjo0NzE0NzQxMzc3LCJzdWIiOiIzNzExNjc4NCIsInNjb3BlcyI6WyJ1c2VyLnJlYWQiLCJ1c2VyLndyaXRlIiwidGFzay5yZWFkIiwidGFzay53cml0ZSIsIndlYmhvb2sucmVhZCIsIndlYmhvb2sud3JpdGUiXX0.IkmkfDVGwouCH-ICFAShQMHyFAHK3y90CSoissUVD8h5HFG4GqN5DEw0IFzlPr1auUKp3H1pAvPutdIQtrDMTmUUmGMUb2dRlCAuQdqxa81Q5KAmcKDgOg2YTWOWEGMy3jETTb7W6vyNGsT_3DFMapMdeOw1jdIUTMZqW3QbSCeGXj3PMRnhI7YynaDtmktjzO9IUDHbeT2HRzzMiep97KvVZNjYtZvgM-kbUjE6Mm68_kA8JMuQeor0Yg7896JPV0YM3-MnHf7elKgoCJbfBCDAbvSX_ZYsSI7IGoLLb0mgJVfFcH_HMYAHhJj5cUEJN2Iml-FkODqrRk72bVxyJs9j1GPQBl4ORXuU9yrjUgHrRaZ5YM__LwsUQB3AuB92oyQseCjULn1sWM1PzIXCcyVjKZSpn9LAAGNf9paCF-_G9ok9tZKccRouCiYl9v5XbmuxV8hXYp6fXZxyaAkj_JN2kErVSkxYzVyyZL1e220aFFnbch6nDvLFHgi-WeTQHFQDzuHsM8RKRixV8uD7pk3de4AEYg0EWqZHCr82qY7TGdSQvuAS0QIy3B89OwQW0ROW4k3Yw0XIKgKSYWyKnc7huc7yPQUIDDDAOa5OojXrVY5ZuL_hwQMIOmejcHTKFdAgzAaVnRkC8_FfVh4wHCPBaHjze9hRp5n4O1pnPFI'; 3 | -------------------------------------------------------------------------------- /tests/integration/JobsResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import { assert } from 'chai'; 3 | import * as fs from 'fs'; 4 | import { Readable } from 'node:stream'; 5 | import { type ReadableStream } from 'node:stream/web'; 6 | import * as os from 'os'; 7 | import apiKey from './ApiKey.js'; 8 | 9 | describe('JobsResource', () => { 10 | let cloudConvert: CloudConvert; 11 | let tmpPath: string; 12 | 13 | beforeEach(() => { 14 | cloudConvert = new CloudConvert(apiKey, true); 15 | }); 16 | 17 | describe('create()', () => { 18 | beforeEach(() => { 19 | tmpPath = os.tmpdir() + '/tmp.png'; 20 | }); 21 | 22 | it('test upload and download files', async () => { 23 | let job = await cloudConvert.jobs.create({ 24 | tag: 'integration-test-upload-download', 25 | tasks: { 26 | 'import-it': { operation: 'import/upload' }, 27 | 'export-it': { input: 'import-it', operation: 'export/url' } 28 | } 29 | }); 30 | 31 | const uploadTask = job.tasks.filter( 32 | task => task.name === 'import-it' 33 | )[0]; 34 | 35 | const stream = fs.createReadStream( 36 | __dirname + '/../integration/files/input.png' 37 | ); 38 | 39 | await cloudConvert.tasks.upload(uploadTask, stream); 40 | 41 | job = await cloudConvert.jobs.wait(job.id); 42 | 43 | assert.equal(job.status, 'finished'); 44 | 45 | // download export file 46 | const file = cloudConvert.jobs.getExportUrls(job)[0]; 47 | 48 | assert.equal(file.filename, 'input.png'); 49 | 50 | const writer = fs.createWriteStream(tmpPath); 51 | 52 | const response = (await fetch(file.url!)).body as ReadableStream; 53 | 54 | Readable.fromWeb(response).pipe(writer); 55 | 56 | await new Promise((resolve, reject) => { 57 | writer.on('finish', resolve); 58 | writer.on('error', reject); 59 | }); 60 | 61 | // check file size 62 | const stat = fs.statSync(tmpPath); 63 | 64 | assert.equal(stat.size, 46937); 65 | 66 | await cloudConvert.jobs.delete(job.id); 67 | }).timeout(30000); 68 | 69 | afterEach(() => { 70 | fs.unlinkSync(tmpPath); 71 | }); 72 | }); 73 | 74 | describe('subscribeEvent()', () => { 75 | it('test listening for finished event', async () => { 76 | const job = await cloudConvert.jobs.create({ 77 | tag: 'integration-test-socket', 78 | tasks: { 79 | 'import-it': { operation: 'import/upload' }, 80 | 'export-it': { input: 'import-it', operation: 'export/url' } 81 | } 82 | }); 83 | 84 | const uploadTask = job.tasks.filter( 85 | task => task.name === 'import-it' 86 | )[0]; 87 | 88 | const stream = fs.createReadStream( 89 | __dirname + '/../integration/files/input.png' 90 | ); 91 | 92 | setTimeout(() => { 93 | // for testing, we need to slow down the upload. otherwise we might miss the event because the job finishes too fast 94 | cloudConvert.tasks.upload(uploadTask, stream); 95 | }, 1000); 96 | 97 | const event = await new Promise(resolve => { 98 | cloudConvert.jobs.subscribeEvent(job.id, 'finished', resolve); 99 | }); 100 | 101 | assert.equal(event.job.status, 'finished'); 102 | 103 | await cloudConvert.jobs.delete(job.id); 104 | }).timeout(30000); 105 | 106 | afterEach(() => { 107 | cloudConvert.closeSocket(); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /tests/integration/TasksResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import { assert } from 'chai'; 3 | import * as fs from 'fs'; 4 | import apiKey from './ApiKey.js'; 5 | 6 | describe('TasksResource', () => { 7 | let cloudConvert: CloudConvert; 8 | 9 | beforeEach(() => { 10 | cloudConvert = new CloudConvert(apiKey, true); 11 | }); 12 | 13 | describe('upload()', () => { 14 | it('uploads input.png', async () => { 15 | let task = await cloudConvert.tasks.create('import/upload', { 16 | name: 'upload-test' 17 | }); 18 | 19 | const stream = fs.createReadStream( 20 | __dirname + '/../integration/files/input.png' 21 | ); 22 | 23 | await cloudConvert.tasks.upload(task, stream); 24 | 25 | task = await cloudConvert.tasks.wait(task.id); 26 | 27 | assert.equal(task.status, 'finished'); 28 | assert.equal(task.result.files[0].filename, 'input.png'); 29 | 30 | await cloudConvert.tasks.delete(task.id); 31 | }).timeout(30000); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/integration/UsersResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import apiKey from './ApiKey.js'; 3 | import { assert } from 'chai'; 4 | 5 | describe('UsersResource', () => { 6 | let cloudConvert: CloudConvert; 7 | 8 | beforeEach(() => { 9 | cloudConvert = new CloudConvert(apiKey, true); 10 | }); 11 | 12 | describe('me()', () => { 13 | it('should fetch the current user', async () => { 14 | const data = await cloudConvert.users.me(); 15 | 16 | console.log(data); 17 | 18 | assert.isObject(data); 19 | assert.containsAllKeys(data, [ 20 | 'id', 21 | 'username', 22 | 'email', 23 | 'credits' 24 | ]); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/integration/files/input.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudconvert/cloudconvert-node/54a173019ca09716699240686da3ee0f88bc4915/tests/integration/files/input.pdf -------------------------------------------------------------------------------- /tests/integration/files/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudconvert/cloudconvert-node/54a173019ca09716699240686da3ee0f88bc4915/tests/integration/files/input.png -------------------------------------------------------------------------------- /tests/unit/JobsResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import { assert } from 'chai'; 3 | import nock from 'nock'; 4 | 5 | describe('JobsResource', () => { 6 | let cloudConvert: CloudConvert; 7 | 8 | beforeEach(() => { 9 | cloudConvert = new CloudConvert('test'); 10 | }); 11 | 12 | describe('all()', () => { 13 | it('should fetch all jobs', async () => { 14 | nock('https://api.cloudconvert.com', { 15 | reqheaders: { Authorization: 'Bearer test' } 16 | }) 17 | .get('/v2/jobs') 18 | .replyWithFile(200, __dirname + '/responses/jobs.json', { 19 | 'Content-Type': 'application/json' 20 | }); 21 | 22 | const data = await cloudConvert.jobs.all(); 23 | 24 | assert.isArray(data); 25 | assert.equal(data[0].id, 'bd7d06b4-60fb-472b-b3a3-9034b273df07'); 26 | assert.isObject(data[0].links); 27 | }); 28 | }); 29 | 30 | describe('get()', () => { 31 | it('should fetch a job by id', async () => { 32 | nock('https://api.cloudconvert.com') 33 | .get('/v2/jobs/cd82535b-0614-4b23-bbba-b24ab0e892f7') 34 | .query(true) 35 | .replyWithFile(200, __dirname + '/responses/job.json', { 36 | 'Content-Type': 'application/json' 37 | }); 38 | 39 | const data = await cloudConvert.jobs.get( 40 | 'cd82535b-0614-4b23-bbba-b24ab0e892f7' 41 | ); 42 | 43 | assert.isObject(data); 44 | assert.equal(data.id, 'cd82535b-0614-4b23-bbba-b24ab0e892f7'); 45 | }); 46 | }); 47 | 48 | describe('create()', () => { 49 | it('should send the create request', async () => { 50 | nock('https://api.cloudconvert.com') 51 | .post('/v2/jobs', { tag: 'test', tasks: {} }) 52 | .replyWithFile(200, __dirname + '/responses/job_created.json', { 53 | 'Content-Type': 'application/json' 54 | }); 55 | 56 | const data = await cloudConvert.jobs.create({ 57 | tag: 'test', 58 | tasks: {} 59 | }); 60 | 61 | assert.isObject(data); 62 | assert.equal(data.id, 'c677ccf7-8876-4f48-bb96-0ab8e0d88cd7'); 63 | }); 64 | }); 65 | 66 | describe('delete()', () => { 67 | it('should send the delete request', async () => { 68 | nock('https://api.cloudconvert.com') 69 | .delete('/v2/jobs/2f901289-c9fe-4c89-9c4b-98be526bdfbf') 70 | .reply(204); 71 | 72 | await cloudConvert.jobs.delete( 73 | '2f901289-c9fe-4c89-9c4b-98be526bdfbf' 74 | ); 75 | }); 76 | }); 77 | 78 | describe('getExportUrls()', () => { 79 | it('should extract the export URLs', async () => { 80 | nock('https://api.cloudconvert.com') 81 | .get('/v2/jobs/b2e4eb2b-a744-4da2-97cd-776d393532a8') 82 | .query(true) 83 | .replyWithFile( 84 | 200, 85 | __dirname + '/responses/job_finished.json', 86 | { 'Content-Type': 'application/json' } 87 | ); 88 | 89 | const job = await cloudConvert.jobs.get( 90 | 'b2e4eb2b-a744-4da2-97cd-776d393532a8', 91 | { include: 'tasks' } 92 | ); 93 | 94 | const exportUrls = cloudConvert.jobs.getExportUrls(job); 95 | 96 | assert.isArray(exportUrls); 97 | assert.lengthOf(exportUrls, 1); 98 | 99 | assert.equal(exportUrls[0].filename, 'original.png'); 100 | assert.match( 101 | exportUrls[0].url, 102 | new RegExp('^https://storage.cloudconvert.com/') 103 | ); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /tests/unit/SignedUrlResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import { assert } from 'chai'; 3 | 4 | describe('SignedUrlResource', () => { 5 | let cloudConvert: CloudConvert; 6 | 7 | beforeEach(() => { 8 | cloudConvert = new CloudConvert('test'); 9 | }); 10 | 11 | describe('create()', () => { 12 | it('should create a signed URL', async () => { 13 | const base = 14 | 'https://s.cloudconvert.com/b3d85428-584e-4639-bc11-76b7dee9c109'; 15 | const signingSecret = 'NT8dpJkttEyfSk3qlRgUJtvTkx64vhyX'; 16 | 17 | const job = { 18 | tasks: { 19 | 'import-it': { 20 | operation: 'import/url', 21 | url: 'https://some.url', 22 | filename: 'logo.png' 23 | }, 24 | 'export-it': { 25 | operation: 'export/url', 26 | input: 'import-it', 27 | inline: true 28 | } 29 | } 30 | } as const; 31 | 32 | const url = cloudConvert.signedUrls.sign( 33 | base, 34 | signingSecret, 35 | job, 36 | 'mykey' 37 | ); 38 | 39 | assert.include(url, base); 40 | assert.include(url, '?job='); 41 | assert.include(url, '&cache_key=mykey'); 42 | assert.include( 43 | url, 44 | '&s=209d54e4454a407de71a07e6e500f45155fecf58a4e53d68329fbf358efcd823' 45 | ); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/TasksResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import { assert } from 'chai'; 3 | import * as fs from 'fs'; 4 | import nock from 'nock'; 5 | 6 | describe('TasksResource', () => { 7 | let cloudConvert: CloudConvert; 8 | 9 | beforeEach(() => { 10 | cloudConvert = new CloudConvert('test'); 11 | }); 12 | 13 | describe('all()', () => { 14 | it('should fetch all tasks', async () => { 15 | nock('https://api.cloudconvert.com', { 16 | reqheaders: { Authorization: 'Bearer test' } 17 | }) 18 | .get('/v2/tasks') 19 | .replyWithFile(200, __dirname + '/responses/tasks.json', { 20 | 'Content-Type': 'application/json' 21 | }); 22 | 23 | const data = await cloudConvert.tasks.all(); 24 | 25 | assert.isArray(data); 26 | assert.equal(data[0].id, '73df1e16-fd8b-47a1-a156-f197babde91a'); 27 | assert.isObject(data[0].links); 28 | }); 29 | }); 30 | 31 | describe('get()', () => { 32 | it('should fetch a task by id', async () => { 33 | nock('https://api.cloudconvert.com') 34 | .get('/v2/tasks/4c80f1ae-5b3a-43d5-bb58-1a5c4eb4e46b') 35 | .replyWithFile(200, __dirname + '/responses/task.json', { 36 | 'Content-Type': 'application/json' 37 | }); 38 | 39 | const data = await cloudConvert.tasks.get( 40 | '4c80f1ae-5b3a-43d5-bb58-1a5c4eb4e46b' 41 | ); 42 | 43 | assert.isObject(data); 44 | assert.equal(data.id, '4c80f1ae-5b3a-43d5-bb58-1a5c4eb4e46b'); 45 | }); 46 | }); 47 | 48 | describe('create()', () => { 49 | it('should send the create request', async () => { 50 | nock('https://api.cloudconvert.com') 51 | .post('/v2/convert', { 52 | name: 'test', 53 | url: 'http://invalid.url', 54 | filename: 'test.file' 55 | }) 56 | .replyWithFile( 57 | 200, 58 | __dirname + '/responses/task_created.json', 59 | { 'Content-Type': 'application/json' } 60 | ); 61 | 62 | const data = await cloudConvert.tasks.create('convert', { 63 | name: 'test', 64 | url: 'http://invalid.url', 65 | filename: 'test.file' 66 | }); 67 | 68 | assert.isObject(data); 69 | assert.equal(data.id, '2f901289-c9fe-4c89-9c4b-98be526bdfbf'); 70 | }); 71 | }); 72 | 73 | describe('delete()', () => { 74 | it('should send the delete request', async () => { 75 | nock('https://api.cloudconvert.com') 76 | .delete('/v2/tasks/2f901289-c9fe-4c89-9c4b-98be526bdfbf') 77 | .reply(204); 78 | 79 | await cloudConvert.tasks.delete( 80 | '2f901289-c9fe-4c89-9c4b-98be526bdfbf' 81 | ); 82 | }); 83 | }); 84 | 85 | describe('upload()', () => { 86 | it('should send the upload request', async () => { 87 | nock('https://api.cloudconvert.com') 88 | .post('/v2/import/upload') 89 | .replyWithFile( 90 | 200, 91 | __dirname + '/responses/upload_task_created.json', 92 | { 'Content-Type': 'application/json' } 93 | ); 94 | 95 | const task = await cloudConvert.tasks.create('import/upload'); 96 | 97 | nock('https://upload.sandbox.cloudconvert.com', { 98 | reqheaders: { 'Content-Type': /multipart\/form-data/i } 99 | }) 100 | .post( 101 | '/storage.de1.cloud.ovh.net/v1/AUTH_b2cffe8f45324c2bba39e8db1aedb58f/cloudconvert-files-sandbox/8aefdb39-34c8-4c7a-9f2e-1751686d615e/?s=jNf7hn3zox1iZfZY6NirNA&e=1559588529' 102 | ) 103 | .reply(201); 104 | 105 | const blob = await fs.openAsBlob( 106 | __dirname + '/../integration/files/input.png' 107 | ); 108 | 109 | await cloudConvert.tasks.upload(task, blob); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/unit/UsersResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import { assert } from 'chai'; 3 | import nock from 'nock'; 4 | 5 | describe('UsersResource', () => { 6 | let cloudConvert: CloudConvert; 7 | 8 | beforeEach(() => { 9 | cloudConvert = new CloudConvert('test'); 10 | }); 11 | 12 | describe('me()', () => { 13 | it('should fetch the current user', async () => { 14 | nock('https://api.cloudconvert.com') 15 | .get('/v2/users/me') 16 | .replyWithFile(200, __dirname + '/responses/user.json', { 17 | 'Content-Type': 'application/json' 18 | }); 19 | 20 | const data = await cloudConvert.users.me(); 21 | 22 | assert.isObject(data); 23 | assert.equal(data.id, 1); 24 | assert.equal(data.credits, 4434); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/unit/WebhooksResourceTest.ts: -------------------------------------------------------------------------------- 1 | import CloudConvert from '../../built/lib/CloudConvert.js'; 2 | import { assert } from 'chai'; 3 | import * as fs from 'fs'; 4 | 5 | describe('WebhooksResource', () => { 6 | let cloudConvert: CloudConvert; 7 | 8 | beforeEach(() => { 9 | cloudConvert = new CloudConvert('test'); 10 | }); 11 | 12 | describe('verify()', () => { 13 | it('should verify the payload', async () => { 14 | const secret = 'secret'; 15 | const signature = 16 | '576b653f726c85265a389532988f483b5c7d7d5f40cede5f5ddf9c3f02934f35'; 17 | const payloadString = fs.readFileSync( 18 | __dirname + '/requests/webhook_job_finished_payload.json', 19 | 'utf-8' 20 | ); 21 | 22 | assert.isFalse( 23 | cloudConvert.webhooks.verify(payloadString, 'invalid', secret) 24 | ); 25 | assert.isTrue( 26 | cloudConvert.webhooks.verify(payloadString, signature, secret) 27 | ); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/unit/requests/webhook_job_finished_payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "job.finished", 3 | "job": { 4 | "id": "c677ccf7-8876-4f48-bb96-0ab8e0d88cd7", 5 | "status": "finished", 6 | "created_at": "2019-06-01T00:35:33+00:00", 7 | "started_at": "2019-06-01T00:35:33+00:00", 8 | "ended_at": "2019-06-01T00:35:33+00:00", 9 | "tasks": [ 10 | { 11 | "id": "22b3d686-126b-4fe2-8238-a9781cd023d9", 12 | "name": "import-it", 13 | "job_id": "c677ccf7-8876-4f48-bb96-0ab8e0d88cd7", 14 | "status": "finished", 15 | "code": null, 16 | "message": null, 17 | "percent": 100, 18 | "operation": "import\/url", 19 | "payload": { 20 | "operation": "import\/url", 21 | "url": "http:\/\/invalid.url", 22 | "filename": "test.file" 23 | }, 24 | "result": null, 25 | "created_at": "2019-06-01T00:35:33+00:00", 26 | "started_at": "2019-06-01T00:35:33+00:00", 27 | "ended_at": "2019-06-01T00:35:33+00:00", 28 | "retry_of_task_id": null, 29 | "copy_of_task_id": null, 30 | "host_name": "jena", 31 | "storage": null, 32 | "depends_on_task_ids": [], 33 | "links": { 34 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/22b3d686-126b-4fe2-8238-a9781cd023d9" 35 | } 36 | }, 37 | { 38 | "id": "15281118-acc3-441f-970d-39a49457d9e5", 39 | "name": "convert-it", 40 | "job_id": "c677ccf7-8876-4f48-bb96-0ab8e0d88cd7", 41 | "status": "finished", 42 | "code": null, 43 | "message": null, 44 | "percent": 100, 45 | "operation": "convert", 46 | "engine": null, 47 | "engine_version": null, 48 | "payload": { 49 | "operation": "convert", 50 | "input": [ 51 | "import-it" 52 | ], 53 | "output_format": "pdf" 54 | }, 55 | "result": null, 56 | "created_at": "2019-06-01T00:35:33+00:00", 57 | "started_at": "2019-06-01T00:35:33+00:00", 58 | "ended_at": "2019-06-01T00:35:33+00:00", 59 | "retry_of_task_id": null, 60 | "copy_of_task_id": null, 61 | "host_name": null, 62 | "storage": null, 63 | "depends_on_task_ids": [ 64 | "22b3d686-126b-4fe2-8238-a9781cd023d9" 65 | ], 66 | "links": { 67 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/15281118-acc3-441f-970d-39a49457d9e5" 68 | } 69 | }, 70 | { 71 | "id": "72dc4f5f-7ef0-4f46-9811-29589a541400", 72 | "name": "export-it", 73 | "job_id": "c677ccf7-8876-4f48-bb96-0ab8e0d88cd7", 74 | "status": "finished", 75 | "code": null, 76 | "message": null, 77 | "percent": 100, 78 | "operation": "export/url", 79 | "engine": null, 80 | "engine_version": null, 81 | "payload": { 82 | "operation": "export/url", 83 | "input": [ 84 | "convert-it" 85 | ] 86 | }, 87 | "result": { 88 | "files": [ 89 | { 90 | "filename": "file.pdf", 91 | "url": "https://storage.cloudconvert.com/eed87242-577e-4e3e-8178-9edbe51975dd/file.pdf?temp_url_sig=79c2db4d884926bbcc5476d01b4922a19137aee9&temp_url_expires=1545962104" 92 | } 93 | ] 94 | }, 95 | "created_at": "2019-06-01T00:35:33+00:00", 96 | "started_at": "2019-06-01T00:35:33+00:00", 97 | "ended_at": "2019-06-01T00:35:33+00:00", 98 | "retry_of_task_id": null, 99 | "copy_of_task_id": null, 100 | "host_name": null, 101 | "storage": null, 102 | "depends_on_task_ids": [ 103 | "15281118-acc3-441f-970d-39a49457d9e5" 104 | ], 105 | "links": { 106 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/72dc4f5f-7ef0-4f46-9811-29589a541400" 107 | } 108 | } 109 | ], 110 | "links": { 111 | "self": "https:\/\/api.cloudconvert.com\/v2\/jobs\/c677ccf7-8876-4f48-bb96-0ab8e0d88cd7" 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/unit/responses/job.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "cd82535b-0614-4b23-bbba-b24ab0e892f7", 4 | "tag": "test-1234", 5 | "status": "error", 6 | "created_at": "2019-05-30T10:53:01+00:00", 7 | "started_at": "2019-05-30T10:53:05+00:00", 8 | "ended_at": "2019-05-30T10:53:23+00:00", 9 | "tasks": [ 10 | { 11 | "id": "4c80f1ae-5b3a-43d5-bb58-1a5c4eb4e46b", 12 | "name": "export-1", 13 | "job_id": "cd82535b-0614-4b23-bbba-b24ab0e892f7", 14 | "status": "error", 15 | "code": "INPUT_TASK_FAILED", 16 | "message": "Input task has failed", 17 | "percent": 100, 18 | "operation": "export\/url", 19 | "payload": { 20 | "operation": "export\/url", 21 | "input": [ 22 | "task-1" 23 | ] 24 | }, 25 | "result": null, 26 | "created_at": "2019-05-30T10:53:01+00:00", 27 | "started_at": null, 28 | "ended_at": "2019-05-30T10:53:23+00:00", 29 | "retry_of_task_id": null, 30 | "copy_of_task_id": null, 31 | "host_name": null, 32 | "storage": null, 33 | "depends_on_task_ids": [ 34 | "6df0920a-7042-4e87-be52-f38a0a29a67e" 35 | ], 36 | "links": { 37 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/4c80f1ae-5b3a-43d5-bb58-1a5c4eb4e46b" 38 | } 39 | }, 40 | { 41 | "id": "6df0920a-7042-4e87-be52-f38a0a29a67e", 42 | "name": "task-1", 43 | "job_id": "cd82535b-0614-4b23-bbba-b24ab0e892f7", 44 | "status": "error", 45 | "code": "INPUT_TASK_FAILED", 46 | "message": "Input task has failed", 47 | "percent": 100, 48 | "operation": "convert", 49 | "engine": null, 50 | "engine_version": null, 51 | "payload": { 52 | "operation": "convert", 53 | "input_format": "mp4", 54 | "output_format": "mp4", 55 | "engine": "ffmpeg", 56 | "input": [ 57 | "import-1" 58 | ], 59 | "video_codec": "x264", 60 | "crf": 0, 61 | "preset": "veryslow", 62 | "profile": "baseline", 63 | "width": 1920, 64 | "height": 1080, 65 | "audio_codec": "copy", 66 | "audio_bitrate": 320, 67 | "engine_version": "4.1.1" 68 | }, 69 | "result": null, 70 | "created_at": "2019-05-30T10:53:01+00:00", 71 | "started_at": null, 72 | "ended_at": "2019-05-30T10:53:23+00:00", 73 | "retry_of_task_id": null, 74 | "copy_of_task_id": null, 75 | "host_name": null, 76 | "storage": null, 77 | "depends_on_task_ids": [ 78 | "22be63c2-0e3f-4909-9c2a-2261dc540aba" 79 | ], 80 | "links": { 81 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/6df0920a-7042-4e87-be52-f38a0a29a67e" 82 | } 83 | }, 84 | { 85 | "id": "22be63c2-0e3f-4909-9c2a-2261dc540aba", 86 | "name": "import-1", 87 | "job_id": "cd82535b-0614-4b23-bbba-b24ab0e892f7", 88 | "status": "error", 89 | "code": "SANDBOX_FILE_NOT_ALLOWED", 90 | "message": "The file file.mp4 is not whitelisted for sandbox use", 91 | "percent": 100, 92 | "operation": "import\/url", 93 | "payload": { 94 | "operation": "import\/url", 95 | "url": "https:\/\/some.url\/file.mp4", 96 | "filename": "file.mp4" 97 | }, 98 | "result": { 99 | "files": [ 100 | { 101 | "filename": "file.mp4", 102 | "md5": "c03538f8edd84537190c264109fa2284" 103 | } 104 | ] 105 | }, 106 | "created_at": "2019-05-30T10:53:01+00:00", 107 | "started_at": "2019-05-30T10:53:05+00:00", 108 | "ended_at": "2019-05-30T10:53:23+00:00", 109 | "retry_of_task_id": null, 110 | "copy_of_task_id": null, 111 | "host_name": "leta", 112 | "storage": null, 113 | "depends_on_task_ids": [], 114 | "links": { 115 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/22be63c2-0e3f-4909-9c2a-2261dc540aba" 116 | } 117 | } 118 | ], 119 | "links": { 120 | "self": "https:\/\/api.cloudconvert.com\/v2\/jobs\/cd82535b-0614-4b23-bbba-b24ab0e892f7" 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/unit/responses/job_created.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "c677ccf7-8876-4f48-bb96-0ab8e0d88cd7", 4 | "status": "waiting", 5 | "created_at": "2019-06-01T00:35:33+00:00", 6 | "started_at": "2019-06-01T00:35:33+00:00", 7 | "ended_at": null, 8 | "tasks": [ 9 | { 10 | "id": "22b3d686-126b-4fe2-8238-a9781cd023d9", 11 | "name": "import-it", 12 | "job_id": "c677ccf7-8876-4f48-bb96-0ab8e0d88cd7", 13 | "status": "waiting", 14 | "code": null, 15 | "message": null, 16 | "percent": 100, 17 | "operation": "import\/url", 18 | "payload": { 19 | "operation": "import\/url", 20 | "url": "http:\/\/invalid.url", 21 | "filename": "test.file" 22 | }, 23 | "result": null, 24 | "created_at": "2019-06-01T00:35:33+00:00", 25 | "started_at": "2019-06-01T00:35:33+00:00", 26 | "ended_at": null, 27 | "retry_of_task_id": null, 28 | "copy_of_task_id": null, 29 | "host_name": "jena", 30 | "storage": null, 31 | "depends_on_task_ids": [], 32 | "links": { 33 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/22b3d686-126b-4fe2-8238-a9781cd023d9" 34 | } 35 | }, 36 | { 37 | "id": "15281118-acc3-441f-970d-39a49457d9e5", 38 | "name": "convert-it", 39 | "job_id": "c677ccf7-8876-4f48-bb96-0ab8e0d88cd7", 40 | "status": "waiting", 41 | "code": null, 42 | "message": null, 43 | "percent": 100, 44 | "operation": "convert", 45 | "engine": null, 46 | "engine_version": null, 47 | "payload": { 48 | "operation": "convert", 49 | "input": [ 50 | "import-it" 51 | ], 52 | "output_format": "pdf" 53 | }, 54 | "result": null, 55 | "created_at": "2019-06-01T00:35:33+00:00", 56 | "started_at": null, 57 | "ended_at": null, 58 | "retry_of_task_id": null, 59 | "copy_of_task_id": null, 60 | "host_name": null, 61 | "storage": null, 62 | "depends_on_task_ids": [ 63 | "22b3d686-126b-4fe2-8238-a9781cd023d9" 64 | ], 65 | "links": { 66 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/15281118-acc3-441f-970d-39a49457d9e5" 67 | } 68 | } 69 | ], 70 | "links": { 71 | "self": "https:\/\/api.cloudconvert.com\/v2\/jobs\/c677ccf7-8876-4f48-bb96-0ab8e0d88cd7" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/unit/responses/job_finished.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "b2e4eb2b-a744-4da2-97cd-776d393532a8", 4 | "tag": "jobbuilder", 5 | "status": "finished", 6 | "created_at": "2022-09-26T08:14:11+00:00", 7 | "started_at": "2022-09-26T08:14:11+00:00", 8 | "ended_at": "2022-09-26T08:14:12+00:00", 9 | "tasks": [ 10 | { 11 | "id": "1b7ccc22-e26c-43dc-85e7-c1ff8fbfd9b4", 12 | "name": "export-1", 13 | "job_id": "b2e4eb2b-a744-4da2-97cd-776d393532a8", 14 | "status": "finished", 15 | "credits": 0, 16 | "code": null, 17 | "message": null, 18 | "percent": 100, 19 | "operation": "export\/url", 20 | "result": { 21 | "files": [ 22 | { 23 | "filename": "original.png", 24 | "size": 1040507, 25 | "url": "https:\/\/storage.cloudconvert.com\/tasks\/1b7ccc22-e26c-43dc-85e7-c1ff8fbfd9b4\/original.png?AWSAccessKeyId=cloudconvert-production&Expires=1664266452&Signature=TDScvmTUzCv4nk5EWkz2Aakxn%2Bs%3D&response-content-disposition=attachment%3B%20filename%3D%22original.png%22&response-content-type=image%2Fpng" 26 | } 27 | ] 28 | }, 29 | "created_at": "2022-09-26T08:14:11+00:00", 30 | "started_at": "2022-09-26T08:14:12+00:00", 31 | "ended_at": "2022-09-26T08:14:12+00:00", 32 | "retry_of_task_id": null, 33 | "copy_of_task_id": null, 34 | "user_id": 1, 35 | "priority": 50, 36 | "host_name": "dulce", 37 | "storage": "ceph-fra", 38 | "depends_on_task_ids": [ 39 | "69534510-3eef-46c2-b7a6-71b3f1aed916" 40 | ], 41 | "links": { 42 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/1b7ccc22-e26c-43dc-85e7-c1ff8fbfd9b4" 43 | } 44 | }, 45 | { 46 | "id": "0a6fd2af-d795-4988-a104-e7c890608bc9", 47 | "name": "import-1", 48 | "job_id": "b2e4eb2b-a744-4da2-97cd-776d393532a8", 49 | "status": "finished", 50 | "credits": 0, 51 | "code": null, 52 | "message": null, 53 | "percent": 100, 54 | "operation": "import\/url", 55 | "result": { 56 | "files": [ 57 | { 58 | "filename": "original.jpg", 59 | "size": 213864 60 | } 61 | ] 62 | }, 63 | "created_at": "2022-09-26T08:14:11+00:00", 64 | "started_at": "2022-09-26T08:14:11+00:00", 65 | "ended_at": "2022-09-26T08:14:11+00:00", 66 | "retry_of_task_id": null, 67 | "copy_of_task_id": null, 68 | "user_id": 1, 69 | "priority": 50, 70 | "host_name": "dulce", 71 | "storage": null, 72 | "depends_on_task_ids": [], 73 | "links": { 74 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/0a6fd2af-d795-4988-a104-e7c890608bc9" 75 | } 76 | }, 77 | { 78 | "id": "69534510-3eef-46c2-b7a6-71b3f1aed916", 79 | "name": "task-1", 80 | "job_id": "b2e4eb2b-a744-4da2-97cd-776d393532a8", 81 | "status": "finished", 82 | "credits": 1, 83 | "code": null, 84 | "message": null, 85 | "percent": 100, 86 | "operation": "convert", 87 | "engine": "imagemagick", 88 | "engine_version": "7.1.0", 89 | "result": { 90 | "files": [ 91 | { 92 | "filename": "original.png", 93 | "size": 1040507 94 | } 95 | ] 96 | }, 97 | "created_at": "2022-09-26T08:14:11+00:00", 98 | "started_at": "2022-09-26T08:14:11+00:00", 99 | "ended_at": "2022-09-26T08:14:11+00:00", 100 | "retry_of_task_id": null, 101 | "copy_of_task_id": null, 102 | "user_id": 1, 103 | "priority": 50, 104 | "host_name": "dulce", 105 | "storage": null, 106 | "depends_on_task_ids": [ 107 | "0a6fd2af-d795-4988-a104-e7c890608bc9" 108 | ], 109 | "links": { 110 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/69534510-3eef-46c2-b7a6-71b3f1aed916" 111 | } 112 | } 113 | ], 114 | "links": { 115 | "self": "https:\/\/api.cloudconvert.com\/v2\/jobs\/b2e4eb2b-a744-4da2-97cd-776d393532a8" 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /tests/unit/responses/jobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": "bd7d06b4-60fb-472b-b3a3-9034b273df07", 5 | "status": "waiting", 6 | "created_at": "2019-05-13T19:52:21+00:00", 7 | "started_at": null, 8 | "ended_at": null, 9 | "links": { 10 | "self": "https:\/\/api.cloudconvert.com\/v2\/jobs\/bd7d06b4-60fb-472b-b3a3-9034b273df07" 11 | } 12 | } 13 | ], 14 | "links": { 15 | "first": "https:\/\/api.cloudconvert.com\/v2\/jobs?page=1", 16 | "last": null, 17 | "prev": null, 18 | "next": null 19 | }, 20 | "meta": { 21 | "current_page": 1, 22 | "from": 1, 23 | "path": "https:\/\/api.cloudconvert.com\/v2\/jobs", 24 | "per_page": 100, 25 | "to": 1 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/responses/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "4c80f1ae-5b3a-43d5-bb58-1a5c4eb4e46b", 4 | "name": "export-1", 5 | "job_id": "cd82535b-0614-4b23-bbba-b24ab0e892f7", 6 | "status": "error", 7 | "code": "INPUT_TASK_FAILED", 8 | "message": "Input task has failed", 9 | "percent": 100, 10 | "operation": "export\/url", 11 | "payload": { 12 | "operation": "export\/url", 13 | "input": [ 14 | "task-1" 15 | ] 16 | }, 17 | "result": null, 18 | "created_at": "2019-05-30T10:53:01+00:00", 19 | "started_at": null, 20 | "ended_at": "2019-05-30T10:53:23+00:00", 21 | "retry_of_task_id": null, 22 | "copy_of_task_id": null, 23 | "retries": [], 24 | "host_name": null, 25 | "storage": null, 26 | "depends_on_tasks": [ 27 | { 28 | "id": "6df0920a-7042-4e87-be52-f38a0a29a67e", 29 | "name": "task-1", 30 | "job_id": "cd82535b-0614-4b23-bbba-b24ab0e892f7", 31 | "status": "error", 32 | "code": "INPUT_TASK_FAILED", 33 | "message": "Input task has failed", 34 | "percent": 100, 35 | "operation": "convert", 36 | "engine": null, 37 | "engine_version": null, 38 | "payload": { 39 | "operation": "convert", 40 | "input_format": "mp4", 41 | "output_format": "mp4", 42 | "engine": "ffmpeg", 43 | "input": [ 44 | "import-1" 45 | ], 46 | "video_codec": "x264", 47 | "crf": 0, 48 | "preset": "veryslow", 49 | "profile": "baseline", 50 | "width": 1920, 51 | "height": 1080, 52 | "audio_codec": "copy", 53 | "audio_bitrate": 320, 54 | "engine_version": "4.1.1" 55 | }, 56 | "result": null, 57 | "created_at": "2019-05-30T10:53:01+00:00", 58 | "started_at": null, 59 | "ended_at": "2019-05-30T10:53:23+00:00", 60 | "retry_of_task_id": null, 61 | "copy_of_task_id": null, 62 | "host_name": null, 63 | "storage": null, 64 | "depends_on_task_ids": [ 65 | "22be63c2-0e3f-4909-9c2a-2261dc540aba" 66 | ], 67 | "links": { 68 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/6df0920a-7042-4e87-be52-f38a0a29a67e" 69 | } 70 | } 71 | ], 72 | "links": { 73 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/4c80f1ae-5b3a-43d5-bb58-1a5c4eb4e46b" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/unit/responses/task_created.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "2f901289-c9fe-4c89-9c4b-98be526bdfbf", 4 | "job_id": null, 5 | "status": "waiting", 6 | "code": null, 7 | "message": null, 8 | "percent": 100, 9 | "operation": "import\/url", 10 | "payload": { 11 | "name": "test", 12 | "url": "http:\/\/invalid.url", 13 | "filename": "test.file" 14 | }, 15 | "result": null, 16 | "created_at": "2019-05-31T23:52:39+00:00", 17 | "started_at": "2019-05-31T23:52:39+00:00", 18 | "ended_at": "2019-05-31T23:53:26+00:00", 19 | "retry_of_task_id": null, 20 | "copy_of_task_id": null, 21 | "retries": [], 22 | "depends_on_tasks": [], 23 | "links": { 24 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/2f901289-c9fe-4c89-9c4b-98be526bdfbf" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/responses/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": "73df1e16-fd8b-47a1-a156-f197babde91a", 5 | "operation": "convert", 6 | "status": "processing", 7 | "message": null, 8 | "code": null, 9 | "created_at": "2018-09-19T14:42:58+00:00", 10 | "started_at": "2018-09-19T14:42:58+00:00", 11 | "ended_at": null, 12 | "payload": { 13 | }, 14 | "result": null, 15 | "links": { 16 | "self": "https://api.cloudconvert.com/v2/tasks/h451E6HMhG" 17 | } 18 | }, 19 | { 20 | "id": "4d610226-5347-4522-b08a-d165b1dde6a0", 21 | "operation": "export/s3", 22 | "status": "waiting", 23 | "message": null, 24 | "code": null, 25 | "created_at": "2018-09-19T14:42:58+00:00", 26 | "started_at": null, 27 | "ended_at": null, 28 | "payload": { 29 | }, 30 | "result": null, 31 | "links": { 32 | "self": "https://api.cloudconvert.com/v2/tasks/Xhrek8bGGq" 33 | } 34 | } 35 | ], 36 | "links": { 37 | "first": "https://api.cloudconvert.com/v2/tasks?page=1", 38 | "last": null, 39 | "prev": null, 40 | "next": null 41 | }, 42 | "meta": { 43 | "current_page": 1, 44 | "from": 1, 45 | "path": "https://api.cloudconvert.com/v2/tasks", 46 | "per_page": 100, 47 | "to": 2 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/unit/responses/upload_task_created.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "2f901289-c9fe-4c89-9c4b-98be526bdfbf", 4 | "job_id": null, 5 | "status": "waiting", 6 | "code": null, 7 | "message": null, 8 | "percent": 100, 9 | "operation": "import\/upload", 10 | "payload": null, 11 | "result": { 12 | "form": { 13 | "url": "https://upload.sandbox.cloudconvert.com/storage.de1.cloud.ovh.net/v1/AUTH_b2cffe8f45324c2bba39e8db1aedb58f/cloudconvert-files-sandbox/8aefdb39-34c8-4c7a-9f2e-1751686d615e/?s=jNf7hn3zox1iZfZY6NirNA&e=1559588529", 14 | "parameters": { 15 | "expires": 1559588529, 16 | "max_file_count": 1, 17 | "max_file_size": 10000000000, 18 | "signature": "79fda6c5ffbfaa857ae9a1430641cc68c5a72297" 19 | } 20 | } 21 | }, 22 | "created_at": "2019-05-31T23:52:39+00:00", 23 | "started_at": "2019-05-31T23:52:39+00:00", 24 | "ended_at": "2019-05-31T23:53:26+00:00", 25 | "retry_of_task_id": null, 26 | "copy_of_task_id": null, 27 | "retries": [], 28 | "depends_on_tasks": [], 29 | "links": { 30 | "self": "https:\/\/api.cloudconvert.com\/v2\/tasks\/2f901289-c9fe-4c89-9c4b-98be526bdfbf" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/responses/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 1, 4 | "username": "Username", 5 | "email": "me@example.com", 6 | "created_at": "2018-12-01T22:26:29+00:00", 7 | "credits": 4434, 8 | "links": { 9 | "self": "https://api.cloudconvert.com/v2/users/1" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "noUncheckedIndexedAccess": true, 7 | "module": "commonjs", 8 | "newLine": "lf", 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": true, 11 | "noUnusedParameters": true, 12 | "outDir": "./built/", 13 | "removeComments": true, 14 | "sourceMap": true, 15 | "incremental": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "resolveJsonModule": true, 19 | "target": "es6", 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "include": ["./lib/*"] 23 | } 24 | --------------------------------------------------------------------------------