├── test ├── e2e │ ├── fixtures │ │ └── zerobytes.jpg │ └── cli │ │ ├── cli.test.ts │ │ ├── assemblies-list.test.ts │ │ ├── bills.test.ts │ │ ├── OutputCtl.ts │ │ └── test-utils.ts ├── generate-coverage-badge.ts ├── unit │ └── test-pagination-stream.test.ts ├── util.ts ├── testserver.ts └── tunnel.ts ├── .yarnrc.yml ├── examples ├── fixtures │ ├── berkley.jpg │ └── circle.svg ├── rasterize_svg_to_png.ts ├── resize_an_image.ts ├── convert_to_webp.ts ├── retry.ts ├── fetch_costs_of_all_assemblies_in_timeframe.ts ├── face_detect_download.ts ├── template_api.ts └── credentials.ts ├── .cursor ├── mcp.json └── rules │ ├── pr-comments.mdc │ ├── general.mdc │ ├── typescript.mdc │ └── coding-style.mdc ├── src ├── InconsistentResponseError.ts ├── PollingTimeoutError.ts ├── alphalib │ ├── types │ │ ├── bill.ts │ │ ├── stackVersions.ts │ │ ├── assemblyReplayNotification.ts │ │ ├── assemblyReplay.ts │ │ ├── robots │ │ │ ├── assembly-savejson.ts │ │ │ ├── progress-simulate.ts │ │ │ ├── meta-read.ts │ │ │ ├── file-watermark.ts │ │ │ ├── file-read.ts │ │ │ ├── document-autorotate.ts │ │ │ ├── tlcdn-deliver.ts │ │ │ ├── edgly-deliver.ts │ │ │ ├── document-split.ts │ │ │ ├── file-hash.ts │ │ │ ├── sftp-import.ts │ │ │ ├── ftp-import.ts │ │ │ ├── meta-write.ts │ │ │ ├── image-bgremove.ts │ │ │ ├── upload-handle.ts │ │ │ ├── image-generate.ts │ │ │ ├── dropbox-store.ts │ │ │ ├── audio-encode.ts │ │ │ ├── dropbox-import.ts │ │ │ ├── cloudfiles-store.ts │ │ │ ├── audio-loop.ts │ │ │ ├── backblaze-store.ts │ │ │ ├── audio-artwork.ts │ │ │ ├── document-merge.ts │ │ │ ├── azure-import.ts │ │ │ ├── supabase-store.ts │ │ │ ├── file-verify.ts │ │ │ ├── sftp-store.ts │ │ │ ├── minio-store.ts │ │ │ ├── wasabi-store.ts │ │ │ ├── ftp-store.ts │ │ │ ├── vimeo-import.ts │ │ │ ├── swift-store.ts │ │ │ └── tigris-store.ts │ │ ├── assembliesGet.ts │ │ └── templateCredential.ts │ └── tryCatch.ts ├── cli.ts ├── PaginationStream.ts ├── ApiError.ts ├── cli │ ├── helpers.ts │ ├── commands │ │ ├── index.ts │ │ ├── notifications.ts │ │ ├── BaseCommand.ts │ │ └── bills.ts │ ├── OutputCtl.ts │ └── template-last-modified.ts └── apiTypes.ts ├── .gemini └── settings.json ├── .gitignore ├── .vscode ├── node-sdk.code-workspace └── settings.json ├── CLAUDE.md ├── tsconfig.json ├── tsconfig.build.json ├── ROADMAP.md ├── vitest.config.ts ├── LICENSE ├── package.json ├── .github └── workflows │ └── claude.yml ├── CONTRIBUTING.md └── biome.json /test/e2e/fixtures/zerobytes.jpg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /examples/fixtures/berkley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transloadit/node-sdk/HEAD/examples/fixtures/berkley.jpg -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "transloadit-internal-sse": { 4 | "url": "http://localhost:5555/mcp" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/InconsistentResponseError.ts: -------------------------------------------------------------------------------- 1 | export default class InconsistentResponseError extends Error { 2 | override name = 'InconsistentResponseError' 3 | } 4 | -------------------------------------------------------------------------------- /.gemini/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "playwright": { 4 | "command": "npx", 5 | "args": ["-y", "@playwright/mcp@latest"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/PollingTimeoutError.ts: -------------------------------------------------------------------------------- 1 | export default class PollingTimeoutError extends Error { 2 | override name = 'PollingTimeoutError' 3 | 4 | code = 'POLLING_TIMED_OUT' 5 | } 6 | -------------------------------------------------------------------------------- /examples/fixtures/circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/alphalib/types/bill.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { assemblyAuthInstructionsSchema } from './template.ts' 4 | 5 | export const billSchema = z 6 | .object({ 7 | auth: assemblyAuthInstructionsSchema, 8 | }) 9 | .strict() 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules 3 | cloudflared* 4 | credentials.js 5 | sample.js 6 | 7 | *.tsbuildinfo 8 | npm-debug.log 9 | env.sh 10 | /coverage 11 | 12 | .pnp.* 13 | .yarn/* 14 | !.yarn/patches 15 | !.yarn/plugins 16 | !.yarn/releases 17 | !.yarn/sdks 18 | !.yarn/versions 19 | .aider* 20 | .DS_Store 21 | .env 22 | -------------------------------------------------------------------------------- /.vscode/node-sdk.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "..", 5 | }, 6 | ], 7 | "settings": { 8 | "workbench.colorCustomizations": { 9 | "titleBar.activeForeground": "#3e3e3e", 10 | "titleBar.activeBackground": "#ffd100", 11 | }, 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e/cli/cli.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { runCli } from './test-utils.ts' 3 | 4 | describe('CLI', () => { 5 | it('should list templates via CLI', async () => { 6 | const { stdout, stderr } = await runCli('templates list') 7 | expect(stderr).to.be.empty 8 | expect(stdout).to.match(/[a-f0-9]{32}/) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/alphalib/types/stackVersions.ts: -------------------------------------------------------------------------------- 1 | export const stackVersions = { 2 | ffmpeg: { 3 | recommendedVersion: 'v6' as const, 4 | test: /^v?[567](\.\d+)?(\.\d+)?$/, 5 | suggestedValues: ['v5', 'v6', 'v7'] as const, 6 | }, 7 | imagemagick: { 8 | recommendedVersion: 'v3' as const, 9 | test: /^v?[23](\.\d+)?(\.\d+)?$/, 10 | suggestedValues: ['v2', 'v3'] as const, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Transloadit Node SDK Repository Guide 2 | 3 | This document serves as a quick reference for agentic coding assistants working in this repository. 4 | 5 | After changes, run `yarn check` to check types, fix/check linting, and run unit tests. 6 | 7 | Detailed guidelines are organized in the following files: 8 | 9 | @.cursor/rules/coding-style.mdc 10 | @.cursor/rules/typescript.mdc @.cursor/rules/general.mdc 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["dist", "src", "coverage"], 3 | "references": [{ "path": "./tsconfig.build.json" }], 4 | "compilerOptions": { 5 | "checkJs": true, 6 | "erasableSyntaxOnly": true, 7 | "isolatedModules": true, 8 | "module": "NodeNext", 9 | "allowImportingTsExtensions": true, 10 | "noImplicitOverride": true, 11 | "noEmit": true, 12 | "resolveJsonModule": true, 13 | "strict": true, 14 | "types": ["vitest/globals"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/generate-coverage-badge.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'node:fs/promises' 4 | import { makeBadge } from 'badge-maker' 5 | 6 | const json = JSON.parse(await fs.readFile(process.argv[2], 'utf-8')) 7 | 8 | // We only care about "statements" 9 | const coveragePercent = `${json.total.statements.pct}%` 10 | 11 | // https://github.com/badges/shields/tree/master/badge-maker#format 12 | const format = { 13 | label: 'coverage', 14 | message: coveragePercent, 15 | color: 'green', 16 | } 17 | 18 | const svg = makeBadge(format) 19 | 20 | await fs.writeFile('coverage-badge.svg', svg) 21 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "exclude": ["test", "coverage", "dist", "examples"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "erasableSyntaxOnly": true, 9 | "isolatedModules": true, 10 | "module": "NodeNext", 11 | "allowImportingTsExtensions": true, 12 | "target": "ES2022", 13 | "noImplicitOverride": true, 14 | "rewriteRelativeImportExtensions": true, 15 | "outDir": "dist", 16 | "resolveJsonModule": true, 17 | "rootDir": "src", 18 | "sourceMap": true, 19 | "strict": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | Major changes by upcoming version. 2 | 3 | # Version 4 4 | 5 | - Upgrade tus-js-client to 3.x https://github.com/transloadit/node-sdk/pull/144 6 | - Require Node >=14.16 https://github.com/transloadit/node-sdk/pull/151 7 | - Improve error debuggability https://github.com/transloadit/node-sdk/issues/154 8 | - Bump all dependencies https://github.com/transloadit/node-sdk/issues/155 9 | 10 | # Version 5 11 | 12 | - Rewrite to ESM - might need to transpile to CJS 13 | - Support more platforms than Node.js https://github.com/transloadit/node-sdk/issues/153 14 | - Other improvements, see https://github.com/transloadit/node-sdk/issues/89 15 | 16 | # Version 6 17 | 18 | ... 19 | -------------------------------------------------------------------------------- /src/alphalib/types/assemblyReplayNotification.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { assemblyAuthInstructionsSchema, optionalStepsSchema } from './template.ts' 4 | 5 | export const assemblyReplayNotificationSchema = z 6 | .object({ 7 | auth: assemblyAuthInstructionsSchema, 8 | steps: optionalStepsSchema as typeof optionalStepsSchema, 9 | wait: z 10 | .boolean() 11 | .default(true) 12 | .describe( 13 | 'If it is provided with the value `false`, then the API request will return immediately even though the Notification is still in progress. This can be useful if your server takes some time to respond, but you do not want the replay API request to hang.', 14 | ), 15 | }) 16 | .strict() 17 | -------------------------------------------------------------------------------- /test/e2e/cli/assemblies-list.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import * as assemblies from '../../../src/cli/commands/assemblies.ts' 3 | import OutputCtl from './OutputCtl.ts' 4 | import type { OutputEntry } from './test-utils.ts' 5 | import { testCase } from './test-utils.ts' 6 | 7 | describe('assemblies', () => { 8 | describe('list', () => { 9 | it( 10 | 'should list assemblies', 11 | testCase(async (client) => { 12 | const output = new OutputCtl() 13 | await assemblies.list(output, client, { pagesize: 1 }) 14 | const logs = output.get() as OutputEntry[] 15 | expect(logs.filter((l) => l.type === 'error')).to.have.lengthOf(0) 16 | }), 17 | ) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /.cursor/rules/pr-comments.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | goal: address PR comments 7 | 8 | - get PR comments 9 | ```bash 10 | # Find PR for current branch 11 | gh pr list --head $(git branch --show-current) | cat 12 | 13 | # Get inline comments (most important) 14 | gh api repos/:owner/:repo/pulls/PR_NUMBER/comments --jq '.[] | {author: .user.login, body: .body, path: .path, line: .line}' | cat 15 | 16 | # Get review comments if needed 17 | gh api repos/:owner/:repo/pulls/PR_NUMBER/reviews --jq '.[] | select(.body != "") | {author: .user.login, body: .body}' | cat 18 | ``` 19 | 20 | - if no PR exists, abort 21 | - suggest fixes for each comment 22 | - always use `| cat` 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/alphalib/types/assemblyReplay.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { 4 | assemblyAuthInstructionsSchema, 5 | fieldsSchema, 6 | notifyUrlSchema, 7 | optionalStepsSchema, 8 | templateIdSchema, 9 | } from './template.ts' 10 | 11 | export const assemblyReplaySchema = z 12 | .object({ 13 | auth: assemblyAuthInstructionsSchema, 14 | steps: optionalStepsSchema as typeof optionalStepsSchema, 15 | template_id: templateIdSchema, 16 | notify_url: notifyUrlSchema, 17 | fields: fieldsSchema, 18 | reparse_template: z 19 | .union([z.literal(0), z.literal(1)]) 20 | .describe( 21 | 'Specify `1` to reparse the Template used in your Assembly (useful if the Template changed in the meantime). Alternatively, `0` replays the identical Steps used in the Assembly.', 22 | ), 23 | }) 24 | .strict() 25 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | include: ['src/**/*.ts'], 7 | exclude: ['**/*.d.ts', '**/*.test.ts', '**/test/**', '**/alphalib/**', '**/cli/**'], 8 | reporter: ['json', 'lcov', 'text', 'clover', 'json-summary', 'html'], 9 | provider: 'v8', 10 | thresholds: { 11 | // We want to boost this to 80%, but that should happen in a separate PR 12 | statements: 2, 13 | branches: 2, 14 | functions: 0, 15 | lines: 2, 16 | perFile: true, 17 | }, 18 | }, 19 | globals: true, 20 | testTimeout: 100000, 21 | exclude: [ 22 | '**/node_modules/**', 23 | '**/dist/**', 24 | 'test/e2e/cli/test-utils.ts', 25 | 'test/e2e/cli/OutputCtl.ts', 26 | ], 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /.cursor/rules/general.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: General 3 | globs: 4 | alwaysApply: true 5 | --- 6 | General: 7 | 8 | - Do not touch `.env` files! 9 | - Favor Yarn (4) over npm 10 | - Never run any dev server yourself. I have one running that auto-reloads on changes. 11 | - Avoid blocking the conversation with terminal commands. For example: A) most of my git commands run through pagers, so pipe their output to `cat` to avoid blocking the 12 | terminal. B) You can use `tail` for logs, but be smart and use `-n` instead of `-f`, or the conversation will block 13 | - Use the `gh` tool to interact with GitHub (search/view an Issue, create a PR). 14 | - All new files are to be in TypeScript. Even if someone suggests: make this new foo3 feature, model it after `foo1.js`, create: `foo3.ts`. Chances are, a `foo2.ts` already exist that you can take a look at also for inspiration. 15 | -------------------------------------------------------------------------------- /test/e2e/cli/bills.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import * as bills from '../../../src/cli/commands/bills.ts' 3 | import OutputCtl from './OutputCtl.ts' 4 | import type { OutputEntry } from './test-utils.ts' 5 | import { testCase } from './test-utils.ts' 6 | 7 | describe('bills', () => { 8 | describe('get', () => { 9 | it( 10 | 'should get bills', 11 | testCase(async (client) => { 12 | const output = new OutputCtl() 13 | const date = new Date() 14 | const month = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}` 15 | await bills.get(output, client, { months: [month] }) 16 | const logs = output.get() as OutputEntry[] 17 | expect(logs.filter((l) => l.type === 'error')).to.have.lengthOf(0) 18 | expect(logs.filter((l) => l.type === 'print')).to.have.length.above(0) 19 | }), 20 | ) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.biome": "explicit", 4 | "source.fixAll.eslint": "explicit", 5 | "source.fixAll.stylelint": "explicit" 6 | }, 7 | "editor.defaultFormatter": "biomejs.biome", 8 | "editor.formatOnSave": true, 9 | "eslint.format.enable": false, 10 | "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, 11 | "[javascript]": { "editor.defaultFormatter": "biomejs.biome" }, 12 | "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, 13 | "[json]": { "editor.defaultFormatter": "biomejs.biome" }, 14 | "[liquid]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, 15 | "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, 16 | "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, 17 | "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, 18 | "[yaml]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/alphalib/tryCatch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a successful result where error is null and data is present 3 | */ 4 | export type Success = [null, T] 5 | 6 | /** 7 | * Represents a failure result where error contains an error instance and data is null 8 | */ 9 | export type Failure = [E, null] 10 | 11 | /** 12 | * Represents the result of an operation that can either succeed with T or fail with E 13 | */ 14 | export type Result = Success | Failure 15 | 16 | /** 17 | * Wraps a promise in a try-catch block and returns a tuple of [error, data] 18 | * where exactly one value is non-null 19 | * 20 | * @param promise The promise to execute safely 21 | * @returns A tuple of [error, data] where one is null 22 | */ 23 | export async function tryCatch(promise: Promise): Promise> { 24 | try { 25 | const data = await promise 26 | return [null, data] as Success 27 | } catch (error) { 28 | return [error as E, null] as Failure 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.cursor/rules/typescript.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | For Typescript: 7 | 8 | - Favor `contentGapItemSchema = z.object()` over `ContentGapItemSchema = z.object()` 9 | - Favor `from './PosterboyCommand.ts'` over `from './PosterboyCommand'` 10 | - Favor `return ideas.filter(isPresent)` over `ideas.filter((idea): idea is Idea => idea !== null)` 11 | - Favor using `.tsx` over `.jsx` file extensions. 12 | - Favor the `tsx` CLI over `ts-node` for running TypeScript files. 13 | - Favor `satisfies` over `as`, consider `as` a sin 14 | - Favor `unknown` over `any`, consider `any` a sin 15 | - Favor validating data with Zod over using `any` or custom type guards 16 | - We use the `rewriteRelativeImportExtensions` TS 5.7 compiler option, so for local TypeScript 17 | files, import with the `.ts` / `.tsx` extension (not js, not extensionless) 18 | - Favor defining props as an interface over inline 19 | - Favor explicit return types over inferring them as it makes typescript a lot faster in the editor 20 | on our scale 21 | -------------------------------------------------------------------------------- /examples/rasterize_svg_to_png.ts: -------------------------------------------------------------------------------- 1 | // Run this file as: 2 | // 3 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/rasterize_svg_to_png.ts ./examples/fixtures/circle.svg 4 | // 5 | // You may need to build the project first using: 6 | // 7 | // yarn prepack 8 | // 9 | import { Transloadit } from 'transloadit' 10 | 11 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 12 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 13 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 14 | } 15 | const transloadit = new Transloadit({ 16 | authKey: TRANSLOADIT_KEY, 17 | authSecret: TRANSLOADIT_SECRET, 18 | }) 19 | 20 | const filePath = process.argv[2] 21 | 22 | const status = await transloadit.createAssembly({ 23 | files: { 24 | file1: filePath, 25 | }, 26 | params: { 27 | steps: { 28 | png: { 29 | use: ':original', 30 | robot: '/image/resize', 31 | format: 'png', 32 | }, 33 | }, 34 | }, 35 | waitForCompletion: true, 36 | }) 37 | console.log('Your PNG file:', status.results?.png?.[0]?.url) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tim Koschuetzki 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. -------------------------------------------------------------------------------- /examples/resize_an_image.ts: -------------------------------------------------------------------------------- 1 | // Run this file as: 2 | // 3 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/resize_an_image.ts ./examples/fixtures/berkley.jpg 4 | // 5 | // You may need to build the project first using: 6 | // 7 | // yarn prepack 8 | // 9 | import { Transloadit } from 'transloadit' 10 | 11 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 12 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 13 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 14 | } 15 | const transloadit = new Transloadit({ 16 | authKey: TRANSLOADIT_KEY, 17 | authSecret: TRANSLOADIT_SECRET, 18 | }) 19 | 20 | const status = await transloadit.createAssembly({ 21 | files: { 22 | file1: process.argv[2], 23 | }, 24 | params: { 25 | steps: { 26 | resized: { 27 | use: ':original', 28 | robot: '/image/resize', 29 | result: true, 30 | imagemagick_stack: 'v2.0.7', 31 | width: 75, 32 | height: 75, 33 | }, 34 | }, 35 | }, 36 | waitForCompletion: true, 37 | }) 38 | console.log('Your resized image:', status.results?.resize?.[0]?.url) 39 | -------------------------------------------------------------------------------- /examples/convert_to_webp.ts: -------------------------------------------------------------------------------- 1 | // Run this file as: 2 | // 3 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/convert_to_webp.ts ./examples/fixtures/berkley.jpg 4 | // 5 | // You may need to build the project first using: 6 | // 7 | // yarn prepack 8 | // 9 | import { Transloadit } from 'transloadit' 10 | 11 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 12 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 13 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 14 | } 15 | const transloadit = new Transloadit({ 16 | authKey: TRANSLOADIT_KEY, 17 | authSecret: TRANSLOADIT_SECRET, 18 | }) 19 | 20 | const filePath = process.argv[2] 21 | 22 | const status = await transloadit.createAssembly({ 23 | files: { 24 | file1: filePath, 25 | }, 26 | params: { 27 | steps: { 28 | webp: { 29 | use: ':original', 30 | robot: '/image/resize', 31 | result: true, 32 | imagemagick_stack: 'v2.0.7', 33 | format: 'webp', 34 | }, 35 | }, 36 | }, 37 | waitForCompletion: true, 38 | }) 39 | console.log('Your WebP file:', status.results?.webp?.[0]?.url) 40 | -------------------------------------------------------------------------------- /examples/retry.ts: -------------------------------------------------------------------------------- 1 | // yarn add p-retry 2 | // 3 | // Run this file as: 4 | // 5 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/retry.ts 6 | // 7 | // You may need to build the project first using: 8 | // 9 | // yarn prepack 10 | // 11 | import pRetry, { AbortError } from 'p-retry' 12 | import { ApiError, Transloadit } from 'transloadit' 13 | 14 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 15 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 16 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 17 | } 18 | const transloadit = new Transloadit({ 19 | authKey: TRANSLOADIT_KEY, 20 | authSecret: TRANSLOADIT_SECRET, 21 | }) 22 | 23 | async function run() { 24 | console.log('Trying...') 25 | try { 26 | const { items } = await transloadit.listTemplates({ sort: 'created', order: 'asc' }) 27 | return items 28 | } catch (err) { 29 | if (err instanceof ApiError && err.code === 'INVALID_SIGNATURE') { 30 | // This is an unrecoverable error, abort retry 31 | throw new AbortError('INVALID_SIGNATURE') 32 | } 33 | throw err 34 | } 35 | } 36 | 37 | console.log(await pRetry(run, { retries: 5 })) 38 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { realpathSync } from 'node:fs' 4 | import path from 'node:path' 5 | import process from 'node:process' 6 | import { fileURLToPath } from 'node:url' 7 | import 'dotenv/config' 8 | import { createCli } from './cli/commands/index.ts' 9 | 10 | const currentFile = realpathSync(fileURLToPath(import.meta.url)) 11 | 12 | function resolveInvokedPath(invoked?: string): string | null { 13 | if (invoked == null) return null 14 | try { 15 | return realpathSync(invoked) 16 | } catch { 17 | return path.resolve(invoked) 18 | } 19 | } 20 | 21 | export function shouldRunCli(invoked?: string): boolean { 22 | const resolved = resolveInvokedPath(invoked) 23 | if (resolved == null) return false 24 | return resolved === currentFile 25 | } 26 | 27 | export async function main(args = process.argv.slice(2)): Promise { 28 | const cli = createCli() 29 | const exitCode = await cli.run(args) 30 | if (exitCode !== 0) { 31 | process.exitCode = exitCode 32 | } 33 | } 34 | 35 | export function runCliWhenExecuted(): void { 36 | if (!shouldRunCli(process.argv[1])) return 37 | 38 | void main().catch((error) => { 39 | console.error((error as Error).message) 40 | process.exitCode = 1 41 | }) 42 | } 43 | 44 | runCliWhenExecuted() 45 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/assembly-savejson.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import type { RobotMetaInput } from './_instructions-primitives.ts' 3 | import { interpolateRobot, robotBase } from './_instructions-primitives.ts' 4 | 5 | // @ts-expect-error - AssemblySavejsonRobot is not ready yet @TODO please supply missing keys 6 | export const meta: RobotMetaInput = { 7 | name: 'AssemblySavejsonRobot', 8 | priceFactor: 0, 9 | queueSlotCount: 5, 10 | isAllowedForUrlTransform: true, 11 | trackOutputFileSize: false, 12 | isInternal: true, 13 | stage: 'ga', 14 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 15 | } 16 | 17 | export const robotAssemblySavejsonInstructionsSchema = robotBase 18 | .extend({ 19 | robot: z.literal('/assembly/savejson').describe(` 20 | TODO: Add robot description here 21 | `), 22 | }) 23 | .strict() 24 | 25 | export type RobotAssemblySavejsonInstructions = z.infer< 26 | typeof robotAssemblySavejsonInstructionsSchema 27 | > 28 | 29 | export const interpolatableRobotAssemblySavejsonInstructionsSchema = interpolateRobot( 30 | robotAssemblySavejsonInstructionsSchema, 31 | ) 32 | export type InterpolatableRobotAssemblySavejsonInstructions = 33 | InterpolatableRobotAssemblySavejsonInstructionsInput 34 | 35 | export type InterpolatableRobotAssemblySavejsonInstructionsInput = z.input< 36 | typeof interpolatableRobotAssemblySavejsonInstructionsSchema 37 | > 38 | -------------------------------------------------------------------------------- /src/PaginationStream.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'node:stream' 2 | import type { PaginationList, PaginationListWithCount } from './apiTypes.ts' 3 | 4 | type FetchPage = ( 5 | pageno: number, 6 | ) => 7 | | PaginationList 8 | | PromiseLike> 9 | | PaginationListWithCount 10 | | PromiseLike> 11 | 12 | export default class PaginationStream extends Readable { 13 | private _fetchPage: FetchPage 14 | 15 | private _nitems?: number 16 | 17 | private _pageno = 0 18 | 19 | private _items: T[] = [] 20 | 21 | private _itemsRead = 0 22 | 23 | constructor(fetchPage: FetchPage) { 24 | super({ objectMode: true }) 25 | this._fetchPage = fetchPage 26 | } 27 | 28 | override async _read() { 29 | if (this._items.length > 0) { 30 | this._itemsRead++ 31 | process.nextTick(() => this.push(this._items.pop())) 32 | return 33 | } 34 | 35 | if (this._nitems != null && this._itemsRead >= this._nitems) { 36 | process.nextTick(() => this.push(null)) 37 | return 38 | } 39 | 40 | try { 41 | const { items, ...rest } = await this._fetchPage(++this._pageno) 42 | if ('count' in rest) { 43 | this._nitems = rest.count 44 | } 45 | 46 | this._items = Array.from(items) 47 | this._items.reverse() 48 | 49 | this._read() 50 | } catch (err) { 51 | this.emit('error', err) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/progress-simulate.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | // @ts-expect-error - ProgressSimulateRobot is not ready yet @TODO please supply missing keys 7 | export const meta: RobotMetaInput = { 8 | name: 'ProgressSimulateRobot', 9 | priceFactor: 1, 10 | queueSlotCount: 20, 11 | isAllowedForUrlTransform: false, 12 | trackOutputFileSize: true, 13 | isInternal: true, 14 | stage: 'ga', 15 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 16 | } 17 | 18 | export const robotProgressSimulateInstructionsSchema = robotBase 19 | .merge(robotUse) 20 | .extend({ 21 | robot: z.literal('/progress/simulate'), 22 | duration: z.number(), 23 | output_files: z.number(), 24 | emit_progress: z.boolean(), 25 | predict_output: z.boolean(), 26 | }) 27 | .strict() 28 | export type RobotProgressSimulateInstructions = z.infer< 29 | typeof robotProgressSimulateInstructionsSchema 30 | > 31 | 32 | export const interpolatableRobotProgressSimulateInstructionsSchema = interpolateRobot( 33 | robotProgressSimulateInstructionsSchema, 34 | ) 35 | export type InterpolatableRobotProgressSimulateInstructions = 36 | InterpolatableRobotProgressSimulateInstructionsInput 37 | 38 | export type InterpolatableRobotProgressSimulateInstructionsInput = z.input< 39 | typeof interpolatableRobotProgressSimulateInstructionsSchema 40 | > 41 | -------------------------------------------------------------------------------- /src/ApiError.ts: -------------------------------------------------------------------------------- 1 | import type { RequestError } from 'got' 2 | import { HTTPError } from 'got' 3 | 4 | export interface TransloaditErrorResponseBody { 5 | error?: string 6 | message?: string 7 | reason?: string 8 | assembly_ssl_url?: string 9 | assembly_id?: string 10 | } 11 | 12 | export class ApiError extends Error { 13 | override name = 'ApiError' 14 | 15 | // there might not be an error code (or message) if the server didn't respond with any JSON response at all 16 | // e.g. if there was a 500 in the HTTP reverse proxy 17 | code?: string 18 | 19 | rawMessage?: string 20 | 21 | reason?: string 22 | 23 | assemblySslUrl?: string 24 | 25 | assemblyId?: string 26 | 27 | override cause?: RequestError | undefined 28 | 29 | constructor(params: { cause?: RequestError; body: TransloaditErrorResponseBody | undefined }) { 30 | const { cause, body = {} } = params 31 | 32 | const parts = ['API error'] 33 | if (cause instanceof HTTPError && cause?.response.statusCode) 34 | parts.push(`(HTTP ${cause.response.statusCode})`) 35 | if (body.error) parts.push(`${body.error}:`) 36 | if (body.message) parts.push(body.message) 37 | if (body.assembly_ssl_url) parts.push(body.assembly_ssl_url) 38 | 39 | const message = parts.join(' ') 40 | 41 | super(message) 42 | this.rawMessage = body.message 43 | this.reason = body.reason 44 | this.assemblyId = body.assembly_id 45 | this.assemblySslUrl = body.assembly_ssl_url 46 | this.code = body.error 47 | this.cause = cause 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/fetch_costs_of_all_assemblies_in_timeframe.ts: -------------------------------------------------------------------------------- 1 | // Run this file as: 2 | // 3 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/fetch_costs_of_all_assemblies_in_timeframe.ts 4 | // 5 | // You may need to build the project first using: 6 | // 7 | // yarn prepack 8 | // 9 | import pMap from 'p-map' 10 | import { Transloadit } from 'transloadit' 11 | 12 | const fromdate = '2020-12-31 15:30:00' 13 | const todate = '2020-12-31 15:30:01' 14 | 15 | const params = { 16 | fromdate, 17 | todate, 18 | page: 1, 19 | } 20 | 21 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 22 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 23 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 24 | } 25 | const transloadit = new Transloadit({ 26 | authKey: TRANSLOADIT_KEY, 27 | authSecret: TRANSLOADIT_SECRET, 28 | }) 29 | 30 | let totalBytes = 0 31 | 32 | let lastCount: number 33 | do { 34 | console.log('Processing page', params.page) 35 | const { count, items } = await transloadit.listAssemblies(params) 36 | lastCount = count 37 | params.page++ 38 | 39 | await pMap( 40 | items, 41 | async (assembly) => { 42 | const assemblyFull = await transloadit.getAssembly(assembly.id) 43 | // console.log(assemblyFull.assembly_id) 44 | 45 | const { bytes_usage: bytesUsage } = assemblyFull 46 | 47 | totalBytes += bytesUsage || 0 48 | }, 49 | { concurrency: 20 }, 50 | ) 51 | } while (lastCount > 0) 52 | 53 | console.log('Total GB:', (totalBytes / (1024 * 1024 * 1024)).toFixed(2)) 54 | -------------------------------------------------------------------------------- /src/alphalib/types/assembliesGet.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | 3 | import { assemblyAuthInstructionsSchema } from './template.ts' 4 | 5 | export const assembliesGetSchema = z 6 | .object({ 7 | auth: assemblyAuthInstructionsSchema, 8 | page: z 9 | .number() 10 | .int() 11 | .default(1) 12 | .describe('Specifies the current page, within the current pagination'), 13 | pagesize: z 14 | .number() 15 | .int() 16 | .min(1) 17 | .max(5000) 18 | .default(50) 19 | .describe( 20 | 'Specifies how many Assemblies to be received per API request, which is useful for pagination.', 21 | ), 22 | type: z 23 | .enum(['all', 'uploading', 'executing', 'canceled', 'completed', 'failed', 'request_aborted']) 24 | .describe('Specifies the types of Assemblies to be retrieved.'), 25 | fromdate: z 26 | .string() 27 | .describe( 28 | 'Specifies the minimum Assembly UTC creation date/time. Only Assemblies after this time will be retrieved. Use the format `Y-m-d H:i:s`.', 29 | ), 30 | todate: z 31 | .string() 32 | .default('NOW()') 33 | .describe( 34 | 'Specifies the maximum Assembly UTC creation date/time. Only Assemblies before this time will be retrieved. Use the format `Y-m-d H:i:s`.', 35 | ), 36 | keywords: z 37 | .array(z.string()) 38 | .describe( 39 | 'Specifies keywords to be matched in the Assembly Status. The Assembly fields checked include the `id`, `redirect_url`, `fields`, and `notify_url`, as well as error messages and files used.', 40 | ), 41 | }) 42 | .strict() 43 | -------------------------------------------------------------------------------- /src/alphalib/types/templateCredential.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { assemblyAuthInstructionsSchema } from './template.ts' 4 | 5 | export const retrieveTemplateCredentialsParamsSchema = z 6 | .object({ 7 | auth: assemblyAuthInstructionsSchema, 8 | }) 9 | .strict() 10 | 11 | export const templateCredentialsSchema = z 12 | .object({ 13 | auth: assemblyAuthInstructionsSchema, 14 | name: z 15 | .string() 16 | .min(4) 17 | .regex(/^[a-zA-Z-]+$/) 18 | .describe( 19 | 'Name of the Template Credentials. Must be longer than 3 characters, can only contain dashes and latin letters.', 20 | ), 21 | type: z 22 | .enum([ 23 | 'ai', 24 | 'azure', 25 | 'backblaze', 26 | 'cloudflare', 27 | 'companion', 28 | 'digitalocean', 29 | 'dropbox', 30 | 'ftp', 31 | 'google', 32 | 'http', 33 | 'minio', 34 | 'rackspace', 35 | 's3', 36 | 'sftp', 37 | 'supabase', 38 | 'swift', 39 | 'tigris', 40 | 'vimeo', 41 | 'wasabi', 42 | 'youtube', 43 | ]) 44 | .describe('The service to create credentials for.'), 45 | content: z 46 | .object({}) 47 | .describe(`Key and value pairs which fill in the details of the Template Credentials. For example, for an S3 bucket, this would be a valid content object to send: 48 | 49 | \`\`\`jsonc 50 | { 51 | "content": { 52 | "key": "xyxy", 53 | "secret": "xyxyxyxy", 54 | "bucket" : "mybucket.example.com", 55 | "bucket_region": "us-east-1" 56 | } 57 | } 58 | \`\`\` 59 | `), 60 | }) 61 | .strict() 62 | -------------------------------------------------------------------------------- /src/cli/helpers.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import type { Readable } from 'node:stream' 3 | import type { APIError } from './types.ts' 4 | import { isAPIError } from './types.ts' 5 | 6 | export function getEnvCredentials(): { authKey: string; authSecret: string } | null { 7 | const authKey = process.env.TRANSLOADIT_KEY ?? process.env.TRANSLOADIT_AUTH_KEY 8 | const authSecret = process.env.TRANSLOADIT_SECRET ?? process.env.TRANSLOADIT_AUTH_SECRET 9 | 10 | if (!authKey || !authSecret) return null 11 | 12 | return { authKey, authSecret } 13 | } 14 | 15 | export function createReadStream(file: string): Readable { 16 | if (file === '-') return process.stdin 17 | return fs.createReadStream(file) 18 | } 19 | 20 | export async function streamToBuffer(stream: Readable): Promise { 21 | const chunks: Buffer[] = [] 22 | for await (const chunk of stream) { 23 | chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) 24 | } 25 | return Buffer.concat(chunks) 26 | } 27 | 28 | export function formatAPIError(err: unknown): string { 29 | if (isAPIError(err)) { 30 | return `${err.error}: ${err.message}` 31 | } 32 | if (err instanceof Error) { 33 | return err.message 34 | } 35 | return String(err) 36 | } 37 | 38 | // Re-export APIError type for convenience 39 | export type { APIError } 40 | 41 | export function zip(listA: A[], listB: B[]): [A, B][] 42 | export function zip(...lists: T[][]): T[][] 43 | export function zip(...lists: T[][]): T[][] { 44 | const length = Math.max(...lists.map((list) => list.length)) 45 | const result: T[][] = new Array(length) 46 | for (let i = 0; i < result.length; i++) { 47 | result[i] = lists.map((list) => list[i] as T) 48 | } 49 | return result 50 | } 51 | -------------------------------------------------------------------------------- /test/e2e/cli/OutputCtl.ts: -------------------------------------------------------------------------------- 1 | import type { LogLevelValue, OutputCtlOptions } from '../../../src/cli/OutputCtl.ts' 2 | import { LOG_LEVEL_DEFAULT } from '../../../src/cli/OutputCtl.ts' 3 | 4 | interface OutputEntry { 5 | type: 'error' | 'warn' | 'notice' | 'info' | 'debug' | 'trace' | 'print' 6 | msg: unknown 7 | json?: unknown 8 | } 9 | 10 | /** 11 | * Test version of OutputCtl that captures output for verification 12 | * instead of writing to console. Implements the same interface as src/cli/OutputCtl. 13 | */ 14 | export default class OutputCtl { 15 | private output: OutputEntry[] 16 | // These properties are required by the src/cli/OutputCtl interface but not used in tests 17 | private json: boolean 18 | private logLevel: LogLevelValue 19 | 20 | constructor({ logLevel = LOG_LEVEL_DEFAULT, jsonMode = false }: OutputCtlOptions = {}) { 21 | this.output = [] 22 | this.json = jsonMode 23 | this.logLevel = logLevel 24 | } 25 | 26 | error(msg: unknown): void { 27 | this.output.push({ type: 'error', msg }) 28 | } 29 | 30 | warn(msg: unknown): void { 31 | this.output.push({ type: 'warn', msg }) 32 | } 33 | 34 | notice(msg: unknown): void { 35 | this.output.push({ type: 'notice', msg }) 36 | } 37 | 38 | info(msg: unknown): void { 39 | this.output.push({ type: 'info', msg }) 40 | } 41 | 42 | debug(msg: unknown): void { 43 | this.output.push({ type: 'debug', msg }) 44 | } 45 | 46 | trace(msg: unknown): void { 47 | this.output.push({ type: 'trace', msg }) 48 | } 49 | 50 | print(msg: unknown, json?: unknown): void { 51 | this.output.push({ type: 'print', msg, json }) 52 | } 53 | 54 | get(debug = false): OutputEntry[] { 55 | return this.output.filter((line) => debug || line.type !== 'debug') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/face_detect_download.ts: -------------------------------------------------------------------------------- 1 | // Run this file as: 2 | // 3 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/face_detect_download.ts ./examples/fixtures/berkley.jpg 4 | // 5 | // You may need to build the project first using: 6 | // 7 | // yarn prepack 8 | // 9 | // This example will take an image and find a face and crop out the face. 10 | // Then it will download the result as a file in the current directory 11 | // See https://transloadit.com/demos/artificial-intelligence/detect-faces-in-images/ 12 | 13 | import assert from 'node:assert' 14 | import { createWriteStream } from 'node:fs' 15 | import got from 'got' 16 | import { Transloadit } from 'transloadit' 17 | 18 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 19 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 20 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 21 | } 22 | const transloadit = new Transloadit({ 23 | authKey: TRANSLOADIT_KEY, 24 | authSecret: TRANSLOADIT_SECRET, 25 | }) 26 | 27 | const filePath = process.argv[2] 28 | 29 | const status = await transloadit.createAssembly({ 30 | files: { 31 | file1: filePath, 32 | }, 33 | params: { 34 | steps: { 35 | facesDetected: { 36 | use: ':original', 37 | robot: '/image/facedetect', 38 | crop: true, 39 | crop_padding: '10%', 40 | faces: 'max-confidence', 41 | format: 'preserve', 42 | }, 43 | }, 44 | }, 45 | waitForCompletion: true, 46 | }) 47 | 48 | // Now save the file 49 | const outPath = './output-face.jpg' 50 | const stream = createWriteStream(outPath) 51 | const url = status.results?.facesDetected?.[0]?.url 52 | assert(url != null) 53 | got.stream(url).pipe(stream) 54 | console.log('Your cropped face has been saved to', outPath) 55 | -------------------------------------------------------------------------------- /examples/template_api.ts: -------------------------------------------------------------------------------- 1 | // Run this file as: 2 | // 3 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/template_api.ts 4 | // 5 | // You may need to build the project first using: 6 | // 7 | // yarn prepack 8 | // 9 | import type { TemplateContent } from 'transloadit' 10 | import { Transloadit } from 'transloadit' 11 | 12 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 13 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 14 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 15 | } 16 | const transloadit = new Transloadit({ 17 | authKey: TRANSLOADIT_KEY, 18 | authSecret: TRANSLOADIT_SECRET, 19 | }) 20 | 21 | const template: TemplateContent = { 22 | steps: { 23 | encode: { 24 | use: ':original', 25 | robot: '/video/encode', 26 | preset: 'ipad-high', 27 | }, 28 | thumbnail: { 29 | use: 'encode', 30 | robot: '/video/thumbs', 31 | }, 32 | }, 33 | } 34 | 35 | const { count } = await transloadit.listTemplates({ sort: 'created', order: 'asc' }) 36 | console.log('Successfully fetched', count, 'template(s)') 37 | 38 | const createTemplateResult = await transloadit.createTemplate({ 39 | name: 'node-sdk-test1', 40 | template, 41 | }) 42 | console.log('Template created successfully:', createTemplateResult) 43 | 44 | const editResult = await transloadit.editTemplate(createTemplateResult.id, { 45 | name: 'node-sdk-test2', 46 | template, 47 | }) 48 | console.log('Successfully edited template', editResult) 49 | 50 | const getTemplateResult = await transloadit.getTemplate(createTemplateResult.id) 51 | console.log('Successfully fetched template', getTemplateResult) 52 | 53 | const delResult = await transloadit.deleteTemplate(createTemplateResult.id) 54 | console.log('Successfully deleted template', delResult) 55 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/meta-read.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import type { RobotMetaInput } from './_instructions-primitives.ts' 3 | import { interpolateRobot, robotBase } from './_instructions-primitives.ts' 4 | 5 | // @ts-expect-error - MetaReadRobot is not ready yet @TODO please supply missing keys 6 | export const meta: RobotMetaInput = { 7 | name: 'MetaReadRobot', 8 | priceFactor: 0, 9 | queueSlotCount: 15, 10 | isAllowedForUrlTransform: true, 11 | trackOutputFileSize: false, 12 | isInternal: true, 13 | stage: 'ga', 14 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 15 | } 16 | 17 | export const robotMetaReadInstructionsSchema = robotBase 18 | .extend({ 19 | robot: z.literal('/meta/read').describe('Reads metadata from a file.'), 20 | }) 21 | .strict() 22 | 23 | export type RobotMetaReadInstructions = z.infer 24 | 25 | export const robotMetaReadInstructionsWithHiddenFieldsSchema = 26 | robotMetaReadInstructionsSchema.extend({ 27 | result: z.union([z.literal('debug'), robotMetaReadInstructionsSchema.shape.result]).optional(), 28 | }) 29 | 30 | export const interpolatableRobotMetaReadInstructionsSchema = interpolateRobot( 31 | robotMetaReadInstructionsSchema, 32 | ) 33 | export type InterpolatableRobotMetaReadInstructions = InterpolatableRobotMetaReadInstructionsInput 34 | 35 | export type InterpolatableRobotMetaReadInstructionsInput = z.input< 36 | typeof interpolatableRobotMetaReadInstructionsSchema 37 | > 38 | 39 | export const interpolatableRobotMetaReadInstructionsWithHiddenFieldsSchema = interpolateRobot( 40 | robotMetaReadInstructionsWithHiddenFieldsSchema, 41 | ) 42 | export type InterpolatableRobotMetaReadInstructionsWithHiddenFields = z.input< 43 | typeof interpolatableRobotMetaReadInstructionsWithHiddenFieldsSchema 44 | > 45 | -------------------------------------------------------------------------------- /src/cli/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { Builtins, Cli } from 'clipanion' 2 | 3 | import packageJson from '../../../package.json' with { type: 'json' } 4 | 5 | import { 6 | AssembliesCreateCommand, 7 | AssembliesDeleteCommand, 8 | AssembliesGetCommand, 9 | AssembliesListCommand, 10 | AssembliesReplayCommand, 11 | } from './assemblies.ts' 12 | 13 | import { SignatureCommand, SmartCdnSignatureCommand } from './auth.ts' 14 | 15 | import { BillsGetCommand } from './bills.ts' 16 | 17 | import { NotificationsReplayCommand } from './notifications.ts' 18 | 19 | import { 20 | TemplatesCreateCommand, 21 | TemplatesDeleteCommand, 22 | TemplatesGetCommand, 23 | TemplatesListCommand, 24 | TemplatesModifyCommand, 25 | TemplatesSyncCommand, 26 | } from './templates.ts' 27 | 28 | export function createCli(): Cli { 29 | const cli = new Cli({ 30 | binaryLabel: 'Transloadit CLI', 31 | binaryName: 'transloadit', 32 | binaryVersion: packageJson.version, 33 | }) 34 | 35 | // Built-in commands 36 | cli.register(Builtins.HelpCommand) 37 | cli.register(Builtins.VersionCommand) 38 | 39 | // Auth commands (signature generation) 40 | cli.register(SignatureCommand) 41 | cli.register(SmartCdnSignatureCommand) 42 | 43 | // Assemblies commands 44 | cli.register(AssembliesCreateCommand) 45 | cli.register(AssembliesListCommand) 46 | cli.register(AssembliesGetCommand) 47 | cli.register(AssembliesDeleteCommand) 48 | cli.register(AssembliesReplayCommand) 49 | 50 | // Templates commands 51 | cli.register(TemplatesCreateCommand) 52 | cli.register(TemplatesGetCommand) 53 | cli.register(TemplatesModifyCommand) 54 | cli.register(TemplatesDeleteCommand) 55 | cli.register(TemplatesListCommand) 56 | cli.register(TemplatesSyncCommand) 57 | 58 | // Bills commands 59 | cli.register(BillsGetCommand) 60 | 61 | // Notifications commands 62 | cli.register(NotificationsReplayCommand) 63 | 64 | return cli 65 | } 66 | -------------------------------------------------------------------------------- /src/cli/commands/notifications.ts: -------------------------------------------------------------------------------- 1 | import { Command, Option } from 'clipanion' 2 | import { tryCatch } from '../../alphalib/tryCatch.ts' 3 | import type { Transloadit } from '../../Transloadit.ts' 4 | import type { IOutputCtl } from '../OutputCtl.ts' 5 | import { ensureError } from '../types.ts' 6 | import { AuthenticatedCommand } from './BaseCommand.ts' 7 | 8 | // --- Types and business logic --- 9 | 10 | export interface NotificationsReplayOptions { 11 | notify_url?: string 12 | assemblies: string[] 13 | } 14 | 15 | export async function replay( 16 | output: IOutputCtl, 17 | client: Transloadit, 18 | { notify_url, assemblies }: NotificationsReplayOptions, 19 | ): Promise { 20 | const promises = assemblies.map((id) => client.replayAssemblyNotification(id, { notify_url })) 21 | const [err] = await tryCatch(Promise.all(promises)) 22 | if (err) { 23 | output.error(ensureError(err).message) 24 | } 25 | } 26 | 27 | // --- Command class --- 28 | 29 | export class NotificationsReplayCommand extends AuthenticatedCommand { 30 | static override paths = [ 31 | ['assembly-notifications', 'replay'], 32 | ['notifications', 'replay'], 33 | ['notification', 'replay'], 34 | ['n', 'replay'], 35 | ['n', 'r'], 36 | ] 37 | 38 | static override usage = Command.Usage({ 39 | category: 'Notifications', 40 | description: 'Replay notifications for assemblies', 41 | examples: [ 42 | ['Replay notifications', 'transloadit assembly-notifications replay ASSEMBLY_ID'], 43 | [ 44 | 'Replay to a new URL', 45 | 'transloadit assembly-notifications replay --notify-url https://example.com/notify ASSEMBLY_ID', 46 | ], 47 | ], 48 | }) 49 | 50 | notifyUrl = Option.String('--notify-url', { 51 | description: 'Specify a new URL to send the notifications to', 52 | }) 53 | 54 | assemblyIds = Option.Rest({ required: 1 }) 55 | 56 | protected async run(): Promise { 57 | await replay(this.output, this.client, { 58 | notify_url: this.notifyUrl, 59 | assemblies: this.assemblyIds, 60 | }) 61 | return undefined 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/file-watermark.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | // @ts-expect-error - FileWatermarkRobot is not ready yet @TODO please supply missing keys 7 | export const meta: RobotMetaInput = { 8 | name: 'FileWatermarkRobot', 9 | priceFactor: 4, 10 | queueSlotCount: 20, 11 | isAllowedForUrlTransform: true, 12 | trackOutputFileSize: false, 13 | isInternal: false, 14 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 15 | stage: 'ga', 16 | } 17 | 18 | export const robotFileWatermarkInstructionsSchema = robotBase 19 | .merge(robotUse) 20 | .extend({ 21 | robot: z.literal('/file/watermark'), 22 | randomize: z.boolean().optional(), 23 | }) 24 | .strict() 25 | 26 | export const robotFileWatermarkInstructionsWithHiddenFieldsSchema = 27 | robotFileWatermarkInstructionsSchema.extend({ 28 | result: z 29 | .union([z.literal('debug'), robotFileWatermarkInstructionsSchema.shape.result]) 30 | .optional(), 31 | }) 32 | 33 | export type RobotFileWatermarkInstructions = z.infer 34 | export type RobotFileWatermarkInstructionsWithHiddenFields = z.infer< 35 | typeof robotFileWatermarkInstructionsWithHiddenFieldsSchema 36 | > 37 | 38 | export const interpolatableRobotFileWatermarkInstructionsSchema = interpolateRobot( 39 | robotFileWatermarkInstructionsSchema, 40 | ) 41 | export type InterpolatableRobotFileWatermarkInstructions = 42 | InterpolatableRobotFileWatermarkInstructionsInput 43 | 44 | export type InterpolatableRobotFileWatermarkInstructionsInput = z.input< 45 | typeof interpolatableRobotFileWatermarkInstructionsSchema 46 | > 47 | 48 | export const interpolatableRobotFileWatermarkInstructionsWithHiddenFieldsSchema = interpolateRobot( 49 | robotFileWatermarkInstructionsWithHiddenFieldsSchema, 50 | ) 51 | export type InterpolatableRobotFileWatermarkInstructionsWithHiddenFields = z.infer< 52 | typeof interpolatableRobotFileWatermarkInstructionsWithHiddenFieldsSchema 53 | > 54 | export type InterpolatableRobotFileWatermarkInstructionsWithHiddenFieldsInput = z.input< 55 | typeof interpolatableRobotFileWatermarkInstructionsWithHiddenFieldsSchema 56 | > 57 | -------------------------------------------------------------------------------- /test/e2e/cli/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process' 2 | import fsp from 'node:fs/promises' 3 | import path from 'node:path' 4 | import process from 'node:process' 5 | import { fileURLToPath } from 'node:url' 6 | import { promisify } from 'node:util' 7 | import { rimraf } from 'rimraf' 8 | import 'dotenv/config' 9 | import { Transloadit as TransloaditClient } from '../../../src/Transloadit.ts' 10 | 11 | export const execAsync = promisify(exec) 12 | 13 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 14 | export const cliPath = path.resolve(__dirname, '../../../src/cli.ts') 15 | 16 | export const tmpDir = '/tmp' 17 | 18 | if (!process.env.TRANSLOADIT_KEY || !process.env.TRANSLOADIT_SECRET) { 19 | console.error( 20 | 'Please provide environment variables TRANSLOADIT_KEY and TRANSLOADIT_SECRET to run tests', 21 | ) 22 | process.exit(1) 23 | } 24 | 25 | export const authKey = process.env.TRANSLOADIT_KEY 26 | export const authSecret = process.env.TRANSLOADIT_SECRET 27 | 28 | process.setMaxListeners(Number.POSITIVE_INFINITY) 29 | 30 | export function delay(ms: number): Promise { 31 | return new Promise((resolve) => setTimeout(resolve, ms)) 32 | } 33 | 34 | export interface OutputEntry { 35 | type: string 36 | msg: unknown 37 | json?: { id?: string; assembly_id?: string } & Record 38 | } 39 | 40 | export function testCase(cb: (client: TransloaditClient) => Promise): () => Promise { 41 | const cwd = process.cwd() 42 | return async () => { 43 | const dirname = path.join( 44 | tmpDir, 45 | `transloadit_test-${Date.now()}-${Math.floor(Math.random() * 10000)}`, 46 | ) 47 | const client = new TransloaditClient({ authKey, authSecret }) 48 | try { 49 | await fsp.mkdir(dirname) 50 | process.chdir(dirname) 51 | return await cb(client) 52 | } finally { 53 | process.chdir(cwd) 54 | await rimraf(dirname) 55 | } 56 | } 57 | } 58 | 59 | export function runCli( 60 | args: string, 61 | env: Record = {}, 62 | ): Promise<{ stdout: string; stderr: string }> { 63 | return execAsync(`npx tsx ${cliPath} ${args}`, { 64 | env: { ...process.env, ...env }, 65 | }) 66 | } 67 | 68 | export function createClient(): TransloaditClient { 69 | return new TransloaditClient({ authKey, authSecret }) 70 | } 71 | -------------------------------------------------------------------------------- /.cursor/rules/coding-style.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | 7 | Coding style: 8 | 9 | - Favor `async run() {` over `run = async () => {` inside ES6 classes 10 | - Favor `if (!(err instanceof Error)) { throw new Error(`Was thrown a non-error: ${err}`) }` inside 11 | `catch` blocks to ensure the `error` is always an instance of `Error` 12 | - Favor using real paths (`../lib/schemas.ts`) over aliases (`@/app/lib/schemas`). 13 | - Favor `for (const comment of comments) {` over `comments.forEach((comment) => {` 14 | - Favor named exports over default exports, with the exception of Next.js pages 15 | - Do not wrap each function body and function call in `try`/`catch` blocks. It pollutes the code. 16 | Assume we will always have an e.g. 17 | `main().catch((err) => { console.error(err); process.exit(1) })` to catch us. I repeat: Avoid 18 | over-use of try-catch such as 19 | `try { // foo } catch (err) { console.error('error while foo'); throw err }`, assume we catch 20 | errors on a higher level and do not need the extra explananation. 21 | - If you must use try/catch, for simple cases, favor `alphalib/tryCatch.ts` 22 | (`const [err, data] = await tryCatch(promise)`) over 23 | `let data; try { data = await promise } catch (err) { }` 24 | - Before creating new files and new code, see if we can leverage existing work, maybe slighty adapt 25 | that without breaking BC, to keep things DRY. 26 | - Favor early exits, so quickly `continue`, `return false` (or `throw` if needed), over nesting 27 | everything in positive conditions, creating christmas trees. 28 | - Use Prettier with 100 char line width, single quotes for JS/TS, semi: false 29 | - Use descriptive names: PascalCase for components/types, camelCase for variables/methods/schemas 30 | - Alphabetize imports, group by source type (built-in/external/internal) 31 | - Favor US English over UK English, so `summarizeError` over `summarise Error` 32 | - Favor `.replaceAll('a', 'b)` over `.replace(/a/g, 'b')` or `.replace(new RegExp('a', 'g'), 'b')` when the only need for regeses was replacing all strings. That's usually both easier to read and more performant. 33 | - Use typographic characters: ellipsis (`…`) instead of `...`, curly quotes (`'` `"`) instead of straight quotes in user-facing text 34 | - Put API keys and secrets in `.env` files, not hardcoded in components 35 | - Check for existing hooks before creating new ones (e.g., `useUppy()` for Uppy functionality) 36 | -------------------------------------------------------------------------------- /src/cli/commands/BaseCommand.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import process from 'node:process' 3 | import { Command, Option } from 'clipanion' 4 | import { Transloadit as TransloaditClient } from '../../Transloadit.ts' 5 | import { getEnvCredentials } from '../helpers.ts' 6 | import type { IOutputCtl } from '../OutputCtl.ts' 7 | import OutputCtl, { LOG_LEVEL_DEFAULT, LOG_LEVEL_NAMES, parseLogLevel } from '../OutputCtl.ts' 8 | 9 | export abstract class BaseCommand extends Command { 10 | logLevelOption = Option.String('-l,--log-level', { 11 | description: `Log level: ${LOG_LEVEL_NAMES.join(', ')} or 3-8 (default: notice)`, 12 | }) 13 | 14 | json = Option.Boolean('-j,--json', false, { 15 | description: 'Output in JSON format', 16 | }) 17 | 18 | endpoint = Option.String('--endpoint', { 19 | description: 20 | 'API endpoint URL (default: https://api2.transloadit.com, or TRANSLOADIT_ENDPOINT env var)', 21 | }) 22 | 23 | protected output!: IOutputCtl 24 | protected client!: TransloaditClient 25 | 26 | protected setupOutput(): void { 27 | const logLevel = this.logLevelOption ? parseLogLevel(this.logLevelOption) : LOG_LEVEL_DEFAULT 28 | this.output = new OutputCtl({ 29 | logLevel, 30 | jsonMode: this.json, 31 | }) 32 | } 33 | 34 | protected setupClient(): boolean { 35 | const creds = getEnvCredentials() 36 | if (!creds) { 37 | this.output.error( 38 | 'Please provide API authentication in the environment variables TRANSLOADIT_KEY and TRANSLOADIT_SECRET', 39 | ) 40 | return false 41 | } 42 | 43 | const endpoint = this.endpoint || process.env.TRANSLOADIT_ENDPOINT 44 | 45 | this.client = new TransloaditClient({ ...creds, ...(endpoint && { endpoint }) }) 46 | return true 47 | } 48 | 49 | abstract override execute(): Promise 50 | } 51 | 52 | export abstract class AuthenticatedCommand extends BaseCommand { 53 | override async execute(): Promise { 54 | this.setupOutput() 55 | if (!this.setupClient()) { 56 | return 1 57 | } 58 | return await this.run() 59 | } 60 | 61 | protected abstract run(): Promise 62 | } 63 | 64 | export abstract class UnauthenticatedCommand extends BaseCommand { 65 | override async execute(): Promise { 66 | this.setupOutput() 67 | return await this.run() 68 | } 69 | 70 | protected abstract run(): Promise 71 | } 72 | -------------------------------------------------------------------------------- /test/unit/test-pagination-stream.test.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from 'node:stream' 2 | 3 | import PaginationStream from '../../src/PaginationStream.ts' 4 | 5 | const toArray = (callback: (list: number[]) => void) => { 6 | const writable = new Writable({ objectMode: true }) 7 | const list: number[] = [] 8 | writable.write = (chunk) => { 9 | list.push(chunk) 10 | return true 11 | } 12 | 13 | writable.end = () => { 14 | callback(list) 15 | return writable 16 | } 17 | 18 | return writable 19 | } 20 | 21 | describe('PaginationStream', () => { 22 | it('should preserve order with synchronous data sources', async () => { 23 | const count = 9 24 | const pages = [ 25 | { count, items: [1, 2, 3] }, 26 | { count, items: [4, 5, 6] }, 27 | { count, items: [7, 8, 9] }, 28 | ] 29 | 30 | const stream = new PaginationStream(async (pageno) => pages[pageno - 1]) 31 | 32 | await new Promise((resolve) => { 33 | stream.pipe( 34 | toArray((array) => { 35 | const expected = pages.flatMap(({ items }) => items) 36 | 37 | expect(array).toEqual(expected) 38 | resolve() 39 | }), 40 | ) 41 | 42 | stream.resume() 43 | }) 44 | }) 45 | 46 | it('should preserve order with asynchronous data sources', async () => { 47 | const count = 9 48 | const pages = [ 49 | { count, items: [1, 2, 3] }, 50 | { count, items: [4, 5, 6] }, 51 | { count, items: [7, 8, 9] }, 52 | ] 53 | 54 | const stream = new PaginationStream( 55 | async (pageno) => 56 | new Promise((resolve) => { 57 | process.nextTick(() => resolve(pages[pageno - 1])) 58 | }), 59 | ) 60 | 61 | await new Promise((resolve) => { 62 | stream.pipe( 63 | toArray((array) => { 64 | const expected = pages.flatMap(({ items }) => items) 65 | 66 | expect(array).toEqual(expected) 67 | resolve() 68 | }), 69 | ) 70 | 71 | stream.resume() 72 | }) 73 | }) 74 | 75 | it('supports responses without count before count becomes available', async () => { 76 | const pages = [{ items: [1, 2] }, { count: 3, items: [3] }] 77 | 78 | const stream = new PaginationStream(async (pageno) => pages[pageno - 1]) 79 | 80 | await new Promise((resolve) => { 81 | stream.pipe( 82 | toArray((array) => { 83 | expect(array).toEqual([1, 2, 3]) 84 | resolve() 85 | }), 86 | ) 87 | 88 | stream.resume() 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transloadit", 3 | "version": "4.1.2", 4 | "description": "Node.js SDK for Transloadit", 5 | "type": "module", 6 | "keywords": [ 7 | "transloadit", 8 | "encoding", 9 | "transcoding", 10 | "video", 11 | "audio", 12 | "mp3" 13 | ], 14 | "author": "Tim Koschuetzki ", 15 | "packageManager": "yarn@4.12.0", 16 | "engines": { 17 | "node": ">= 20" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-s3": "^3.891.0", 21 | "@aws-sdk/s3-request-presigner": "^3.891.0", 22 | "@transloadit/sev-logger": "^0.0.15", 23 | "clipanion": "^4.0.0-rc.4", 24 | "debug": "^4.4.3", 25 | "dotenv": "^17.2.3", 26 | "form-data": "^4.0.4", 27 | "got": "14.4.9", 28 | "into-stream": "^9.0.0", 29 | "is-stream": "^4.0.1", 30 | "node-watch": "^0.7.4", 31 | "p-map": "^7.0.3", 32 | "p-queue": "^9.0.1", 33 | "recursive-readdir": "^2.2.3", 34 | "tus-js-client": "^4.3.1", 35 | "type-fest": "^4.41.0", 36 | "zod": "3.25.76" 37 | }, 38 | "devDependencies": { 39 | "@biomejs/biome": "^2.2.4", 40 | "@types/debug": "^4.1.12", 41 | "@types/recursive-readdir": "^2.2.4", 42 | "@types/temp": "^0.9.4", 43 | "@vitest/coverage-v8": "^3.2.4", 44 | "badge-maker": "^5.0.2", 45 | "execa": "9.6.0", 46 | "image-size": "^2.0.2", 47 | "minimatch": "^10.1.1", 48 | "nock": "^14.0.10", 49 | "npm-run-all": "^4.1.5", 50 | "p-retry": "^7.0.0", 51 | "rimraf": "^6.1.2", 52 | "temp": "^0.9.4", 53 | "tsx": "4.21.0", 54 | "typescript": "5.9.3", 55 | "vitest": "^3.2.4" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git://github.com/transloadit/node-sdk.git" 60 | }, 61 | "directories": { 62 | "src": "./src" 63 | }, 64 | "scripts": { 65 | "check": "yarn lint:ts && yarn fix && yarn test:unit", 66 | "fix:js": "biome check --write .", 67 | "lint:ts": "tsc --build", 68 | "fix:js:unsafe": "biome check --write . --unsafe", 69 | "lint:js": "biome check .", 70 | "lint": "npm-run-all --parallel 'lint:js'", 71 | "fix": "npm-run-all --serial 'fix:js'", 72 | "prepack": "rm -f tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo && tsc --build tsconfig.build.json", 73 | "test:unit": "vitest run --coverage ./test/unit", 74 | "test:e2e": "vitest run ./test/e2e", 75 | "test": "vitest run --coverage" 76 | }, 77 | "license": "MIT", 78 | "main": "./dist/Transloadit.js", 79 | "exports": { 80 | ".": "./dist/Transloadit.js", 81 | "./package.json": "./package.json" 82 | }, 83 | "files": [ 84 | "dist", 85 | "src" 86 | ], 87 | "bin": "./dist/cli.js" 88 | } 89 | -------------------------------------------------------------------------------- /src/cli/commands/bills.ts: -------------------------------------------------------------------------------- 1 | import { Command, Option } from 'clipanion' 2 | import { z } from 'zod' 3 | import { tryCatch } from '../../alphalib/tryCatch.ts' 4 | import type { Transloadit } from '../../Transloadit.ts' 5 | import { formatAPIError } from '../helpers.ts' 6 | import type { IOutputCtl } from '../OutputCtl.ts' 7 | import { AuthenticatedCommand } from './BaseCommand.ts' 8 | 9 | // --- Types and business logic --- 10 | 11 | export interface BillsGetOptions { 12 | months: string[] 13 | } 14 | 15 | const BillResponseSchema = z.object({ 16 | total: z.number(), 17 | }) 18 | 19 | export async function get( 20 | output: IOutputCtl, 21 | client: Transloadit, 22 | { months }: BillsGetOptions, 23 | ): Promise { 24 | const requests = months.map((month) => client.getBill(month)) 25 | 26 | const [err, results] = await tryCatch(Promise.all(requests)) 27 | if (err) { 28 | output.error(formatAPIError(err)) 29 | return 30 | } 31 | 32 | for (const result of results) { 33 | const parsed = BillResponseSchema.safeParse(result) 34 | if (parsed.success) { 35 | output.print(`$${parsed.data.total}`, result) 36 | } else { 37 | output.print('Unable to parse bill response', result) 38 | } 39 | } 40 | } 41 | 42 | // --- Command class --- 43 | 44 | export class BillsGetCommand extends AuthenticatedCommand { 45 | static override paths = [ 46 | ['bills', 'get'], 47 | ['bill', 'get'], 48 | ['b', 'get'], 49 | ['b', 'g'], 50 | ] 51 | 52 | static override usage = Command.Usage({ 53 | category: 'Bills', 54 | description: 'Fetch billing information', 55 | details: ` 56 | Fetch billing information for the specified months. 57 | Months should be specified in YYYY-MM format. 58 | If no month is specified, returns the current month. 59 | `, 60 | examples: [ 61 | ['Get current month billing', 'transloadit bills get'], 62 | ['Get specific month', 'transloadit bills get 2024-01'], 63 | ['Get multiple months', 'transloadit bills get 2024-01 2024-02'], 64 | ], 65 | }) 66 | 67 | months = Option.Rest() 68 | 69 | protected async run(): Promise { 70 | const monthList: string[] = [] 71 | 72 | for (const month of this.months) { 73 | if (!/^\d{4}-\d{1,2}$/.test(month)) { 74 | this.output.error(`invalid date format '${month}' (YYYY-MM)`) 75 | return 1 76 | } 77 | monthList.push(month) 78 | } 79 | 80 | // Default to current month if none specified 81 | if (monthList.length === 0) { 82 | const d = new Date() 83 | monthList.push(`${d.getUTCFullYear()}-${d.getUTCMonth() + 1}`) 84 | } 85 | 86 | await get(this.output, this.client, { 87 | months: monthList, 88 | }) 89 | return undefined 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/file-read.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 5, 9 | discount_factor: 0.2, 10 | discount_pct: 80, 11 | minimum_charge: 512000, 12 | output_factor: 1, 13 | override_lvl1: 'Document Processing', 14 | purpose_sentence: 'reads file contents from supported file-types', 15 | purpose_verb: 'read', 16 | purpose_word: 'read files', 17 | purpose_words: 'Read file contents', 18 | service_slug: 'document-processing', 19 | slot_count: 5, 20 | title: 'Read file contents', 21 | typical_file_size_mb: 1.2, 22 | typical_file_type: 'file', 23 | name: 'FileReadRobot', 24 | priceFactor: 5, 25 | queueSlotCount: 5, 26 | minimumCharge: 512000, 27 | isAllowedForUrlTransform: true, 28 | isInternal: false, 29 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 30 | stage: 'ga', 31 | } 32 | 33 | export const robotFileReadInstructionsSchema = robotBase 34 | .merge(robotUse) 35 | .extend({ 36 | robot: z.literal('/file/read').describe(` 37 | This Robot accepts any file, and will read the file using UTF-8 encoding. The result is outputted to \`file.meta.content\` to be accessed in later Steps. 38 | 39 | The Robot currently only accepts files under 500KB. 40 | `), 41 | }) 42 | .strict() 43 | 44 | export const robotFileReadInstructionsWithHiddenFieldsSchema = 45 | robotFileReadInstructionsSchema.extend({ 46 | result: z.union([z.literal('debug'), robotFileReadInstructionsSchema.shape.result]).optional(), 47 | }) 48 | 49 | export type RobotFileReadInstructions = z.infer 50 | export type RobotFileReadInstructionsWithHiddenFields = z.infer< 51 | typeof robotFileReadInstructionsWithHiddenFieldsSchema 52 | > 53 | 54 | export const interpolatableRobotFileReadInstructionsSchema = interpolateRobot( 55 | robotFileReadInstructionsSchema, 56 | ) 57 | export type InterpolatableRobotFileReadInstructions = InterpolatableRobotFileReadInstructionsInput 58 | 59 | export type InterpolatableRobotFileReadInstructionsInput = z.input< 60 | typeof interpolatableRobotFileReadInstructionsSchema 61 | > 62 | 63 | export const interpolatableRobotFileReadInstructionsWithHiddenFieldsSchema = interpolateRobot( 64 | robotFileReadInstructionsWithHiddenFieldsSchema, 65 | ) 66 | export type InterpolatableRobotFileReadInstructionsWithHiddenFields = z.infer< 67 | typeof interpolatableRobotFileReadInstructionsWithHiddenFieldsSchema 68 | > 69 | export type InterpolatableRobotFileReadInstructionsWithHiddenFieldsInput = z.input< 70 | typeof interpolatableRobotFileReadInstructionsWithHiddenFieldsSchema 71 | > 72 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | import type { Transloadit } from '../src/Transloadit.ts' 2 | import { RequestError } from '../src/Transloadit.ts' 3 | 4 | export const createProxy = (transloaditInstance: Transloadit) => { 5 | return new Proxy(transloaditInstance, { 6 | get(target, propKey) { 7 | // @ts-expect-error I dunno how to type 8 | const origMethod = target[propKey] 9 | if (typeof origMethod === 'function') { 10 | return (...args: unknown[]) => { 11 | const result = origMethod.apply(target, args) 12 | 13 | if (!(result && 'then' in result)) { 14 | return result 15 | } 16 | 17 | const newPromise = (result as Promise).catch((err: unknown) => { 18 | if (err instanceof Error && 'cause' in err && err.cause instanceof RequestError) { 19 | if (err.cause.request != null) { 20 | // for util.inspect: 21 | Object.defineProperty(err.cause, 'request', { 22 | value: err.cause.request, 23 | enumerable: false, 24 | }) 25 | // for vitest "Serialized Error" 26 | Object.defineProperty(err.cause.request, 'toJSON', { 27 | value: () => undefined, 28 | enumerable: false, 29 | }) 30 | } 31 | if (err.cause.response != null) { 32 | Object.defineProperty(err.cause, 'response', { 33 | value: err.cause.response, 34 | enumerable: false, 35 | }) 36 | Object.defineProperty(err.cause.response, 'toJSON', { 37 | value: () => undefined, 38 | enumerable: false, 39 | }) 40 | } 41 | if (err.cause.options != null) { 42 | Object.defineProperty(err.cause, 'options', { 43 | value: err.cause.options, 44 | enumerable: false, 45 | }) 46 | Object.defineProperty(err.cause.options, 'toJSON', { 47 | value: () => undefined, 48 | enumerable: false, 49 | }) 50 | } 51 | if (err.cause.timings != null) { 52 | Object.defineProperty(err.cause, 'timings', { 53 | value: err.cause.timings, 54 | enumerable: false, 55 | }) 56 | Object.defineProperty(err.cause.timings, 'toJSON', { 57 | value: () => undefined, 58 | enumerable: false, 59 | }) 60 | } 61 | } 62 | throw err 63 | }) 64 | 65 | // pass on the assembly id if present 66 | if (result?.assemblyId != null) { 67 | Object.assign(newPromise, { assemblyId: result.assemblyId }) 68 | } 69 | return newPromise 70 | } 71 | } 72 | 73 | return origMethod 74 | }, 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/document-autorotate.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 1, 9 | discount_factor: 1, 10 | discount_pct: 0, 11 | example_code_description: 12 | 'Auto-rotate individual pages of a documents to the correction orientation:', 13 | minimum_charge: 2097152, 14 | output_factor: 1, 15 | override_lvl1: 'Document Processing', 16 | purpose_sentence: 'corrects the orientation of documents', 17 | purpose_verb: 'auto-rotate', 18 | purpose_word: 'auto-rotate documents', 19 | purpose_words: 'Auto-rotate documents', 20 | service_slug: 'document-processing', 21 | slot_count: 10, 22 | title: 'Auto-rotate documents to the correct orientation', 23 | typical_file_size_mb: 0.8, 24 | typical_file_type: 'document', 25 | name: 'DocumentAutorotateRobot', 26 | priceFactor: 1, 27 | queueSlotCount: 10, 28 | minimumCharge: 2097152, 29 | isAllowedForUrlTransform: true, 30 | trackOutputFileSize: true, 31 | isInternal: false, 32 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 33 | stage: 'ga', 34 | } 35 | 36 | export const robotDocumentAutorotateInstructionsSchema = robotBase 37 | .merge(robotUse) 38 | .extend({ 39 | robot: z.literal('/document/autorotate'), 40 | }) 41 | .strict() 42 | 43 | export const robotDocumentAutorotateInstructionsWithHiddenFieldsSchema = 44 | robotDocumentAutorotateInstructionsSchema.extend({ 45 | result: z 46 | .union([z.literal('debug'), robotDocumentAutorotateInstructionsSchema.shape.result]) 47 | .optional(), 48 | }) 49 | 50 | export type RobotDocumentAutorotateInstructions = z.infer< 51 | typeof robotDocumentAutorotateInstructionsSchema 52 | > 53 | export type RobotDocumentAutorotateInstructionsWithHiddenFields = z.infer< 54 | typeof robotDocumentAutorotateInstructionsWithHiddenFieldsSchema 55 | > 56 | 57 | export const interpolatableRobotDocumentAutorotateInstructionsSchema = interpolateRobot( 58 | robotDocumentAutorotateInstructionsSchema, 59 | ) 60 | export type InterpolatableRobotDocumentAutorotateInstructions = 61 | InterpolatableRobotDocumentAutorotateInstructionsInput 62 | 63 | export type InterpolatableRobotDocumentAutorotateInstructionsInput = z.input< 64 | typeof interpolatableRobotDocumentAutorotateInstructionsSchema 65 | > 66 | 67 | export const interpolatableRobotDocumentAutorotateInstructionsWithHiddenFieldsSchema = 68 | interpolateRobot(robotDocumentAutorotateInstructionsWithHiddenFieldsSchema) 69 | export type InterpolatableRobotDocumentAutorotateInstructionsWithHiddenFields = z.infer< 70 | typeof interpolatableRobotDocumentAutorotateInstructionsWithHiddenFieldsSchema 71 | > 72 | export type InterpolatableRobotDocumentAutorotateInstructionsWithHiddenFieldsInput = z.input< 73 | typeof interpolatableRobotDocumentAutorotateInstructionsWithHiddenFieldsSchema 74 | > 75 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/tlcdn-deliver.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import type { RobotMetaInput } from './_instructions-primitives.ts' 3 | import { interpolateRobot, robotBase } from './_instructions-primitives.ts' 4 | 5 | export const meta: RobotMetaInput = { 6 | allowed_for_url_transform: false, 7 | bytescount: 20, 8 | discount_factor: 0.05, 9 | discount_pct: 95, 10 | minimum_charge: 102400, 11 | output_factor: 1, 12 | override_lvl1: 'Content Delivery', 13 | purpose_sentence: 'caches and delivers files globally', 14 | purpose_verb: 'cache & deliver', 15 | purpose_word: 'Cache and deliver files', 16 | purpose_words: 'Cache and deliver files globally', 17 | service_slug: 'content-delivery', 18 | slot_count: 0, 19 | title: 'Cache and deliver files globally', 20 | typical_file_size_mb: 1.2, 21 | typical_file_type: 'file', 22 | name: 'TlcdnDeliverRobot', 23 | priceFactor: 20, 24 | queueSlotCount: 0, 25 | minimumCharge: 102400, 26 | downloadInputFiles: false, 27 | preserveInputFileUrls: true, 28 | isAllowedForUrlTransform: false, 29 | trackOutputFileSize: false, 30 | isInternal: true, 31 | stage: 'ga', 32 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 33 | } 34 | 35 | export const robotTlcdnDeliverInstructionsSchema = robotBase 36 | .extend({ 37 | robot: z.literal('/tlcdn/deliver').describe(` 38 | When you want Transloadit to tranform files on the fly, this Robot can cache and deliver the results close to your end-user, saving on latency and encoding volume. The use of this Robot is implicit when you use the tlcdn.com domain. 39 | `), 40 | }) 41 | .strict() 42 | 43 | export const robotTlcdnDeliverInstructionsWithHiddenFieldsSchema = 44 | robotTlcdnDeliverInstructionsSchema.extend({ 45 | result: z 46 | .union([z.literal('debug'), robotTlcdnDeliverInstructionsSchema.shape.result]) 47 | .optional(), 48 | }) 49 | 50 | export type RobotTlcdnDeliverInstructions = z.infer 51 | export type RobotTlcdnDeliverInstructionsWithHiddenFields = z.infer< 52 | typeof robotTlcdnDeliverInstructionsWithHiddenFieldsSchema 53 | > 54 | 55 | export const interpolatableRobotTlcdnDeliverInstructionsSchema = interpolateRobot( 56 | robotTlcdnDeliverInstructionsSchema, 57 | ) 58 | export type InterpolatableRobotTlcdnDeliverInstructions = 59 | InterpolatableRobotTlcdnDeliverInstructionsInput 60 | 61 | export type InterpolatableRobotTlcdnDeliverInstructionsInput = z.input< 62 | typeof interpolatableRobotTlcdnDeliverInstructionsSchema 63 | > 64 | 65 | export const interpolatableRobotTlcdnDeliverInstructionsWithHiddenFieldsSchema = interpolateRobot( 66 | robotTlcdnDeliverInstructionsWithHiddenFieldsSchema, 67 | ) 68 | export type InterpolatableRobotTlcdnDeliverInstructionsWithHiddenFields = z.infer< 69 | typeof interpolatableRobotTlcdnDeliverInstructionsWithHiddenFieldsSchema 70 | > 71 | export type InterpolatableRobotTlcdnDeliverInstructionsWithHiddenFieldsInput = z.input< 72 | typeof interpolatableRobotTlcdnDeliverInstructionsWithHiddenFieldsSchema 73 | > 74 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/edgly-deliver.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import type { RobotMetaInput } from './_instructions-primitives.ts' 3 | import { interpolateRobot, robotBase } from './_instructions-primitives.ts' 4 | 5 | export const meta: RobotMetaInput = { 6 | allowed_for_url_transform: false, 7 | bytescount: 20, 8 | discount_factor: 0.05, 9 | discount_pct: 95, 10 | minimum_charge: 102400, 11 | output_factor: 1, 12 | override_lvl1: 'Content Delivery', 13 | purpose_sentence: 'caches and delivers files globally', 14 | purpose_verb: 'cache & deliver', 15 | purpose_word: 'Cache and deliver files', 16 | purpose_words: 'Cache and deliver files globally', 17 | service_slug: 'content-delivery', 18 | slot_count: 0, 19 | title: 'Cache and deliver files globally', 20 | typical_file_size_mb: 1.2, 21 | typical_file_type: 'file', 22 | name: 'EdglyDeliverRobot', 23 | priceFactor: 20, 24 | queueSlotCount: 0, 25 | minimumCharge: 102400, 26 | downloadInputFiles: false, 27 | preserveInputFileUrls: true, 28 | isAllowedForUrlTransform: false, 29 | trackOutputFileSize: false, 30 | isInternal: true, 31 | stage: 'removed', 32 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 33 | } 34 | 35 | export const robotEdglyDeliverInstructionsSchema = robotBase 36 | .extend({ 37 | robot: z.literal('/edgly/deliver').describe(` 38 | When you want Transloadit to tranform files on the fly, this Robot can cache and deliver the results close to your end-user, saving on latency and encoding volume. The use of this Robot is implicit when you use the edgly.net domain. 39 | `), 40 | }) 41 | .strict() 42 | 43 | export const robotEdglyDeliverInstructionsWithHiddenFieldsSchema = 44 | robotEdglyDeliverInstructionsSchema.extend({ 45 | result: z 46 | .union([z.literal('debug'), robotEdglyDeliverInstructionsSchema.shape.result]) 47 | .optional(), 48 | }) 49 | 50 | export type RobotEdglyDeliverInstructions = z.infer 51 | export type RobotEdglyDeliverInstructionsWithHiddenFields = z.infer< 52 | typeof robotEdglyDeliverInstructionsWithHiddenFieldsSchema 53 | > 54 | 55 | export const interpolatableRobotEdglyDeliverInstructionsSchema = interpolateRobot( 56 | robotEdglyDeliverInstructionsSchema, 57 | ) 58 | export type InterpolatableRobotEdglyDeliverInstructions = 59 | InterpolatableRobotEdglyDeliverInstructionsInput 60 | 61 | export type InterpolatableRobotEdglyDeliverInstructionsInput = z.input< 62 | typeof interpolatableRobotEdglyDeliverInstructionsSchema 63 | > 64 | 65 | export const interpolatableRobotEdglyDeliverInstructionsWithHiddenFieldsSchema = interpolateRobot( 66 | robotEdglyDeliverInstructionsWithHiddenFieldsSchema, 67 | ) 68 | export type InterpolatableRobotEdglyDeliverInstructionsWithHiddenFields = z.infer< 69 | typeof interpolatableRobotEdglyDeliverInstructionsWithHiddenFieldsSchema 70 | > 71 | export type InterpolatableRobotEdglyDeliverInstructionsWithHiddenFieldsInput = z.input< 72 | typeof interpolatableRobotEdglyDeliverInstructionsWithHiddenFieldsSchema 73 | > 74 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/document-split.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 1, 9 | discount_factor: 1, 10 | discount_pct: 0, 11 | example_code_description: 'Extract single or multiple pages from a PDF document:', 12 | minimum_charge: 2097152, 13 | output_factor: 1, 14 | override_lvl1: 'Document Processing', 15 | purpose_sentence: 'extracts pages from documents', 16 | purpose_verb: 'extract', 17 | purpose_word: 'extracts pages', 18 | purpose_words: 'Extracts pages', 19 | service_slug: 'document-processing', 20 | slot_count: 10, 21 | title: 'Extract pages from a document', 22 | typical_file_size_mb: 0.8, 23 | typical_file_type: 'document', 24 | name: 'DocumentSplitRobot', 25 | priceFactor: 1, 26 | queueSlotCount: 10, 27 | minimumCharge: 1048576, 28 | isAllowedForUrlTransform: true, 29 | trackOutputFileSize: true, 30 | isInternal: false, 31 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 32 | stage: 'ga', 33 | } 34 | 35 | export const robotDocumentSplitInstructionsSchema = robotBase 36 | .merge(robotUse) 37 | .extend({ 38 | robot: z.literal('/document/split'), 39 | pages: z 40 | .union([z.string(), z.array(z.string())]) 41 | .describe( 42 | 'The pages to select from the input PDF and to be included in the output PDF. Each entry can be a single page number (e.g. 5), or a range (e.g. `5-10`). Page numbers start at 1. By default all pages are extracted.', 43 | ) 44 | .optional(), 45 | }) 46 | .strict() 47 | 48 | export const robotDocumentSplitInstructionsWithHiddenFieldsSchema = 49 | robotDocumentSplitInstructionsSchema.extend({ 50 | result: z 51 | .union([z.literal('debug'), robotDocumentSplitInstructionsSchema.shape.result]) 52 | .optional(), 53 | }) 54 | 55 | export type RobotDocumentSplitInstructions = z.infer 56 | export type RobotDocumentSplitInstructionsWithHiddenFields = z.infer< 57 | typeof robotDocumentSplitInstructionsWithHiddenFieldsSchema 58 | > 59 | 60 | export const interpolatableRobotDocumentSplitInstructionsSchema = interpolateRobot( 61 | robotDocumentSplitInstructionsSchema, 62 | ) 63 | export type InterpolatableRobotDocumentSplitInstructions = 64 | InterpolatableRobotDocumentSplitInstructionsInput 65 | 66 | export type InterpolatableRobotDocumentSplitInstructionsInput = z.input< 67 | typeof interpolatableRobotDocumentSplitInstructionsSchema 68 | > 69 | 70 | export const interpolatableRobotDocumentSplitInstructionsWithHiddenFieldsSchema = interpolateRobot( 71 | robotDocumentSplitInstructionsWithHiddenFieldsSchema, 72 | ) 73 | export type InterpolatableRobotDocumentSplitInstructionsWithHiddenFields = z.infer< 74 | typeof interpolatableRobotDocumentSplitInstructionsWithHiddenFieldsSchema 75 | > 76 | export type InterpolatableRobotDocumentSplitInstructionsWithHiddenFieldsInput = z.input< 77 | typeof interpolatableRobotDocumentSplitInstructionsWithHiddenFieldsSchema 78 | > 79 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/file-hash.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: false, 8 | bytescount: 5, 9 | discount_factor: 0.2, 10 | discount_pct: 80, 11 | example_code: { 12 | steps: { 13 | hashed: { 14 | robot: '/file/hash', 15 | use: ':original', 16 | algorithm: 'sha1', 17 | }, 18 | }, 19 | }, 20 | example_code_description: 'Hash each uploaded file using the SHA-1 algorithm:', 21 | minimum_charge: 0, 22 | output_factor: 1, 23 | override_lvl1: 'Media Cataloging', 24 | purpose_sentence: 'hashes files in Assemblies', 25 | purpose_verb: 'hash', 26 | purpose_word: 'file', 27 | purpose_words: 'Hash files', 28 | service_slug: 'media-cataloging', 29 | slot_count: 60, 30 | title: 'Hash Files', 31 | typical_file_size_mb: 1.2, 32 | typical_file_type: 'file', 33 | name: 'FileHashRobot', 34 | priceFactor: 5, 35 | queueSlotCount: 60, 36 | isAllowedForUrlTransform: false, 37 | isInternal: false, 38 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 39 | stage: 'ga', 40 | } 41 | 42 | export const robotFileHashInstructionsSchema = robotBase 43 | .merge(robotUse) 44 | .extend({ 45 | robot: z.literal('/file/hash').describe(` 46 | This Robot allows you to hash any file as part of the Assembly execution process. This can be useful for verifying the integrity of a file for example. 47 | `), 48 | algorithm: z 49 | .enum(['b2', 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']) 50 | .default('sha256') 51 | .describe(` 52 | The hashing algorithm to use. 53 | 54 | The file hash is exported as \`file.meta.hash\`. 55 | `), 56 | }) 57 | .strict() 58 | 59 | export const robotFileHashInstructionsWithHiddenFieldsSchema = 60 | robotFileHashInstructionsSchema.extend({ 61 | result: z.union([z.literal('debug'), robotFileHashInstructionsSchema.shape.result]).optional(), 62 | }) 63 | 64 | export type RobotFileHashInstructions = z.infer 65 | export type RobotFileHashInstructionsWithHiddenFields = z.infer< 66 | typeof robotFileHashInstructionsWithHiddenFieldsSchema 67 | > 68 | 69 | export const interpolatableRobotFileHashInstructionsSchema = interpolateRobot( 70 | robotFileHashInstructionsSchema, 71 | ) 72 | export type InterpolatableRobotFileHashInstructions = InterpolatableRobotFileHashInstructionsInput 73 | 74 | export type InterpolatableRobotFileHashInstructionsInput = z.input< 75 | typeof interpolatableRobotFileHashInstructionsSchema 76 | > 77 | 78 | export const interpolatableRobotFileHashInstructionsWithHiddenFieldsSchema = interpolateRobot( 79 | robotFileHashInstructionsWithHiddenFieldsSchema, 80 | ) 81 | export type InterpolatableRobotFileHashInstructionsWithHiddenFields = z.infer< 82 | typeof interpolatableRobotFileHashInstructionsWithHiddenFieldsSchema 83 | > 84 | export type InterpolatableRobotFileHashInstructionsWithHiddenFieldsInput = z.input< 85 | typeof interpolatableRobotFileHashInstructionsWithHiddenFieldsSchema 86 | > 87 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | check-permissions: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | has-write-access: ${{ steps.check.outputs.has-write-access }} 18 | steps: 19 | - name: Check user permissions 20 | id: check 21 | uses: actions/github-script@v7 22 | with: 23 | script: | 24 | // Get the username of the person who triggered the event 25 | let username; 26 | if (context.eventName === 'issue_comment' || context.eventName === 'pull_request_review_comment') { 27 | username = context.payload.comment.user.login; 28 | } else if (context.eventName === 'pull_request_review') { 29 | username = context.payload.review.user.login; 30 | } else if (context.eventName === 'issues') { 31 | username = context.payload.issue.user.login; 32 | } 33 | 34 | // Check if user has write permissions 35 | try { 36 | const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ 37 | owner: context.repo.owner, 38 | repo: context.repo.repo, 39 | username: username 40 | }); 41 | 42 | const hasWriteAccess = ['admin', 'maintain', 'write'].includes(permission.permission); 43 | console.log(`User ${username} has permission: ${permission.permission}`); 44 | core.setOutput('has-write-access', hasWriteAccess.toString()); 45 | 46 | if (!hasWriteAccess) { 47 | console.log(`User ${username} does not have write access. Claude bot will not be triggered.`); 48 | } 49 | } catch (error) { 50 | console.log(`Error checking permissions for ${username}: ${error.message}`); 51 | core.setOutput('has-write-access', 'false'); 52 | } 53 | 54 | claude: 55 | needs: check-permissions 56 | if: | 57 | needs.check-permissions.outputs.has-write-access == 'true' && 58 | ( 59 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 60 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 61 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 62 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 63 | ) 64 | runs-on: ubuntu-latest 65 | permissions: 66 | contents: read 67 | pull-requests: read 68 | issues: read 69 | id-token: write 70 | steps: 71 | - name: Checkout repository 72 | uses: actions/checkout@v4 73 | with: 74 | fetch-depth: 1 75 | 76 | - name: Run Claude Code 77 | id: claude 78 | uses: anthropics/claude-code-action@beta 79 | with: 80 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 81 | claude_args: | 82 | --model claude-opus-4-5-20251101 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd be happy to accept pull requests. If you plan on working on something big, please first drop us a line! 4 | 5 | ## Getting started 6 | 7 | To get started, first fork the project on GitHub and clone the project using Git. 8 | 9 | ## Dependencies management 10 | 11 | This project uses [Yarn](https://yarnpkg.com) 4 for dependency management. To install dependencies, run the following command from the project root: 12 | 13 | ```sh 14 | yarn install 15 | ``` 16 | 17 | To check for updates, run: 18 | 19 | ```sh 20 | yarn upgrade-interactive 21 | ``` 22 | 23 | ## Linting 24 | 25 | This project is linted using Biome. You can lint the project by running: 26 | 27 | ```sh 28 | yarn lint:js 29 | ``` 30 | 31 | ## Formatting 32 | 33 | This project is formatted using Biome. You can format the project: 34 | 35 | ```sh 36 | yarn fix:js 37 | ``` 38 | 39 | ## Testing 40 | 41 | This project is tested using [Vitest](https://vitest.dev). There are two kinds of tests. 42 | 43 | ### Unit tests 44 | 45 | Unit tests are in the [`test/unit`](test/unit) folder of the project. You can run them using the following command: 46 | 47 | ```sh 48 | yarn test:unit 49 | ``` 50 | 51 | This will also generate a coverage report in the `coverage` directory. 52 | 53 | ### e2e tests 54 | 55 | e2e tests are in the [`test/e2e`](test/e2e) folder. They require some extra setup. 56 | 57 | Firstly, these tests require the Cloudflare executable. You can download this with: 58 | 59 | ```sh 60 | curl -fsSLo cloudflared-linux-amd64 https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 61 | chmod +x cloudflared-linux-amd64 62 | ``` 63 | 64 | They also require a Transloadit key and secret, which you can get from https://transloadit.com/c/credentials. 65 | 66 | You can run the e2e tests with: 67 | 68 | ```sh 69 | TRANSLOADIT_KEY='YOUR_TRANSLOADIT_KEY' TRANSLOADIT_SECRET='YOUR_TRANSLOADIT_SECRET' CLOUDFLARED_PATH='./cloudflared-linux-amd64' yarn test:e2e 70 | ``` 71 | 72 | ### Code Coverage 73 | 74 | Coverage reports are: 75 | 76 | - Generated locally in the `coverage` directory 77 | - Uploaded to Codecov for tracking 78 | - Enforced in CI (builds will fail if coverage drops below thresholds) 79 | 80 | View the coverage report locally by opening `coverage/index.html` in your browser. 81 | 82 | ## Releasing 83 | 84 | Only maintainers can make releases. Releases to [npm](https://www.npmjs.com) are automated using GitHub actions. To make a release, perform the following steps: 85 | 86 | 1. Update `CHANGELOG.md` with a new entry describing the release. And `git add CHANGELOG.md && git commit -m "Update CHANGELOG.md"`. 87 | 2. Update the version using npm. This will update the version in the `package.json` file and create a git tag. E.g.: 88 | 89 | - `npm version patch` 90 | - OR, for pre-releases: `npm version prerelease` 91 | 92 | 3. Push the tag to GitHub: `git push origin main --tags` 93 | 4. If the tests pass, GitHub actions will now publish the new version to npm. 94 | 5. When successful add [release notes](https://github.com/transloadit/node-sdk/releases). 95 | 6. If this was a pre-release, remember to run this to reset the [npm `latest` tag](https://www.npmjs.com/package/transloadit?activeTab=versions) to the previous version (replace `x.y.z` with previous version): 96 | 97 | - `npm dist-tag add transloadit@X.Y.Z latest` 98 | -------------------------------------------------------------------------------- /examples/credentials.ts: -------------------------------------------------------------------------------- 1 | // Run this file as: 2 | // 3 | // env TRANSLOADIT_KEY=xxx TRANSLOADIT_SECRET=yyy yarn tsx examples/credentials.ts 4 | // 5 | // You may need to build the project first using: 6 | // 7 | // yarn prepack 8 | // 9 | import type { CreateTemplateCredentialParams } from 'transloadit' 10 | import { Transloadit } from 'transloadit' 11 | 12 | const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET } = process.env 13 | if (TRANSLOADIT_KEY == null || TRANSLOADIT_SECRET == null) { 14 | throw new Error('Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET') 15 | } 16 | const transloadit = new Transloadit({ 17 | authKey: TRANSLOADIT_KEY, 18 | authSecret: TRANSLOADIT_SECRET, 19 | }) 20 | 21 | const firstName = 'myProductionS3' 22 | const secondName = 'myStagingS3' 23 | 24 | const credentialParams: CreateTemplateCredentialParams = { 25 | name: firstName, 26 | type: 's3', 27 | content: { 28 | key: 'xyxy', 29 | secret: 'xyxyxyxy', 30 | bucket: 'mybucket.example.com', 31 | bucket_region: 'us-east-1', 32 | }, 33 | } 34 | 35 | console.log('==> listTemplateCredentials') 36 | const { credentials } = await transloadit.listTemplateCredentials({ 37 | sort: 'created', 38 | order: 'asc', 39 | }) 40 | console.log('Successfully fetched', credentials.length, 'credential(s)') 41 | 42 | // ^-- with Templates, there is `items` and `count`. 43 | // with Credentials, there is `ok`, `message`, `credentials` 44 | 45 | for (const credential of credentials) { 46 | if ([firstName, secondName].includes(credential.name)) { 47 | console.log(`==> deleteTemplateCredential: ${credential.id} (${credential.name})`) 48 | const delResult = await transloadit.deleteTemplateCredential(credential.id) 49 | console.log('Successfully deleted credential', delResult) 50 | // ^-- identical structure between `Templates` and `Credentials` 51 | } 52 | } 53 | 54 | console.log('==> createTemplateCredential') 55 | const createTemplateCredentialResult = await transloadit.createTemplateCredential(credentialParams) 56 | console.log('TemplateCredential created successfully:', createTemplateCredentialResult) 57 | // ^-- with Templates, there is `ok`, `message`, `id`, `content`, `name`, `require_signature_auth`. Same is true for: created, updated, fetched 58 | // with Credentials, there is `ok`, `message`, `credentials` <-- and a single object nested directly under it, which is unexpected with that plural imho. Same is true for created, updated, fetched 59 | 60 | console.log( 61 | `==> editTemplateCredential: ${createTemplateCredentialResult.credential.id} (${createTemplateCredentialResult.credential.name})`, 62 | ) 63 | const editResult = await transloadit.editTemplateCredential( 64 | createTemplateCredentialResult.credential.id, 65 | { 66 | ...credentialParams, 67 | name: secondName, 68 | }, 69 | ) 70 | console.log('Successfully edited credential', editResult) 71 | // ^-- see create 72 | 73 | console.log( 74 | `==> getTemplateCredential: ${createTemplateCredentialResult.credential.id} (${createTemplateCredentialResult.credential.name})`, 75 | ) 76 | const getTemplateCredentialResult = await transloadit.getTemplateCredential( 77 | createTemplateCredentialResult.credential.id, 78 | ) 79 | console.log('Successfully fetched credential', getTemplateCredentialResult) 80 | // ^-- not working at al, getting a 404. looking at the API, this is not implemented yet 81 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", 3 | "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, 4 | "files": { 5 | "ignoreUnknown": false, 6 | "includes": [ 7 | "**", 8 | "!package.json", 9 | "!coverage", 10 | "!dist", 11 | "!fixture", 12 | "!.vscode", 13 | "!src/alphalib" 14 | ] 15 | }, 16 | "formatter": { 17 | "enabled": true, 18 | "formatWithErrors": false, 19 | "indentStyle": "space", 20 | "indentWidth": 2, 21 | "lineEnding": "lf", 22 | "lineWidth": 100, 23 | "attributePosition": "auto", 24 | "bracketSameLine": false, 25 | "bracketSpacing": true, 26 | "expand": "auto", 27 | "useEditorconfig": true, 28 | "includes": ["**", "!**/lib/", "!**/node_modules/"] 29 | }, 30 | "linter": { 31 | "enabled": true, 32 | "rules": { 33 | "recommended": true, 34 | "suspicious": { 35 | "noExplicitAny": "error", 36 | "noImplicitAnyLet": "error", 37 | "noConfusingVoidType": "error", 38 | "noAssignInExpressions": "error", 39 | "noArrayIndexKey": "error", 40 | "noShadowRestrictedNames": "error", 41 | "noExportsInTest": "error", 42 | "noDuplicateTestHooks": "error", 43 | "useIterableCallbackReturn": "error", 44 | "noTemplateCurlyInString": "off", 45 | "useAwait": "error" 46 | }, 47 | "correctness": { 48 | "noInvalidUseBeforeDeclaration": "error", 49 | "noVoidTypeReturn": "error" 50 | }, 51 | "complexity": { 52 | "useLiteralKeys": "error", 53 | "noForEach": "error" 54 | }, 55 | "style": { 56 | "noNonNullAssertion": "error", 57 | "noNamespace": "error", 58 | "noParameterAssign": "error", 59 | "noUnusedTemplateLiteral": "error", 60 | "useAsConstAssertion": "error", 61 | "useDefaultParameterLast": "error", 62 | "useEnumInitializers": "error", 63 | "useSelfClosingElements": "error", 64 | "useSingleVarDeclarator": "error", 65 | "useNumberNamespace": "error", 66 | "noInferrableTypes": "error", 67 | "noUselessElse": "error", 68 | "useImportType": { 69 | "level": "error", 70 | "options": { 71 | "style": "separatedType" 72 | } 73 | }, 74 | "useNodejsImportProtocol": "error" 75 | } 76 | } 77 | }, 78 | "javascript": { 79 | "formatter": { 80 | "jsxQuoteStyle": "double", 81 | "quoteProperties": "asNeeded", 82 | "trailingCommas": "all", 83 | "semicolons": "asNeeded", 84 | "arrowParentheses": "always", 85 | "bracketSameLine": false, 86 | "quoteStyle": "single", 87 | "attributePosition": "auto", 88 | "bracketSpacing": true 89 | } 90 | }, 91 | "assist": { 92 | "enabled": true, 93 | "actions": { "source": { "organizeImports": "on" } } 94 | }, 95 | "overrides": [ 96 | { 97 | "includes": ["*.html"], 98 | "javascript": { "formatter": { "quoteStyle": "double" } } 99 | }, 100 | { 101 | "includes": ["*.scss", "*.css"], 102 | "javascript": { "formatter": { "quoteStyle": "double" } }, 103 | "formatter": { "lineWidth": 80 } 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/sftp-import.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotImport, sftpBase } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 10, 9 | discount_factor: 0.1, 10 | discount_pct: 90, 11 | example_code: { 12 | steps: { 13 | imported: { 14 | robot: '/sftp/import', 15 | credentials: 'YOUR_SFTP_CREDENTIALS', 16 | path: 'path/to/files/', 17 | }, 18 | }, 19 | }, 20 | example_code_description: 21 | 'Import files from the `path/to/files` directory and its subdirectories:', 22 | minimum_charge: 0, 23 | output_factor: 1, 24 | override_lvl1: 'File Importing', 25 | purpose_sentence: 26 | 'imports whole libraries of files from your SFTP servers into Transloadit. This Robot relies on public key authentication', 27 | purpose_verb: 'import', 28 | purpose_word: 'SFTP servers', 29 | purpose_words: 'Import files from SFTP servers', 30 | service_slug: 'file-importing', 31 | slot_count: 20, 32 | title: 'Import files from SFTP servers', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'SftpImportRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 20, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | isInternal: false, 41 | removeJobResultFilesFromDiskRightAfterStoringOnS3: true, 42 | stage: 'ga', 43 | } 44 | 45 | export const robotSftpImportInstructionsSchema = robotBase 46 | .merge(robotImport) 47 | .merge(sftpBase) 48 | .extend({ 49 | robot: z.literal('/sftp/import'), 50 | path: z.string().describe(` 51 | The path on your SFTP server where to search for files. 52 | `), 53 | }) 54 | .strict() 55 | 56 | export const robotSftpImportInstructionsWithHiddenFieldsSchema = 57 | robotSftpImportInstructionsSchema.extend({ 58 | result: z 59 | .union([z.literal('debug'), robotSftpImportInstructionsSchema.shape.result]) 60 | .optional(), 61 | allowNetwork: z 62 | .string() 63 | .optional() 64 | .describe(` 65 | Network access permission for the SFTP connection. This is used to control which networks the SFTP robot can access. 66 | `), 67 | }) 68 | 69 | export type RobotSftpImportInstructions = z.infer 70 | export type RobotSftpImportInstructionsWithHiddenFields = z.infer< 71 | typeof robotSftpImportInstructionsWithHiddenFieldsSchema 72 | > 73 | 74 | export const interpolatableRobotSftpImportInstructionsSchema = interpolateRobot( 75 | robotSftpImportInstructionsSchema, 76 | ) 77 | export type InterpolatableRobotSftpImportInstructions = 78 | InterpolatableRobotSftpImportInstructionsInput 79 | 80 | export type InterpolatableRobotSftpImportInstructionsInput = z.input< 81 | typeof interpolatableRobotSftpImportInstructionsSchema 82 | > 83 | 84 | export const interpolatableRobotSftpImportInstructionsWithHiddenFieldsSchema = interpolateRobot( 85 | robotSftpImportInstructionsWithHiddenFieldsSchema, 86 | ) 87 | export type InterpolatableRobotSftpImportInstructionsWithHiddenFields = z.infer< 88 | typeof interpolatableRobotSftpImportInstructionsWithHiddenFieldsSchema 89 | > 90 | export type InterpolatableRobotSftpImportInstructionsWithHiddenFieldsInput = z.input< 91 | typeof interpolatableRobotSftpImportInstructionsWithHiddenFieldsSchema 92 | > 93 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/ftp-import.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { 5 | ftpBase, 6 | interpolateRobot, 7 | path, 8 | robotBase, 9 | robotImport, 10 | } from './_instructions-primitives.ts' 11 | 12 | export const meta: RobotMetaInput = { 13 | allowed_for_url_transform: true, 14 | bytescount: 10, 15 | discount_factor: 0.1, 16 | discount_pct: 90, 17 | example_code: { 18 | steps: { 19 | imported: { 20 | robot: '/ftp/import', 21 | credentials: 'YOUR_FTP_CREDENTIALS', 22 | path: 'path/to/files/', 23 | }, 24 | }, 25 | }, 26 | example_code_description: 27 | 'Import files from the `path/to/files` directory and its subdirectories:', 28 | minimum_charge: 0, 29 | output_factor: 1, 30 | override_lvl1: 'File Importing', 31 | purpose_sentence: 32 | 'imports whole libraries of files from your FTP servers into Transloadit. This Robot relies on password access. For more security, consider our /sftp/import Robot', 33 | purpose_verb: 'import', 34 | purpose_word: 'FTP servers', 35 | purpose_words: 'Import files from FTP servers', 36 | service_slug: 'file-importing', 37 | slot_count: 20, 38 | title: 'Import files from FTP servers', 39 | typical_file_size_mb: 1.2, 40 | typical_file_type: 'file', 41 | name: 'FtpImportRobot', 42 | priceFactor: 6.6666, 43 | queueSlotCount: 20, 44 | isAllowedForUrlTransform: true, 45 | trackOutputFileSize: false, 46 | isInternal: false, 47 | removeJobResultFilesFromDiskRightAfterStoringOnS3: true, 48 | stage: 'ga', 49 | } 50 | 51 | export const robotFtpImportInstructionsSchema = robotBase 52 | .merge(robotImport) 53 | .merge(ftpBase) 54 | .extend({ 55 | robot: z.literal('/ftp/import'), 56 | path: path.describe(` 57 | The path on your FTP server where to search for files. Files are imported recursively from all sub-directories and sub-sub-directories (and so on) from this path. 58 | `), 59 | passive_mode: z 60 | .boolean() 61 | .default(true) 62 | .describe(` 63 | Determines if passive mode should be used for the FTP connection. 64 | `), 65 | }) 66 | .strict() 67 | 68 | export const robotFtpImportInstructionsWithHiddenFieldsSchema = 69 | robotFtpImportInstructionsSchema.extend({ 70 | result: z.union([z.literal('debug'), robotFtpImportInstructionsSchema.shape.result]).optional(), 71 | }) 72 | 73 | export type RobotFtpImportInstructions = z.infer 74 | export type RobotFtpImportInstructionsWithHiddenFields = z.infer< 75 | typeof robotFtpImportInstructionsWithHiddenFieldsSchema 76 | > 77 | 78 | export const interpolatableRobotFtpImportInstructionsSchema = interpolateRobot( 79 | robotFtpImportInstructionsSchema, 80 | ) 81 | export type InterpolatableRobotFtpImportInstructions = InterpolatableRobotFtpImportInstructionsInput 82 | 83 | export type InterpolatableRobotFtpImportInstructionsInput = z.input< 84 | typeof interpolatableRobotFtpImportInstructionsSchema 85 | > 86 | 87 | export const interpolatableRobotFtpImportInstructionsWithHiddenFieldsSchema = interpolateRobot( 88 | robotFtpImportInstructionsWithHiddenFieldsSchema, 89 | ) 90 | export type InterpolatableRobotFtpImportInstructionsWithHiddenFields = z.infer< 91 | typeof interpolatableRobotFtpImportInstructionsWithHiddenFieldsSchema 92 | > 93 | export type InterpolatableRobotFtpImportInstructionsWithHiddenFieldsInput = z.input< 94 | typeof interpolatableRobotFtpImportInstructionsWithHiddenFieldsSchema 95 | > 96 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/meta-write.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { stackVersions } from '../stackVersions.ts' 4 | import type { RobotMetaInput } from './_instructions-primitives.ts' 5 | import { interpolateRobot, robotBase, robotFFmpeg, robotUse } from './_instructions-primitives.ts' 6 | 7 | export const meta: RobotMetaInput = { 8 | allowed_for_url_transform: true, 9 | bytescount: 1, 10 | discount_factor: 1, 11 | discount_pct: 0, 12 | example_code: { 13 | steps: { 14 | attributed: { 15 | robot: '/meta/write', 16 | use: ':original', 17 | data_to_write: { 18 | copyright: '© Transloadit', 19 | }, 20 | ffmpeg_stack: stackVersions.ffmpeg.recommendedVersion, 21 | }, 22 | }, 23 | }, 24 | example_code_description: 'Add a copyright notice to uploaded images:', 25 | minimum_charge: 0, 26 | output_factor: 1, 27 | override_lvl1: 'Media Cataloging', 28 | purpose_sentence: 'writes metadata into files', 29 | purpose_verb: 'write', 30 | purpose_word: 'write metadata', 31 | purpose_words: 'Write metadata to media', 32 | service_slug: 'media-cataloging', 33 | slot_count: 10, 34 | title: 'Write metadata to media', 35 | typical_file_size_mb: 1.2, 36 | typical_file_type: 'file', 37 | uses_tools: ['ffmpeg'], 38 | name: 'MetaWriteRobot', 39 | priceFactor: 1, 40 | queueSlotCount: 10, 41 | isAllowedForUrlTransform: true, 42 | trackOutputFileSize: true, 43 | isInternal: false, 44 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 45 | stage: 'ga', 46 | } 47 | 48 | export const robotMetaWriteInstructionsSchema = robotBase 49 | .merge(robotUse) 50 | .merge(robotFFmpeg) 51 | .extend({ 52 | robot: z.literal('/meta/write').describe(` 53 | **Note:** This Robot currently accepts images, videos and audio files. 54 | `), 55 | data_to_write: z 56 | .object({}) 57 | .passthrough() 58 | .default({}) 59 | .describe(` 60 | A key/value map defining the metadata to write into the file. 61 | 62 | Valid metadata keys can be found [here](https://exiftool.org/TagNames/EXIF.html). For example: \`ProcessingSoftware\`. 63 | `), 64 | }) 65 | .strict() 66 | 67 | export const robotMetaWriteInstructionsWithHiddenFieldsSchema = 68 | robotMetaWriteInstructionsSchema.extend({ 69 | result: z.union([z.literal('debug'), robotMetaWriteInstructionsSchema.shape.result]).optional(), 70 | }) 71 | 72 | export type RobotMetaWriteInstructions = z.infer 73 | export type RobotMetaWriteInstructionsWithHiddenFields = z.infer< 74 | typeof robotMetaWriteInstructionsWithHiddenFieldsSchema 75 | > 76 | 77 | export const interpolatableRobotMetaWriteInstructionsSchema = interpolateRobot( 78 | robotMetaWriteInstructionsSchema, 79 | ) 80 | export type InterpolatableRobotMetaWriteInstructions = InterpolatableRobotMetaWriteInstructionsInput 81 | 82 | export type InterpolatableRobotMetaWriteInstructionsInput = z.input< 83 | typeof interpolatableRobotMetaWriteInstructionsSchema 84 | > 85 | 86 | export const interpolatableRobotMetaWriteInstructionsWithHiddenFieldsSchema = interpolateRobot( 87 | robotMetaWriteInstructionsWithHiddenFieldsSchema, 88 | ) 89 | export type InterpolatableRobotMetaWriteInstructionsWithHiddenFields = z.infer< 90 | typeof interpolatableRobotMetaWriteInstructionsWithHiddenFieldsSchema 91 | > 92 | export type InterpolatableRobotMetaWriteInstructionsWithHiddenFieldsInput = z.input< 93 | typeof interpolatableRobotMetaWriteInstructionsWithHiddenFieldsSchema 94 | > 95 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/image-bgremove.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | discount_factor: 1, 9 | bytescount: 1, 10 | discount_pct: 0, 11 | example_code: { 12 | steps: { 13 | remove_background: { 14 | robot: '/image/bgremove', 15 | use: ':original', 16 | }, 17 | }, 18 | }, 19 | example_code_description: 'Remove the background from the uploaded image:', 20 | minimum_charge: 0, 21 | output_factor: 0.6, 22 | override_lvl1: 'Image Manipulation', 23 | purpose_sentence: 'removes the background from images', 24 | purpose_verb: 'remove', 25 | purpose_word: 'remove', 26 | purpose_words: 'Remove the background from images', 27 | service_slug: 'image-manipulation', 28 | slot_count: 10, 29 | title: 'Remove the background from images', 30 | typical_file_size_mb: 0.8, 31 | typical_file_type: 'image', 32 | name: 'ImageBgremoveRobot', 33 | priceFactor: 1, 34 | queueSlotCount: 10, 35 | minimumChargeUsd: 0.006, 36 | isAllowedForUrlTransform: true, 37 | trackOutputFileSize: true, 38 | isInternal: false, 39 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 40 | stage: 'ga', 41 | } 42 | 43 | export const robotImageBgremoveInstructionsSchema = robotBase 44 | .merge(robotUse) 45 | .extend({ 46 | robot: z.literal('/image/bgremove'), 47 | select: z 48 | .enum(['foreground', 'background']) 49 | .optional() 50 | .describe('Region to select and keep in the image. The other region is removed.'), 51 | format: z.enum(['png', 'gif', 'webp']).optional().describe('Format of the generated image.'), 52 | provider: z 53 | .enum(['transloadit', 'replicate', 'fal']) 54 | .optional() 55 | .describe('Provider to use for removing the background.'), 56 | model: z 57 | .string() 58 | .optional() 59 | .describe( 60 | 'Provider-specific model to use for removing the background. Mostly intended for testing and evaluation.', 61 | ), 62 | }) 63 | .strict() 64 | 65 | export const robotImageBgremoveInstructionsWithHiddenFieldsSchema = 66 | robotImageBgremoveInstructionsSchema.extend({ 67 | result: z 68 | .union([z.literal('debug'), robotImageBgremoveInstructionsSchema.shape.result]) 69 | .optional(), 70 | }) 71 | 72 | export type RobotImageBgremoveInstructions = z.infer 73 | export type RobotImageBgremoveInstructionsWithHiddenFields = z.infer< 74 | typeof robotImageBgremoveInstructionsWithHiddenFieldsSchema 75 | > 76 | 77 | export const interpolatableRobotImageBgremoveInstructionsSchema = interpolateRobot( 78 | robotImageBgremoveInstructionsSchema, 79 | ) 80 | export type InterpolatableRobotImageBgremoveInstructions = 81 | InterpolatableRobotImageBgremoveInstructionsInput 82 | 83 | export type InterpolatableRobotImageBgremoveInstructionsInput = z.input< 84 | typeof interpolatableRobotImageBgremoveInstructionsSchema 85 | > 86 | 87 | export const interpolatableRobotImageBgremoveInstructionsWithHiddenFieldsSchema = interpolateRobot( 88 | robotImageBgremoveInstructionsWithHiddenFieldsSchema, 89 | ) 90 | export type InterpolatableRobotImageBgremoveInstructionsWithHiddenFields = z.infer< 91 | typeof interpolatableRobotImageBgremoveInstructionsWithHiddenFieldsSchema 92 | > 93 | export type InterpolatableRobotImageBgremoveInstructionsWithHiddenFieldsInput = z.input< 94 | typeof interpolatableRobotImageBgremoveInstructionsWithHiddenFieldsSchema 95 | > 96 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/upload-handle.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: false, 8 | bytescount: 10, 9 | discount_factor: 0.1, 10 | discount_pct: 90, 11 | example_code: { 12 | steps: { 13 | ':original': { 14 | robot: '/upload/handle', 15 | }, 16 | exported: { 17 | robot: '/s3/store', 18 | use: ':original', 19 | credentials: 'YOUR_S3_CREDENTIALS', 20 | }, 21 | }, 22 | }, 23 | example_code_description: 'Handle uploads and export the uploaded files to S3:', 24 | minimum_charge: 0, 25 | output_factor: 1, 26 | override_lvl1: 'Handling Uploads', 27 | purpose_sentence: 28 | 'receives uploads that your users throw at you from browser or apps, or that you throw at us programmatically', 29 | purpose_verb: 'handle', 30 | purpose_word: 'handle uploads', 31 | purpose_words: 'Handle uploads', 32 | service_slug: 'handling-uploads', 33 | slot_count: 0, 34 | title: 'Handle uploads', 35 | typical_file_size_mb: 1.2, 36 | typical_file_type: 'file', 37 | name: 'UploadHandleRobot', 38 | priceFactor: 10, 39 | queueSlotCount: 0, 40 | downloadInputFiles: false, 41 | preserveInputFileUrls: true, 42 | isAllowedForUrlTransform: false, 43 | trackOutputFileSize: false, 44 | isInternal: false, 45 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 46 | stage: 'ga', 47 | } 48 | 49 | export const robotUploadHandleInstructionsSchema = robotBase 50 | .extend({ 51 | robot: z.literal('/upload/handle').describe(` 52 | Transloadit handles file uploads by default, so specifying this Robot is optional. 53 | 54 | It can still be a good idea to define this Robot, though. It makes your Assembly Instructions explicit, and allows you to configure exactly how uploads should be handled. For example, you can extract specific metadata from the uploaded files. 55 | 56 | There are **3 important constraints** when using this Robot: 57 | 58 | 1. Don’t define a \`use\` parameter, unlike with other Robots. 59 | 2. Use it only once in a single set of Assembly Instructions. 60 | 3. Name the Step as \`:original\`. 61 | `), 62 | }) 63 | .strict() 64 | 65 | export const robotUploadHandleInstructionsWithHiddenFieldsSchema = 66 | robotUploadHandleInstructionsSchema.extend({ 67 | result: z 68 | .union([z.literal('debug'), robotUploadHandleInstructionsSchema.shape.result]) 69 | .optional(), 70 | }) 71 | 72 | export type RobotUploadHandleInstructions = z.infer 73 | export type RobotUploadHandleInstructionsWithHiddenFields = z.infer< 74 | typeof robotUploadHandleInstructionsWithHiddenFieldsSchema 75 | > 76 | 77 | export const interpolatableRobotUploadHandleInstructionsSchema = interpolateRobot( 78 | robotUploadHandleInstructionsSchema, 79 | ) 80 | export type InterpolatableRobotUploadHandleInstructions = 81 | InterpolatableRobotUploadHandleInstructionsInput 82 | 83 | export type InterpolatableRobotUploadHandleInstructionsInput = z.input< 84 | typeof interpolatableRobotUploadHandleInstructionsSchema 85 | > 86 | 87 | export const interpolatableRobotUploadHandleInstructionsWithHiddenFieldsSchema = interpolateRobot( 88 | robotUploadHandleInstructionsWithHiddenFieldsSchema, 89 | ) 90 | export type InterpolatableRobotUploadHandleInstructionsWithHiddenFields = z.infer< 91 | typeof interpolatableRobotUploadHandleInstructionsWithHiddenFieldsSchema 92 | > 93 | export type InterpolatableRobotUploadHandleInstructionsWithHiddenFieldsInput = z.input< 94 | typeof interpolatableRobotUploadHandleInstructionsWithHiddenFieldsSchema 95 | > 96 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/image-generate.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 1, 9 | discount_factor: 1, 10 | discount_pct: 0, 11 | minimum_charge: 0, 12 | output_factor: 0.6, 13 | purpose_sentence: 'generates images from text prompts using AI', 14 | purpose_verb: 'generate', 15 | purpose_word: 'generate', 16 | purpose_words: 'Generate images from text prompts', 17 | service_slug: 'artificial-intelligence', 18 | slot_count: 10, 19 | title: 'Generate images from text prompts', 20 | typical_file_size_mb: 1.2, 21 | typical_file_type: 'image', 22 | name: 'ImageGenerateRobot', 23 | priceFactor: 1, 24 | queueSlotCount: 10, 25 | minimumChargeUsd: 0.06, 26 | isAllowedForUrlTransform: true, 27 | trackOutputFileSize: true, 28 | isInternal: false, 29 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 30 | stage: 'ga', 31 | } 32 | 33 | export const robotImageGenerateInstructionsSchema = robotBase 34 | .merge(robotUse) 35 | .extend({ 36 | robot: z.literal('/image/generate'), 37 | model: z 38 | .string() 39 | .optional() 40 | .describe('The AI model to use for image generation. Defaults to google/nano-banana.'), 41 | prompt: z.string().describe('The prompt describing the desired image content.'), 42 | format: z 43 | .enum(['jpeg', 'jpg', 'png', 'gif', 'webp', 'svg']) 44 | .optional() 45 | .describe('Format of the generated image.'), 46 | seed: z.number().optional().describe('Seed for the random number generator.'), 47 | aspect_ratio: z.string().optional().describe('Aspect ratio of the generated image.'), 48 | height: z.number().optional().describe('Height of the generated image.'), 49 | width: z.number().optional().describe('Width of the generated image.'), 50 | style: z.string().optional().describe('Style of the generated image.'), 51 | num_outputs: z 52 | .number() 53 | .int() 54 | .min(1) 55 | .max(10) 56 | .optional() 57 | .describe('Number of image variants to generate.'), 58 | }) 59 | .strict() 60 | 61 | export const robotImageGenerateInstructionsWithHiddenFieldsSchema = 62 | robotImageGenerateInstructionsSchema.extend({ 63 | provider: z.string().optional().describe('Provider for generating the image.'), 64 | result: z 65 | .union([z.literal('debug'), robotImageGenerateInstructionsSchema.shape.result]) 66 | .optional(), 67 | }) 68 | 69 | export type RobotImageGenerateInstructions = z.infer 70 | export type RobotImageGenerateInstructionsWithHiddenFields = z.infer< 71 | typeof robotImageGenerateInstructionsWithHiddenFieldsSchema 72 | > 73 | 74 | export const interpolatableRobotImageGenerateInstructionsSchema = interpolateRobot( 75 | robotImageGenerateInstructionsWithHiddenFieldsSchema, 76 | ) 77 | export type InterpolatableRobotImageGenerateInstructions = 78 | InterpolatableRobotImageGenerateInstructionsInput 79 | 80 | export type InterpolatableRobotImageGenerateInstructionsInput = z.input< 81 | typeof interpolatableRobotImageGenerateInstructionsSchema 82 | > 83 | 84 | export const interpolatableRobotImageGenerateInstructionsWithHiddenFieldsSchema = interpolateRobot( 85 | robotImageGenerateInstructionsWithHiddenFieldsSchema, 86 | ) 87 | export type InterpolatableRobotImageGenerateInstructionsWithHiddenFields = z.infer< 88 | typeof interpolatableRobotImageGenerateInstructionsWithHiddenFieldsSchema 89 | > 90 | export type InterpolatableRobotImageGenerateInstructionsWithHiddenFieldsInput = z.input< 91 | typeof interpolatableRobotImageGenerateInstructionsWithHiddenFieldsSchema 92 | > 93 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/dropbox-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { dropboxBase, interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/dropbox/store', 15 | use: ':original', 16 | credentials: 'YOUR_DROPBOX_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on Dropbox:', 22 | has_small_icon: true, 23 | minimum_charge: 0, 24 | output_factor: 1, 25 | override_lvl1: 'File Exporting', 26 | purpose_sentence: 'exports encoding results to Dropbox', 27 | purpose_verb: 'export', 28 | purpose_word: 'Dropbox', 29 | purpose_words: 'Export files to Dropbox', 30 | service_slug: 'file-exporting', 31 | slot_count: 10, 32 | title: 'Export files to Dropbox', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'DropboxStoreRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 10, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | isInternal: false, 41 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 42 | stage: 'ga', 43 | } 44 | 45 | export const robotDropboxStoreInstructionsSchema = robotBase 46 | .merge(robotUse) 47 | .merge(dropboxBase) 48 | .extend({ 49 | robot: z.literal('/dropbox/store'), 50 | path: z 51 | .string() 52 | .default('${unique_prefix}/${file.url_name}') 53 | .describe(` 54 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). 55 | `), 56 | create_sharing_link: z 57 | .boolean() 58 | .default(false) 59 | .describe(` 60 | Whether to create a URL to this file for sharing with other people. This will overwrite the file's \`"url"\` property. 61 | `), 62 | }) 63 | .strict() 64 | 65 | export const robotDropboxStoreInstructionsWithHiddenFieldsSchema = 66 | robotDropboxStoreInstructionsSchema.extend({ 67 | result: z 68 | .union([z.literal('debug'), robotDropboxStoreInstructionsSchema.shape.result]) 69 | .optional(), 70 | access_token: z.string().optional(), // Legacy field for backward compatibility 71 | }) 72 | 73 | export type RobotDropboxStoreInstructions = z.infer 74 | export type RobotDropboxStoreInstructionsInput = z.input 75 | export type RobotDropboxStoreInstructionsWithHiddenFields = z.infer< 76 | typeof robotDropboxStoreInstructionsWithHiddenFieldsSchema 77 | > 78 | 79 | export const interpolatableRobotDropboxStoreInstructionsSchema = interpolateRobot( 80 | robotDropboxStoreInstructionsSchema, 81 | ) 82 | export type InterpolatableRobotDropboxStoreInstructions = 83 | InterpolatableRobotDropboxStoreInstructionsInput 84 | 85 | export type InterpolatableRobotDropboxStoreInstructionsInput = z.input< 86 | typeof interpolatableRobotDropboxStoreInstructionsSchema 87 | > 88 | 89 | export const interpolatableRobotDropboxStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 90 | robotDropboxStoreInstructionsWithHiddenFieldsSchema, 91 | ) 92 | export type InterpolatableRobotDropboxStoreInstructionsWithHiddenFields = z.infer< 93 | typeof interpolatableRobotDropboxStoreInstructionsWithHiddenFieldsSchema 94 | > 95 | export type InterpolatableRobotDropboxStoreInstructionsWithHiddenFieldsInput = z.input< 96 | typeof interpolatableRobotDropboxStoreInstructionsWithHiddenFieldsSchema 97 | > 98 | -------------------------------------------------------------------------------- /test/testserver.ts: -------------------------------------------------------------------------------- 1 | import type { RequestListener, Server } from 'node:http' 2 | import { createServer } from 'node:http' 3 | import { setTimeout } from 'node:timers/promises' 4 | import debug from 'debug' 5 | import got from 'got' 6 | import type { CreateTunnelResult } from './tunnel.ts' 7 | import { createTunnel } from './tunnel.ts' 8 | 9 | const log = debug('transloadit:testserver') 10 | 11 | interface HttpServer { 12 | server: Server 13 | port: number 14 | } 15 | 16 | function createHttpServer(handler: RequestListener): Promise { 17 | return new Promise((resolve, reject) => { 18 | const server = createServer(handler) 19 | 20 | let port = 8000 21 | 22 | // Find a free port to use 23 | function tryListen() { 24 | server.listen(port, '127.0.0.1', () => { 25 | log(`server listening on port ${port}`) 26 | resolve({ server, port }) 27 | }) 28 | } 29 | server.on('error', (err) => { 30 | if ('code' in err && err.code === 'EADDRINUSE') { 31 | if (++port >= 65535) { 32 | server.close() 33 | reject(new Error('Failed to find any free port to listen on')) 34 | return 35 | } 36 | tryListen() 37 | return 38 | } 39 | reject(err) 40 | }) 41 | 42 | tryListen() 43 | }) 44 | } 45 | 46 | export async function createTestServer(onRequest: RequestListener) { 47 | if (!process.env.CLOUDFLARED_PATH) { 48 | throw new Error('CLOUDFLARED_PATH environment variable not set') 49 | } 50 | 51 | let expectedPath: string 52 | let initialized = false 53 | let onTunnelOperational: () => void 54 | let tunnel: CreateTunnelResult 55 | 56 | const handleHttpRequest: RequestListener = (req, res) => { 57 | log('HTTP request handler', req.method, req.url) 58 | 59 | if (!initialized) { 60 | if (req.url !== expectedPath) throw new Error(`Unexpected path ${req.url}`) 61 | initialized = true 62 | onTunnelOperational() 63 | res.end() 64 | } else { 65 | onRequest(req, res) 66 | } 67 | } 68 | 69 | const { server, port } = await createHttpServer(handleHttpRequest) 70 | 71 | async function close() { 72 | await tunnel?.close() 73 | await new Promise((resolve) => server.close(() => resolve())) 74 | log('closed tunnel') 75 | } 76 | 77 | try { 78 | tunnel = await createTunnel({ cloudFlaredPath: process.env.CLOUDFLARED_PATH, port }) 79 | 80 | log('waiting for tunnel to be created') 81 | const { url: tunnelPublicUrl } = await tunnel 82 | log('tunnel created', tunnelPublicUrl) 83 | 84 | log('Waiting for tunnel to allow requests to pass through') 85 | 86 | async function sendTunnelRequest() { 87 | // try connecting to the tunnel and resolve when connection successfully passed through 88 | for (let i = 0; i < 10; i += 1) { 89 | if (initialized) return 90 | 91 | expectedPath = `/initialize-test${i}` 92 | try { 93 | await got(`${tunnelPublicUrl}${expectedPath}`, { timeout: { request: 2000 } }) 94 | return 95 | } catch { 96 | // console.error(err.message) 97 | await setTimeout(3000) 98 | } 99 | } 100 | throw new Error('Timed out checking for an operational tunnel') 101 | } 102 | 103 | await Promise.all([ 104 | new Promise((resolve) => { 105 | onTunnelOperational = resolve 106 | }), 107 | sendTunnelRequest(), 108 | ]) 109 | 110 | log('Tunnel ready', tunnelPublicUrl) 111 | 112 | return { 113 | port, 114 | close, 115 | url: tunnelPublicUrl, 116 | } 117 | } catch (err) { 118 | await close() 119 | throw err 120 | } 121 | } 122 | 123 | export type TestServer = Awaited> 124 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/audio-encode.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { stackVersions } from '../stackVersions.ts' 4 | import type { RobotMetaInput } from './_instructions-primitives.ts' 5 | import { 6 | bitrateSchema, 7 | interpolateRobot, 8 | robotBase, 9 | robotFFmpegAudio, 10 | robotUse, 11 | sampleRateSchema, 12 | } from './_instructions-primitives.ts' 13 | 14 | export const meta: RobotMetaInput = { 15 | allowed_for_url_transform: false, 16 | bytescount: 4, 17 | discount_factor: 0.25, 18 | discount_pct: 75, 19 | example_code: { 20 | steps: { 21 | mp3_encoded: { 22 | robot: '/audio/encode', 23 | use: ':original', 24 | preset: 'mp3', 25 | bitrate: 256000, 26 | ffmpeg_stack: stackVersions.ffmpeg.recommendedVersion, 27 | }, 28 | }, 29 | }, 30 | example_code_description: 'Encode uploaded audio to MP3 format at a 256 kbps bitrate:', 31 | minimum_charge: 0, 32 | output_factor: 0.8, 33 | override_lvl1: 'Audio Encoding', 34 | purpose_sentence: 35 | 'converts audio files into all kinds of formats for you. We provide encoding presets for the most common formats', 36 | purpose_verb: 'encode', 37 | purpose_word: 'encode', 38 | purpose_words: 'Encode audio', 39 | service_slug: 'audio-encoding', 40 | slot_count: 20, 41 | title: 'Encode audio', 42 | typical_file_size_mb: 3.8, 43 | typical_file_type: 'audio file', 44 | uses_tools: ['ffmpeg'], 45 | name: 'AudioEncodeRobot', 46 | priceFactor: 4, 47 | queueSlotCount: 20, 48 | isAllowedForUrlTransform: false, 49 | trackOutputFileSize: true, 50 | isInternal: false, 51 | stage: 'ga', 52 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 53 | } 54 | 55 | export const robotAudioEncodeInstructionsSchema = robotBase 56 | .merge(robotUse) 57 | .merge(robotFFmpegAudio) 58 | .extend({ 59 | result: z 60 | .boolean() 61 | .optional() 62 | .describe('Whether the results of this Step should be present in the Assembly Status JSON'), 63 | robot: z.literal('/audio/encode'), 64 | bitrate: bitrateSchema.optional().describe(` 65 | Bit rate of the resulting audio file, in bits per second. If not specified will default to the bit rate of the input audio file. 66 | `), 67 | sample_rate: sampleRateSchema.optional().describe(` 68 | Sample rate of the resulting audio file, in Hertz. If not specified will default to the sample rate of the input audio file. 69 | `), 70 | }) 71 | .strict() 72 | 73 | export const robotAudioEncodeInstructionsWithHiddenFieldsSchema = 74 | robotAudioEncodeInstructionsSchema.extend({ 75 | result: z 76 | .union([z.literal('debug'), robotAudioEncodeInstructionsSchema.shape.result]) 77 | .optional(), 78 | }) 79 | 80 | export type RobotAudioEncodeInstructions = z.infer 81 | export type RobotAudioEncodeInstructionsWithHiddenFields = z.infer< 82 | typeof robotAudioEncodeInstructionsWithHiddenFieldsSchema 83 | > 84 | 85 | export const interpolatableRobotAudioEncodeInstructionsSchema = interpolateRobot( 86 | robotAudioEncodeInstructionsSchema, 87 | ) 88 | export type InterpolatableRobotAudioEncodeInstructions = 89 | InterpolatableRobotAudioEncodeInstructionsInput 90 | 91 | export type InterpolatableRobotAudioEncodeInstructionsInput = z.input< 92 | typeof interpolatableRobotAudioEncodeInstructionsSchema 93 | > 94 | 95 | export const interpolatableRobotAudioEncodeInstructionsWithHiddenFieldsSchema = interpolateRobot( 96 | robotAudioEncodeInstructionsWithHiddenFieldsSchema, 97 | ) 98 | export type InterpolatableRobotAudioEncodeInstructionsWithHiddenFields = z.infer< 99 | typeof interpolatableRobotAudioEncodeInstructionsWithHiddenFieldsSchema 100 | > 101 | export type InterpolatableRobotAudioEncodeInstructionsWithHiddenFieldsInput = z.input< 102 | typeof interpolatableRobotAudioEncodeInstructionsWithHiddenFieldsSchema 103 | > 104 | -------------------------------------------------------------------------------- /src/cli/OutputCtl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Log levels following syslog severity (https://en.wikipedia.org/wiki/Syslog#Severity_level) 3 | * Lower numbers = more severe, higher numbers = more verbose 4 | */ 5 | export const LOG_LEVEL = { 6 | ERR: 3, // Error conditions 7 | WARN: 4, // Warning conditions 8 | NOTICE: 5, // Normal but significant (default) 9 | INFO: 6, // Informational 10 | DEBUG: 7, // Debug-level messages 11 | TRACE: 8, // Most verbose/detailed 12 | } as const 13 | 14 | export type LogLevelName = keyof typeof LOG_LEVEL 15 | export type LogLevelValue = (typeof LOG_LEVEL)[LogLevelName] 16 | 17 | export const LOG_LEVEL_DEFAULT: LogLevelValue = LOG_LEVEL.NOTICE 18 | 19 | /** Valid log level names for CLI parsing */ 20 | export const LOG_LEVEL_NAMES = Object.keys(LOG_LEVEL).map((k) => 21 | k.toLowerCase(), 22 | ) as Lowercase[] 23 | 24 | /** Valid numeric log level values */ 25 | const LOG_LEVEL_VALUES = new Set(Object.values(LOG_LEVEL)) 26 | 27 | /** Parse a log level string (name or number) to its numeric value */ 28 | export function parseLogLevel(level: string): LogLevelValue { 29 | // Try parsing as number first 30 | const num = Number(level) 31 | if (!Number.isNaN(num)) { 32 | if (LOG_LEVEL_VALUES.has(num as LogLevelValue)) { 33 | return num as LogLevelValue 34 | } 35 | throw new Error( 36 | `Invalid log level: ${level}. Valid values: ${[...LOG_LEVEL_VALUES].join(', ')} or ${LOG_LEVEL_NAMES.join(', ')}`, 37 | ) 38 | } 39 | 40 | // Try as level name 41 | const upper = level.toUpperCase() as LogLevelName 42 | if (upper in LOG_LEVEL) { 43 | return LOG_LEVEL[upper] 44 | } 45 | throw new Error( 46 | `Invalid log level: ${level}. Valid levels: ${LOG_LEVEL_NAMES.join(', ')} or ${[...LOG_LEVEL_VALUES].join(', ')}`, 47 | ) 48 | } 49 | 50 | export interface OutputCtlOptions { 51 | logLevel?: LogLevelValue 52 | jsonMode?: boolean 53 | } 54 | 55 | /** Interface for output controllers (used to allow test mocks) */ 56 | export interface IOutputCtl { 57 | error(msg: unknown): void 58 | warn(msg: unknown): void 59 | notice(msg: unknown): void 60 | info(msg: unknown): void 61 | debug(msg: unknown): void 62 | trace(msg: unknown): void 63 | print(simple: unknown, json: unknown): void 64 | } 65 | 66 | export default class OutputCtl implements IOutputCtl { 67 | private json: boolean 68 | private logLevel: LogLevelValue 69 | 70 | constructor({ logLevel = LOG_LEVEL_DEFAULT, jsonMode = false }: OutputCtlOptions = {}) { 71 | this.json = jsonMode 72 | this.logLevel = logLevel 73 | 74 | process.stdout.on('error', (err: NodeJS.ErrnoException) => { 75 | if (err.code === 'EPIPE') { 76 | process.exitCode = 0 77 | } 78 | }) 79 | process.stderr.on('error', (err: NodeJS.ErrnoException) => { 80 | if (err.code === 'EPIPE') { 81 | process.exitCode = 0 82 | } 83 | }) 84 | } 85 | 86 | error(msg: unknown): void { 87 | if (this.logLevel >= LOG_LEVEL.ERR) console.error('err ', msg) 88 | } 89 | 90 | warn(msg: unknown): void { 91 | if (this.logLevel >= LOG_LEVEL.WARN) console.error('warn ', msg) 92 | } 93 | 94 | notice(msg: unknown): void { 95 | if (this.logLevel >= LOG_LEVEL.NOTICE) console.error('notice ', msg) 96 | } 97 | 98 | info(msg: unknown): void { 99 | if (this.logLevel >= LOG_LEVEL.INFO) console.error('info ', msg) 100 | } 101 | 102 | debug(msg: unknown): void { 103 | if (this.logLevel >= LOG_LEVEL.DEBUG) console.error('debug ', msg) 104 | } 105 | 106 | trace(msg: unknown): void { 107 | if (this.logLevel >= LOG_LEVEL.TRACE) console.error('trace ', msg) 108 | } 109 | 110 | print(simple: unknown, json: unknown): void { 111 | if (this.json) console.log(JSON.stringify(json)) 112 | else if (typeof simple === 'string') console.log(simple) 113 | else console.dir(simple, { depth: null }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/dropbox-import.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { 5 | dropboxBase, 6 | interpolateRobot, 7 | path, 8 | robotBase, 9 | robotImport, 10 | } from './_instructions-primitives.ts' 11 | 12 | export const meta: RobotMetaInput = { 13 | allowed_for_url_transform: true, 14 | bytescount: 10, 15 | discount_factor: 0.1, 16 | discount_pct: 90, 17 | example_code: { 18 | steps: { 19 | imported: { 20 | robot: '/dropbox/import', 21 | credentials: 'YOUR_DROPBOX_CREDENTIALS', 22 | path: 'path/to/files/', 23 | }, 24 | }, 25 | }, 26 | example_code_description: 27 | 'Import files from the `path/to/files` directory and its subdirectories:', 28 | has_small_icon: true, 29 | minimum_charge: 0, 30 | output_factor: 1, 31 | override_lvl1: 'File Importing', 32 | purpose_sentence: 'imports whole directories of files from your Dropbox', 33 | purpose_verb: 'import', 34 | purpose_word: 'Dropbox', 35 | purpose_words: 'Import files from Dropbox', 36 | requires_credentials: true, 37 | service_slug: 'file-importing', 38 | slot_count: 20, 39 | title: 'Import files from Dropbox', 40 | typical_file_size_mb: 1.2, 41 | typical_file_type: 'file', 42 | name: 'DropboxImportRobot', 43 | priceFactor: 6.6666, 44 | queueSlotCount: 20, 45 | isAllowedForUrlTransform: true, 46 | trackOutputFileSize: false, 47 | isInternal: false, 48 | removeJobResultFilesFromDiskRightAfterStoringOnS3: true, 49 | stage: 'ga', 50 | } 51 | 52 | export const robotDropboxImportInstructionsSchema = robotBase 53 | .merge(robotImport) 54 | .merge(dropboxBase) 55 | .extend({ 56 | robot: z.literal('/dropbox/import'), 57 | path: path.describe(` 58 | The path in your Dropbox to the specific file or directory. If the path points to a file, only this file will be imported. For example: \`images/avatar.jpg\`. 59 | 60 | If it points to a directory, indicated by a trailing slash (\`/\`), then all files that are descendants of this directory are recursively imported. For example: \`images/\`. 61 | 62 | If you want to import all files from the root directory, please use \`/\` as the value here. 63 | 64 | You can also use an array of path strings here to import multiple paths in the same Robot's Step. 65 | `), 66 | }) 67 | .strict() 68 | 69 | export const robotDropboxImportInstructionsWithHiddenFieldsSchema = 70 | robotDropboxImportInstructionsSchema.extend({ 71 | result: z 72 | .union([z.literal('debug'), robotDropboxImportInstructionsSchema.shape.result]) 73 | .optional(), 74 | access_token: z.string().optional(), // Legacy field for backward compatibility 75 | }) 76 | 77 | export type RobotDropboxImportInstructions = z.infer 78 | export type RobotDropboxImportInstructionsWithHiddenFields = z.infer< 79 | typeof robotDropboxImportInstructionsWithHiddenFieldsSchema 80 | > 81 | 82 | export const interpolatableRobotDropboxImportInstructionsSchema = interpolateRobot( 83 | robotDropboxImportInstructionsSchema, 84 | ) 85 | export type InterpolatableRobotDropboxImportInstructions = 86 | InterpolatableRobotDropboxImportInstructionsInput 87 | 88 | export type InterpolatableRobotDropboxImportInstructionsInput = z.input< 89 | typeof interpolatableRobotDropboxImportInstructionsSchema 90 | > 91 | 92 | export const interpolatableRobotDropboxImportInstructionsWithHiddenFieldsSchema = interpolateRobot( 93 | robotDropboxImportInstructionsWithHiddenFieldsSchema, 94 | ) 95 | export type InterpolatableRobotDropboxImportInstructionsWithHiddenFields = z.infer< 96 | typeof interpolatableRobotDropboxImportInstructionsWithHiddenFieldsSchema 97 | > 98 | export type InterpolatableRobotDropboxImportInstructionsWithHiddenFieldsInput = z.input< 99 | typeof interpolatableRobotDropboxImportInstructionsWithHiddenFieldsSchema 100 | > 101 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/cloudfiles-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { 5 | cloudfilesBase, 6 | interpolateRobot, 7 | robotBase, 8 | robotUse, 9 | } from './_instructions-primitives.ts' 10 | 11 | export const meta: RobotMetaInput = { 12 | allowed_for_url_transform: true, 13 | bytescount: 6, 14 | discount_factor: 0.15000150001500018, 15 | discount_pct: 84.99984999849998, 16 | example_code: { 17 | steps: { 18 | exported: { 19 | robot: '/cloudfiles/store', 20 | use: ':original', 21 | credentials: 'YOUR_CLOUDFILES_CREDENTIALS', 22 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 23 | }, 24 | }, 25 | }, 26 | example_code_description: 'Export uploaded files to `my_target_folder` on Rackspace Cloud Files:', 27 | extended_description: ` 28 | 29 | 30 | ## A note about URLs 31 | 32 | If your container is CDN-enabled, the resulting \`file.url\` indicates the path to the file in your 33 | CDN container, or is \`null\` otherwise. 34 | 35 | The storage container URL for this file is always available via \`file.meta.storage_url\`. 36 | `, 37 | minimum_charge: 0, 38 | output_factor: 1, 39 | override_lvl1: 'File Exporting', 40 | purpose_sentence: 'exports encoding results to Rackspace Cloud Files', 41 | purpose_verb: 'export', 42 | purpose_word: 'Rackspace Cloud Files', 43 | purpose_words: 'Export files to Rackspace Cloud Files', 44 | service_slug: 'file-exporting', 45 | slot_count: 10, 46 | title: 'Export files to Rackspace Cloud Files', 47 | typical_file_size_mb: 1.2, 48 | typical_file_type: 'file', 49 | name: 'CloudfilesStoreRobot', 50 | priceFactor: 6.6666, 51 | queueSlotCount: 10, 52 | isAllowedForUrlTransform: true, 53 | trackOutputFileSize: false, 54 | isInternal: false, 55 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 56 | stage: 'ga', 57 | } 58 | 59 | export const robotCloudfilesStoreInstructionsSchema = robotBase 60 | .merge(robotUse) 61 | .merge(cloudfilesBase) 62 | .extend({ 63 | robot: z.literal('/cloudfiles/store'), 64 | path: z 65 | .string() 66 | .default('${unique_prefix}/${file.url_name}') 67 | .describe(` 68 | The path at which to store the file. This value can also contain [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). 69 | `), 70 | }) 71 | .strict() 72 | 73 | export const robotCloudfilesStoreInstructionsWithHiddenFieldsSchema = 74 | robotCloudfilesStoreInstructionsSchema.extend({ 75 | result: z 76 | .union([z.literal('debug'), robotCloudfilesStoreInstructionsSchema.shape.result]) 77 | .optional(), 78 | }) 79 | 80 | export type RobotCloudfilesStoreInstructions = z.infer< 81 | typeof robotCloudfilesStoreInstructionsSchema 82 | > 83 | export type RobotCloudfilesStoreInstructionsWithHiddenFields = z.infer< 84 | typeof robotCloudfilesStoreInstructionsWithHiddenFieldsSchema 85 | > 86 | 87 | export const interpolatableRobotCloudfilesStoreInstructionsSchema = interpolateRobot( 88 | robotCloudfilesStoreInstructionsSchema, 89 | ) 90 | export type InterpolatableRobotCloudfilesStoreInstructions = 91 | InterpolatableRobotCloudfilesStoreInstructionsInput 92 | 93 | export type InterpolatableRobotCloudfilesStoreInstructionsInput = z.input< 94 | typeof interpolatableRobotCloudfilesStoreInstructionsSchema 95 | > 96 | 97 | export const interpolatableRobotCloudfilesStoreInstructionsWithHiddenFieldsSchema = 98 | interpolateRobot(robotCloudfilesStoreInstructionsWithHiddenFieldsSchema) 99 | export type InterpolatableRobotCloudfilesStoreInstructionsWithHiddenFields = z.infer< 100 | typeof interpolatableRobotCloudfilesStoreInstructionsWithHiddenFieldsSchema 101 | > 102 | export type InterpolatableRobotCloudfilesStoreInstructionsWithHiddenFieldsInput = z.input< 103 | typeof interpolatableRobotCloudfilesStoreInstructionsWithHiddenFieldsSchema 104 | > 105 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/audio-loop.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { stackVersions } from '../stackVersions.ts' 4 | import type { RobotMetaInput } from './_instructions-primitives.ts' 5 | import { 6 | bitrateSchema, 7 | interpolateRobot, 8 | robotBase, 9 | robotFFmpegAudio, 10 | robotUse, 11 | sampleRateSchema, 12 | } from './_instructions-primitives.ts' 13 | 14 | export const meta: RobotMetaInput = { 15 | allowed_for_url_transform: false, 16 | bytescount: 4, 17 | discount_factor: 0.25, 18 | discount_pct: 75, 19 | example_code: { 20 | steps: { 21 | looped: { 22 | robot: '/audio/loop', 23 | use: ':original', 24 | duration: 300, 25 | ffmpeg_stack: stackVersions.ffmpeg.recommendedVersion, 26 | }, 27 | }, 28 | }, 29 | example_code_description: 'Loop uploaded audio to achieve a target duration of 300 seconds:', 30 | marketing_intro: 31 | 'Whether you’re producing beats, white-noise, or just empty segments as fillers between audio tracks that you’re to stringing together with [🤖/audio/concat](/docs/robots/audio-concat/), [🤖/audio/loop](/docs/robots/audio-loop/) has got your back.', 32 | minimum_charge: 0, 33 | output_factor: 0.8, 34 | override_lvl1: 'Audio Encoding', 35 | purpose_sentence: 'loops one audio file as often as is required to match a given duration', 36 | purpose_verb: 'loop', 37 | purpose_word: 'loop', 38 | purpose_words: 'Loop audio', 39 | service_slug: 'audio-encoding', 40 | slot_count: 20, 41 | title: 'Loop audio', 42 | typical_file_size_mb: 3.8, 43 | typical_file_type: 'audio file', 44 | uses_tools: ['ffmpeg'], 45 | name: 'AudioLoopRobot', 46 | priceFactor: 4, 47 | queueSlotCount: 20, 48 | isAllowedForUrlTransform: false, 49 | trackOutputFileSize: true, 50 | isInternal: false, 51 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 52 | stage: 'ga', 53 | } 54 | 55 | export const robotAudioLoopInstructionsSchema = robotBase 56 | .merge(robotUse) 57 | .merge(robotFFmpegAudio) 58 | .extend({ 59 | robot: z.literal('/audio/loop'), 60 | bitrate: bitrateSchema.optional().describe(` 61 | Bit rate of the resulting audio file, in bits per second. If not specified will default to the bit rate of the input audio file. 62 | `), 63 | sample_rate: sampleRateSchema.optional().describe(` 64 | Sample rate of the resulting audio file, in Hertz. If not specified will default to the sample rate of the input audio file. 65 | `), 66 | duration: z 67 | .number() 68 | .default(60) 69 | .describe(` 70 | Target duration for the whole process in seconds. The Robot will loop the input audio file for as long as this target duration is not reached yet. 71 | `), 72 | }) 73 | .strict() 74 | 75 | export const robotAudioLoopInstructionsWithHiddenFieldsSchema = 76 | robotAudioLoopInstructionsSchema.extend({ 77 | result: z.union([z.literal('debug'), robotAudioLoopInstructionsSchema.shape.result]).optional(), 78 | }) 79 | 80 | export type RobotAudioLoopInstructions = z.infer 81 | export type RobotAudioLoopInstructionsWithHiddenFields = z.infer< 82 | typeof robotAudioLoopInstructionsWithHiddenFieldsSchema 83 | > 84 | 85 | export const interpolatableRobotAudioLoopInstructionsSchema = interpolateRobot( 86 | robotAudioLoopInstructionsSchema, 87 | ) 88 | export type InterpolatableRobotAudioLoopInstructions = InterpolatableRobotAudioLoopInstructionsInput 89 | 90 | export type InterpolatableRobotAudioLoopInstructionsInput = z.input< 91 | typeof interpolatableRobotAudioLoopInstructionsSchema 92 | > 93 | 94 | export const interpolatableRobotAudioLoopInstructionsWithHiddenFieldsSchema = interpolateRobot( 95 | robotAudioLoopInstructionsWithHiddenFieldsSchema, 96 | ) 97 | export type InterpolatableRobotAudioLoopInstructionsWithHiddenFields = z.infer< 98 | typeof interpolatableRobotAudioLoopInstructionsWithHiddenFieldsSchema 99 | > 100 | export type InterpolatableRobotAudioLoopInstructionsWithHiddenFieldsInput = z.input< 101 | typeof interpolatableRobotAudioLoopInstructionsWithHiddenFieldsSchema 102 | > 103 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/backblaze-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { backblazeBase, interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/backblaze/store', 15 | use: ':original', 16 | credentials: 'YOUR_BACKBLAZE_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on Backblaze:', 22 | extended_description: ` 23 | ## Access 24 | 25 | Your Backblaze buckets need to have the \`listBuckets\` (to obtain a bucket ID from a bucket name), \`writeFiles\` and \`listFiles\` permissions. 26 | `, 27 | has_small_icon: true, 28 | minimum_charge: 0, 29 | output_factor: 1, 30 | override_lvl1: 'File Exporting', 31 | purpose_sentence: 'exports encoding results to Backblaze', 32 | purpose_verb: 'export', 33 | purpose_word: 'Backblaze', 34 | purpose_words: 'Export files to Backblaze', 35 | service_slug: 'file-exporting', 36 | slot_count: 10, 37 | title: 'Export files to Backblaze', 38 | typical_file_size_mb: 1.2, 39 | typical_file_type: 'file', 40 | name: 'BackblazeStoreRobot', 41 | priceFactor: 6.6666, 42 | queueSlotCount: 10, 43 | isAllowedForUrlTransform: true, 44 | trackOutputFileSize: false, 45 | isInternal: false, 46 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 47 | stage: 'ga', 48 | } 49 | 50 | export const robotBackblazeStoreInstructionsSchema = robotBase 51 | .merge(robotUse) 52 | .merge(backblazeBase) 53 | .extend({ 54 | robot: z.literal('/backblaze/store'), 55 | path: z 56 | .string() 57 | .default('${unique_prefix}/${file.url_name}') 58 | .describe(` 59 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). 60 | `), 61 | headers: z 62 | .record(z.string()) 63 | .default({}) 64 | .describe(` 65 | An object containing a list of headers to be set for this file on backblaze, such as \`{ FileURL: "\${file.url_name}" }\`. This can also include any available [Assembly Variables](/docs/topics/assembly-instructions/#assembly-variables). 66 | 67 | [Here](https://www.backblaze.com/b2/docs/b2_upload_file.html) you can find a list of available headers. 68 | 69 | Object Metadata can be specified using \`X-Bz-Info-*\` headers. 70 | `), 71 | }) 72 | .strict() 73 | 74 | export const robotBackblazeStoreInstructionsWithHiddenFieldsSchema = 75 | robotBackblazeStoreInstructionsSchema.extend({ 76 | result: z 77 | .union([z.literal('debug'), robotBackblazeStoreInstructionsSchema.shape.result]) 78 | .optional(), 79 | }) 80 | 81 | export type RobotBackblazeStoreInstructions = z.infer 82 | export type RobotBackblazeStoreInstructionsWithHiddenFields = z.infer< 83 | typeof robotBackblazeStoreInstructionsWithHiddenFieldsSchema 84 | > 85 | 86 | export const interpolatableRobotBackblazeStoreInstructionsSchema = interpolateRobot( 87 | robotBackblazeStoreInstructionsSchema, 88 | ) 89 | export type InterpolatableRobotBackblazeStoreInstructions = 90 | InterpolatableRobotBackblazeStoreInstructionsInput 91 | 92 | export type InterpolatableRobotBackblazeStoreInstructionsInput = z.input< 93 | typeof interpolatableRobotBackblazeStoreInstructionsSchema 94 | > 95 | 96 | export const interpolatableRobotBackblazeStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 97 | robotBackblazeStoreInstructionsWithHiddenFieldsSchema, 98 | ) 99 | export type InterpolatableRobotBackblazeStoreInstructionsWithHiddenFields = z.infer< 100 | typeof interpolatableRobotBackblazeStoreInstructionsWithHiddenFieldsSchema 101 | > 102 | export type InterpolatableRobotBackblazeStoreInstructionsWithHiddenFieldsInput = z.input< 103 | typeof interpolatableRobotBackblazeStoreInstructionsWithHiddenFieldsSchema 104 | > 105 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/audio-artwork.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { stackVersions } from '../stackVersions.ts' 4 | import type { RobotMetaInput } from './_instructions-primitives.ts' 5 | import { 6 | interpolateRobot, 7 | robotBase, 8 | robotFFmpegAudio, 9 | robotUse, 10 | } from './_instructions-primitives.ts' 11 | 12 | export const meta: RobotMetaInput = { 13 | allowed_for_url_transform: true, 14 | bytescount: 1, 15 | discount_factor: 1, 16 | discount_pct: 0, 17 | example_code: { 18 | steps: { 19 | artwork_extracted: { 20 | robot: '/audio/artwork', 21 | use: ':original', 22 | ffmpeg_stack: stackVersions.ffmpeg.recommendedVersion, 23 | }, 24 | }, 25 | }, 26 | example_code_description: 'Extract embedded cover artwork from uploaded audio files:', 27 | minimum_charge: 0, 28 | output_factor: 0.8, 29 | override_lvl1: 'Audio Encoding', 30 | purpose_sentence: 31 | 'extracts the embedded cover artwork from audio files and allows you to pipe it into other Steps, for example into /image/resize Steps. It can also insert images into audio files as cover artwork', 32 | purpose_verb: 'extract', 33 | purpose_word: 'extract/insert artwork', 34 | purpose_words: 'Extract or insert audio artwork', 35 | service_slug: 'audio-encoding', 36 | slot_count: 20, 37 | title: 'Extract or insert audio artwork', 38 | typical_file_size_mb: 3.8, 39 | typical_file_type: 'audio file', 40 | uses_tools: ['ffmpeg'], 41 | name: 'AudioArtworkRobot', 42 | priceFactor: 1, 43 | queueSlotCount: 20, 44 | isAllowedForUrlTransform: true, 45 | trackOutputFileSize: true, 46 | isInternal: false, 47 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 48 | stage: 'ga', 49 | } 50 | 51 | export const robotAudioArtworkInstructionsSchema = robotBase 52 | .merge(robotUse) 53 | .merge(robotFFmpegAudio) 54 | .extend({ 55 | robot: z.literal('/audio/artwork').describe(` 56 | For extraction, this Robot uses the image format embedded within the audio file — most often, this is JPEG. 57 | 58 | If you need the image in a different format, pipe the result of this Robot into [🤖/image/resize](/docs/robots/image-resize/). 59 | 60 | The \`method\` parameter determines whether to extract or insert. 61 | `), 62 | method: z 63 | .enum(['extract', 'insert']) 64 | .default('extract') 65 | .describe(` 66 | What should be done with the audio file. A value of \`"extract"\` means audio artwork will be extracted. A value of \`"insert"\` means the provided image will be inserted as audio artwork. 67 | `), 68 | change_format_if_necessary: z 69 | .boolean() 70 | .default(false) 71 | .describe(` 72 | Whether the original file should be transcoded into a new format if there is an issue with the original file. 73 | `), 74 | }) 75 | .strict() 76 | 77 | export const robotAudioArtworkInstructionsWithHiddenFieldsSchema = 78 | robotAudioArtworkInstructionsSchema.extend({ 79 | result: z 80 | .union([z.literal('debug'), robotAudioArtworkInstructionsSchema.shape.result]) 81 | .optional(), 82 | }) 83 | 84 | export type RobotAudioArtworkInstructions = z.infer 85 | export type RobotAudioArtworkInstructionsWithHiddenFields = z.infer< 86 | typeof robotAudioArtworkInstructionsWithHiddenFieldsSchema 87 | > 88 | 89 | export const interpolatableRobotAudioArtworkInstructionsSchema = interpolateRobot( 90 | robotAudioArtworkInstructionsSchema, 91 | ) 92 | export type InterpolatableRobotAudioArtworkInstructions = 93 | InterpolatableRobotAudioArtworkInstructionsInput 94 | 95 | export type InterpolatableRobotAudioArtworkInstructionsInput = z.input< 96 | typeof interpolatableRobotAudioArtworkInstructionsSchema 97 | > 98 | 99 | export const interpolatableRobotAudioArtworkInstructionsWithHiddenFieldsSchema = interpolateRobot( 100 | robotAudioArtworkInstructionsWithHiddenFieldsSchema, 101 | ) 102 | export type InterpolatableRobotAudioArtworkInstructionsWithHiddenFields = z.infer< 103 | typeof interpolatableRobotAudioArtworkInstructionsWithHiddenFieldsSchema 104 | > 105 | export type InterpolatableRobotAudioArtworkInstructionsWithHiddenFieldsInput = z.input< 106 | typeof interpolatableRobotAudioArtworkInstructionsWithHiddenFieldsSchema 107 | > 108 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/document-merge.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 1, 9 | discount_factor: 1, 10 | discount_pct: 0, 11 | example_code: { 12 | steps: { 13 | merged: { 14 | robot: '/document/merge', 15 | use: { 16 | steps: [':original'], 17 | bundle_steps: true, 18 | }, 19 | }, 20 | }, 21 | }, 22 | example_code_description: 'Merge all uploaded PDF documents into one:', 23 | extended_description: ` 24 | > ![Note] 25 | > This Robot can merge PDF files only at the moment. 26 | 27 | Input files are sorted alphanumerically unless you provide the as-syntax in the "use" parameter. For example: 28 | 29 | \`\`\`json 30 | { 31 | "use": [ 32 | { "name": "my_step_name", "as": "document_2" }, 33 | { "name": "my_other_step_name", "as": "document_1" } 34 | ] 35 | } 36 | \`\`\` 37 | `, 38 | minimum_charge: 1048576, 39 | output_factor: 1, 40 | override_lvl1: 'Document Processing', 41 | purpose_sentence: 'concatenates several PDF documents into a single file', 42 | purpose_verb: 'convert', 43 | purpose_word: 'convert', 44 | purpose_words: 'Merge documents into one', 45 | service_slug: 'document-processing', 46 | slot_count: 10, 47 | title: 'Merge documents into one', 48 | typical_file_size_mb: 0.8, 49 | typical_file_type: 'document', 50 | name: 'DocumentMergeRobot', 51 | priceFactor: 1, 52 | queueSlotCount: 10, 53 | minimumCharge: 1048576, 54 | isAllowedForUrlTransform: true, 55 | trackOutputFileSize: true, 56 | isInternal: false, 57 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 58 | stage: 'ga', 59 | } 60 | 61 | export const robotDocumentMergeInstructionsSchema = robotBase 62 | .merge(robotUse) 63 | .extend({ 64 | robot: z.literal('/document/merge'), 65 | input_passwords: z 66 | .array(z.string()) 67 | .default([]) 68 | .describe(` 69 | An array of passwords for the input documents, in case they are encrypted. The order of passwords must match the order of the documents as they are passed to the /document/merge step. 70 | 71 | This can be achieved via our as-syntax using "document_1", "document_2", etc if provided. See the demos below. 72 | 73 | If the as-syntax is not used in the "use" parameter, the documents are sorted alphanumerically based on their filename, and in that order input passwords should be provided. 74 | `), 75 | output_password: z 76 | .string() 77 | .optional() 78 | .describe(` 79 | If not empty, encrypts the output file and makes it accessible only by typing in this password. 80 | `), 81 | }) 82 | .strict() 83 | 84 | export const robotDocumentMergeInstructionsWithHiddenFieldsSchema = 85 | robotDocumentMergeInstructionsSchema.extend({ 86 | result: z 87 | .union([z.literal('debug'), robotDocumentMergeInstructionsSchema.shape.result]) 88 | .optional(), 89 | }) 90 | 91 | export type RobotDocumentMergeInstructions = z.infer 92 | export type RobotDocumentMergeInstructionsWithHiddenFields = z.infer< 93 | typeof robotDocumentMergeInstructionsWithHiddenFieldsSchema 94 | > 95 | 96 | export const interpolatableRobotDocumentMergeInstructionsSchema = interpolateRobot( 97 | robotDocumentMergeInstructionsSchema, 98 | ) 99 | export type InterpolatableRobotDocumentMergeInstructions = 100 | InterpolatableRobotDocumentMergeInstructionsInput 101 | 102 | export type InterpolatableRobotDocumentMergeInstructionsInput = z.input< 103 | typeof interpolatableRobotDocumentMergeInstructionsSchema 104 | > 105 | 106 | export const interpolatableRobotDocumentMergeInstructionsWithHiddenFieldsSchema = interpolateRobot( 107 | robotDocumentMergeInstructionsWithHiddenFieldsSchema, 108 | ) 109 | export type InterpolatableRobotDocumentMergeInstructionsWithHiddenFields = z.infer< 110 | typeof interpolatableRobotDocumentMergeInstructionsWithHiddenFieldsSchema 111 | > 112 | export type InterpolatableRobotDocumentMergeInstructionsWithHiddenFieldsInput = z.input< 113 | typeof interpolatableRobotDocumentMergeInstructionsWithHiddenFieldsSchema 114 | > 115 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/azure-import.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { 5 | azureBase, 6 | files_per_page, 7 | interpolateRobot, 8 | next_page_token, 9 | path, 10 | recursive, 11 | robotBase, 12 | robotImport, 13 | } from './_instructions-primitives.ts' 14 | 15 | export const meta: RobotMetaInput = { 16 | allowed_for_url_transform: true, 17 | bytescount: 10, 18 | discount_factor: 0.1, 19 | discount_pct: 90, 20 | example_code: { 21 | steps: { 22 | imported: { 23 | robot: '/azure/import', 24 | credentials: 'YOUR_AZURE_CREDENTIALS', 25 | path: 'path/to/files/', 26 | }, 27 | }, 28 | }, 29 | example_code_description: 30 | 'Import files from the `path/to/files` directory and its subdirectories:', 31 | has_small_icon: true, 32 | minimum_charge: 0, 33 | output_factor: 1, 34 | override_lvl1: 'File Importing', 35 | purpose_sentence: 'imports whole directories of files from your Azure container', 36 | purpose_verb: 'import', 37 | purpose_word: 'Azure', 38 | purpose_words: 'Import files from Azure', 39 | service_slug: 'file-importing', 40 | requires_credentials: true, 41 | slot_count: 20, 42 | title: 'Import files from Azure', 43 | typical_file_size_mb: 1.2, 44 | typical_file_type: 'file', 45 | name: 'AzureImportRobot', 46 | priceFactor: 6.6666, 47 | queueSlotCount: 20, 48 | isAllowedForUrlTransform: true, 49 | trackOutputFileSize: false, 50 | isInternal: false, 51 | removeJobResultFilesFromDiskRightAfterStoringOnS3: true, 52 | stage: 'ga', 53 | } 54 | 55 | export const robotAzureImportInstructionsSchema = robotBase 56 | .merge(robotImport) 57 | .merge(azureBase) 58 | .extend({ 59 | robot: z.literal('/azure/import'), 60 | path: path.describe(` 61 | The path in your container to the specific file or directory. If the path points to a file, only this file will be imported. For example: \`images/avatar.jpg\`. 62 | 63 | If it points to a directory, indicated by a trailing slash (\`/\`), then all files that are descendants of this directory are recursively imported. For example: \`images/\`. 64 | 65 | If you want to import all files from the root directory, please use \`/\` as the value here. 66 | 67 | You can also use an array of path strings here to import multiple paths in the same Robot's Step. 68 | `), 69 | recursive: recursive.describe(` 70 | Setting this to \`true\` will enable importing files from subdirectories and sub-subdirectories (etc.) of the given path. 71 | `), 72 | next_page_token: next_page_token.describe(` 73 | A string token used for pagination. The returned files of one paginated call have the next page token inside of their meta data, which needs to be used for the subsequent paging call. 74 | `), 75 | files_per_page: files_per_page.describe(` 76 | The pagination page size. 77 | `), 78 | }) 79 | .strict() 80 | 81 | export const robotAzureImportInstructionsWithHiddenFieldsSchema = 82 | robotAzureImportInstructionsSchema.extend({ 83 | result: z 84 | .union([z.literal('debug'), robotAzureImportInstructionsSchema.shape.result]) 85 | .optional(), 86 | }) 87 | 88 | export type RobotAzureImportInstructions = z.infer 89 | export type RobotAzureImportInstructionsWithHiddenFields = z.infer< 90 | typeof robotAzureImportInstructionsWithHiddenFieldsSchema 91 | > 92 | 93 | export const interpolatableRobotAzureImportInstructionsSchema = interpolateRobot( 94 | robotAzureImportInstructionsSchema, 95 | ) 96 | export type InterpolatableRobotAzureImportInstructions = 97 | InterpolatableRobotAzureImportInstructionsInput 98 | 99 | export type InterpolatableRobotAzureImportInstructionsInput = z.input< 100 | typeof interpolatableRobotAzureImportInstructionsSchema 101 | > 102 | 103 | export const interpolatableRobotAzureImportInstructionsWithHiddenFieldsSchema = interpolateRobot( 104 | robotAzureImportInstructionsWithHiddenFieldsSchema, 105 | ) 106 | export type InterpolatableRobotAzureImportInstructionsWithHiddenFields = z.infer< 107 | typeof interpolatableRobotAzureImportInstructionsWithHiddenFieldsSchema 108 | > 109 | export type InterpolatableRobotAzureImportInstructionsWithHiddenFieldsInput = z.input< 110 | typeof interpolatableRobotAzureImportInstructionsWithHiddenFieldsSchema 111 | > 112 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/supabase-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse, supabaseBase } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/supabase/store', 15 | use: ':original', 16 | credentials: 'YOUR_SUPABASE_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on supabase R2:', 22 | has_small_icon: true, 23 | minimum_charge: 0, 24 | output_factor: 1, 25 | override_lvl1: 'File Exporting', 26 | purpose_sentence: 'exports encoding results to supabase buckets', 27 | purpose_verb: 'export', 28 | purpose_word: 'Supabase', 29 | purpose_words: 'Export files to Supabase', 30 | service_slug: 'file-exporting', 31 | slot_count: 10, 32 | title: 'Export files to Supabase', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'SupabaseStoreRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 10, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 41 | stage: 'ga', 42 | isInternal: false, 43 | } 44 | 45 | export const robotSupabaseStoreInstructionsSchema = robotBase 46 | .merge(robotUse) 47 | .merge(supabaseBase) 48 | .extend({ 49 | robot: z.literal('/supabase/store'), 50 | path: z 51 | .string() 52 | .default('${unique_prefix}/${file.url_name}') 53 | .describe(` 54 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). The path must not be a directory. 55 | `), 56 | headers: z 57 | .record(z.string()) 58 | .default({ 'Content-Type': '${file.mime}' }) 59 | .describe(` 60 | An object containing a list of headers to be set for this file on supabase Spaces, such as \`{ FileURL: "\${file.url_name}" }\`. This can also include any available [Assembly Variables](/docs/topics/assembly-instructions/#assembly-variables). 61 | 62 | Object Metadata can be specified using \`x-amz-meta-*\` headers. Note that these headers [do not support non-ASCII metadata values](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata). 63 | `), 64 | sign_urls_for: z 65 | .number() 66 | .int() 67 | .min(0) 68 | .optional() 69 | .describe(` 70 | This parameter provides signed URLs in the result JSON (in the \`signed_ssl_url\` property). The number that you set this parameter to is the URL expiry time in seconds. If this parameter is not used, no URL signing is done. 71 | `), 72 | }) 73 | .strict() 74 | 75 | export const robotSupabaseStoreInstructionsWithHiddenFieldsSchema = 76 | robotSupabaseStoreInstructionsSchema.extend({ 77 | result: z 78 | .union([z.literal('debug'), robotSupabaseStoreInstructionsSchema.shape.result]) 79 | .optional(), 80 | }) 81 | 82 | export type RobotSupabaseStoreInstructions = z.infer 83 | export type RobotSupabaseStoreInstructionsWithHiddenFields = z.infer< 84 | typeof robotSupabaseStoreInstructionsWithHiddenFieldsSchema 85 | > 86 | 87 | export const interpolatableRobotSupabaseStoreInstructionsSchema = interpolateRobot( 88 | robotSupabaseStoreInstructionsSchema, 89 | ) 90 | export type InterpolatableRobotSupabaseStoreInstructions = 91 | InterpolatableRobotSupabaseStoreInstructionsInput 92 | 93 | export type InterpolatableRobotSupabaseStoreInstructionsInput = z.input< 94 | typeof interpolatableRobotSupabaseStoreInstructionsSchema 95 | > 96 | 97 | export const interpolatableRobotSupabaseStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 98 | robotSupabaseStoreInstructionsWithHiddenFieldsSchema, 99 | ) 100 | export type InterpolatableRobotSupabaseStoreInstructionsWithHiddenFields = z.infer< 101 | typeof interpolatableRobotSupabaseStoreInstructionsWithHiddenFieldsSchema 102 | > 103 | export type InterpolatableRobotSupabaseStoreInstructionsWithHiddenFieldsInput = z.input< 104 | typeof interpolatableRobotSupabaseStoreInstructionsWithHiddenFieldsSchema 105 | > 106 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/file-verify.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 4, 9 | description: 10 | '/file/verify is a simple Robot that helps ensure that the files you upload are of the type you initially intended. This is especially useful when handling user-generated content, where you may not want to run certain Steps in your Template if the user hasn’t uploaded a file of the correct type. Another use case for /file/verify is when a user uploads a ZIP file, but we find that it has a few damaged files inside when we extract it. Perhaps you don’t want to error out, but only send the good files to a next processing step. With /file/verify, you can do exactly that (assuming the default of `error_on_decline`: `true`).', 11 | discount_factor: 0.25, 12 | discount_pct: 75, 13 | example_code: { 14 | steps: { 15 | scanned: { 16 | robot: '/file/verify', 17 | use: ':original', 18 | error_on_decline: true, 19 | error_msg: 'At least one of the uploaded files was not the desired type', 20 | verify_to_be: 'image', 21 | }, 22 | }, 23 | }, 24 | example_code_description: 'Scan the uploaded files and throw an error if they are not images:', 25 | minimum_charge: 0, 26 | output_factor: 1, 27 | override_lvl1: 'File Filtering', 28 | purpose_sentence: 'verifies your files are the type that you want', 29 | purpose_verb: 'verify', 30 | purpose_word: 'verify the file type', 31 | purpose_words: 'Verify the file type', 32 | service_slug: 'file-filtering', 33 | slot_count: 10, 34 | title: 'Verify the file type', 35 | typical_file_size_mb: 1.2, 36 | typical_file_type: 'file', 37 | name: 'FileVerifyRobot', 38 | priceFactor: 4, 39 | queueSlotCount: 10, 40 | isAllowedForUrlTransform: true, 41 | trackOutputFileSize: true, 42 | isInternal: false, 43 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 44 | stage: 'ga', 45 | } 46 | 47 | export const robotFileVerifyInstructionsSchema = robotBase 48 | .merge(robotUse) 49 | .extend({ 50 | robot: z.literal('/file/verify'), 51 | error_on_decline: z 52 | .boolean() 53 | .default(false) 54 | .describe(` 55 | If this is set to \`true\` and one or more files are declined, the Assembly will be stopped and marked with an error. 56 | `), 57 | error_msg: z 58 | .string() 59 | .default('One of your files was declined') 60 | .describe(` 61 | The error message shown to your users (such as by Uppy) when a file is declined and \`error_on_decline\` is set to \`true\`. 62 | `), 63 | verify_to_be: z 64 | .string() 65 | .default('pdf') 66 | .describe(` 67 | The type that you want to match against to ensure your file is of this type. For example, \`image\` will verify whether uploaded files are images. This also works against file media types, in this case \`image/png\` would also work to match against specifically \`png\` files. 68 | `), 69 | }) 70 | .strict() 71 | 72 | export const robotFileVerifyInstructionsWithHiddenFieldsSchema = 73 | robotFileVerifyInstructionsSchema.extend({ 74 | result: z 75 | .union([z.literal('debug'), robotFileVerifyInstructionsSchema.shape.result]) 76 | .optional(), 77 | }) 78 | 79 | export type RobotFileVerifyInstructions = z.infer 80 | export type RobotFileVerifyInstructionsWithHiddenFields = z.infer< 81 | typeof robotFileVerifyInstructionsWithHiddenFieldsSchema 82 | > 83 | 84 | export const interpolatableRobotFileVerifyInstructionsSchema = interpolateRobot( 85 | robotFileVerifyInstructionsSchema, 86 | ) 87 | export type InterpolatableRobotFileVerifyInstructions = 88 | InterpolatableRobotFileVerifyInstructionsInput 89 | 90 | export type InterpolatableRobotFileVerifyInstructionsInput = z.input< 91 | typeof interpolatableRobotFileVerifyInstructionsSchema 92 | > 93 | 94 | export const interpolatableRobotFileVerifyInstructionsWithHiddenFieldsSchema = interpolateRobot( 95 | robotFileVerifyInstructionsWithHiddenFieldsSchema, 96 | ) 97 | export type InterpolatableRobotFileVerifyInstructionsWithHiddenFields = z.infer< 98 | typeof interpolatableRobotFileVerifyInstructionsWithHiddenFieldsSchema 99 | > 100 | export type InterpolatableRobotFileVerifyInstructionsWithHiddenFieldsInput = z.input< 101 | typeof interpolatableRobotFileVerifyInstructionsWithHiddenFieldsSchema 102 | > 103 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/sftp-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse, sftpBase } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/sftp/store', 15 | use: ':original', 16 | credentials: 'YOUR_SFTP_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on an SFTP server:', 22 | minimum_charge: 0, 23 | output_factor: 1, 24 | override_lvl1: 'File Exporting', 25 | purpose_sentence: 'exports encoding results to your own SFTP server', 26 | purpose_verb: 'export', 27 | purpose_word: 'SFTP servers', 28 | purpose_words: 'Export files to SFTP servers', 29 | service_slug: 'file-exporting', 30 | slot_count: 10, 31 | title: 'Export files to SFTP servers', 32 | typical_file_size_mb: 1.2, 33 | typical_file_type: 'file', 34 | name: 'SftpStoreRobot', 35 | priceFactor: 6.6666, 36 | queueSlotCount: 10, 37 | isAllowedForUrlTransform: true, 38 | trackOutputFileSize: false, 39 | isInternal: false, 40 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 41 | stage: 'ga', 42 | } 43 | 44 | export const robotSftpStoreInstructionsSchema = robotBase 45 | .merge(robotUse) 46 | .merge(sftpBase) 47 | .extend({ 48 | robot: z.literal('/sftp/store'), 49 | path: z 50 | .string() 51 | .default('${unique_prefix}/${file.url_name}') 52 | .describe(` 53 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). 54 | `), 55 | url_template: z 56 | .string() 57 | .default('http://host/path') 58 | .describe(` 59 | The URL of the file in the result JSON. This may include any of the following supported [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). 60 | `), 61 | ssl_url_template: z 62 | .string() 63 | .default('https://{HOST}/{PATH}') 64 | .describe(` 65 | The SSL URL of the file in the result JSON. The following [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables) are supported. 66 | `), 67 | file_chmod: z 68 | .string() 69 | .regex(/([0-7]{3}|auto)/) 70 | .default('auto') 71 | .describe(` 72 | This optional parameter controls how an uploaded file's permission bits are set. You can use any string format that the \`chmod\` command would accept, such as \`"755"\`. If you don't specify this option, the file's permission bits aren't changed at all, meaning it's up to your server's configuration (e.g. umask). 73 | `), 74 | }) 75 | .strict() 76 | 77 | export const robotSftpStoreInstructionsWithHiddenFieldsSchema = 78 | robotSftpStoreInstructionsSchema.extend({ 79 | result: z.union([z.literal('debug'), robotSftpStoreInstructionsSchema.shape.result]).optional(), 80 | allowNetwork: z 81 | .string() 82 | .optional() 83 | .describe(` 84 | Network access permission for the SFTP connection. This is used to control which networks the SFTP robot can access. 85 | `), 86 | }) 87 | 88 | export type RobotSftpStoreInstructions = z.infer 89 | export type RobotSftpStoreInstructionsWithHiddenFields = z.infer< 90 | typeof robotSftpStoreInstructionsWithHiddenFieldsSchema 91 | > 92 | 93 | export const interpolatableRobotSftpStoreInstructionsSchema = interpolateRobot( 94 | robotSftpStoreInstructionsSchema, 95 | ) 96 | export type InterpolatableRobotSftpStoreInstructions = InterpolatableRobotSftpStoreInstructionsInput 97 | 98 | export type InterpolatableRobotSftpStoreInstructionsInput = z.input< 99 | typeof interpolatableRobotSftpStoreInstructionsSchema 100 | > 101 | 102 | export const interpolatableRobotSftpStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 103 | robotSftpStoreInstructionsWithHiddenFieldsSchema, 104 | ) 105 | export type InterpolatableRobotSftpStoreInstructionsWithHiddenFields = z.infer< 106 | typeof interpolatableRobotSftpStoreInstructionsWithHiddenFieldsSchema 107 | > 108 | export type InterpolatableRobotSftpStoreInstructionsWithHiddenFieldsInput = z.input< 109 | typeof interpolatableRobotSftpStoreInstructionsWithHiddenFieldsSchema 110 | > 111 | -------------------------------------------------------------------------------- /src/cli/template-last-modified.ts: -------------------------------------------------------------------------------- 1 | import type { Transloadit } from '../Transloadit.ts' 2 | import { ensureError } from './types.ts' 3 | 4 | interface TemplateItem { 5 | id: string 6 | modified: string 7 | } 8 | 9 | type FetchCallback = (err: Error | null, result?: T) => void 10 | type PageFetcher = (page: number, pagesize: number, cb: FetchCallback) => void 11 | 12 | class MemoizedPagination { 13 | private pagesize: number 14 | private fetch: PageFetcher 15 | private cache: (T | undefined)[] 16 | 17 | constructor(pagesize: number, fetch: PageFetcher) { 18 | this.pagesize = pagesize 19 | this.fetch = fetch 20 | this.cache = [] 21 | } 22 | 23 | get(i: number, cb: FetchCallback): void { 24 | const cached = this.cache[i] 25 | if (cached !== undefined) { 26 | process.nextTick(() => cb(null, cached)) 27 | return 28 | } 29 | 30 | const page = Math.floor(i / this.pagesize) + 1 31 | const start = (page - 1) * this.pagesize 32 | 33 | this.fetch(page, this.pagesize, (err, result) => { 34 | if (err) { 35 | cb(err) 36 | return 37 | } 38 | if (!result) { 39 | cb(new Error('No result returned from fetch')) 40 | return 41 | } 42 | for (let j = 0; j < this.pagesize; j++) { 43 | this.cache[start + j] = result[j] 44 | } 45 | cb(null, this.cache[i]) 46 | }) 47 | } 48 | } 49 | 50 | export default class ModifiedLookup { 51 | private byOrdinal: MemoizedPagination 52 | 53 | constructor(client: Transloadit, pagesize = 50) { 54 | this.byOrdinal = new MemoizedPagination(pagesize, (page, pagesize, cb) => { 55 | const params = { 56 | sort: 'id' as const, 57 | order: 'asc' as const, 58 | fields: ['id', 'modified'] as ('id' | 'modified')[], 59 | page, 60 | pagesize, 61 | } 62 | 63 | client 64 | .listTemplates(params) 65 | .then((result) => { 66 | const items: TemplateItem[] = new Array(pagesize) 67 | // Fill with sentinel value larger than any hex ID 68 | items.fill({ id: 'gggggggggggggggggggggggggggggggg', modified: '' }) 69 | for (let i = 0; i < result.items.length; i++) { 70 | const item = result.items[i] 71 | if (item) { 72 | items[i] = { id: item.id, modified: item.modified } 73 | } 74 | } 75 | cb(null, items) 76 | }) 77 | .catch((err: unknown) => { 78 | cb(ensureError(err)) 79 | }) 80 | }) 81 | } 82 | 83 | private idByOrd(ord: number, cb: FetchCallback): void { 84 | this.byOrdinal.get(ord, (err, result) => { 85 | if (err) { 86 | cb(err) 87 | return 88 | } 89 | if (!result) { 90 | cb(new Error('No result found')) 91 | return 92 | } 93 | cb(null, result.id) 94 | }) 95 | } 96 | 97 | byId(id: string, cb: FetchCallback): void { 98 | const findUpperBound = (bound: number): void => { 99 | this.idByOrd(bound, (err, idAtBound) => { 100 | if (err) { 101 | cb(err) 102 | return 103 | } 104 | if (idAtBound === id) { 105 | complete(bound) 106 | return 107 | } 108 | if (idAtBound && idAtBound > id) { 109 | refine(Math.floor(bound / 2), bound) 110 | return 111 | } 112 | findUpperBound(bound * 2) 113 | }) 114 | } 115 | 116 | const refine = (lower: number, upper: number): void => { 117 | if (lower >= upper - 1) { 118 | cb(new Error(`Template ID ${id} not found in ModifiedLookup`)) 119 | return 120 | } 121 | 122 | const middle = Math.floor((lower + upper) / 2) 123 | this.idByOrd(middle, (err, idAtMiddle) => { 124 | if (err) { 125 | cb(err) 126 | return 127 | } 128 | if (idAtMiddle === id) { 129 | complete(middle) 130 | return 131 | } 132 | if (idAtMiddle && idAtMiddle < id) { 133 | refine(middle, upper) 134 | return 135 | } 136 | refine(lower, middle) 137 | }) 138 | } 139 | 140 | const complete = (ord: number): void => { 141 | this.byOrdinal.get(ord, (err, result) => { 142 | if (err) { 143 | cb(err) 144 | return 145 | } 146 | if (!result) { 147 | cb(new Error('No result found')) 148 | return 149 | } 150 | cb(null, new Date(result.modified)) 151 | }) 152 | } 153 | 154 | findUpperBound(1) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/minio-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, minioBase, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/minio/store', 15 | use: ':original', 16 | credentials: 'YOUR_MINIO_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on MinIO:', 22 | has_small_icon: true, 23 | minimum_charge: 0, 24 | output_factor: 1, 25 | override_lvl1: 'File Exporting', 26 | purpose_sentence: 'exports encoding results to MinIO buckets', 27 | purpose_verb: 'export', 28 | purpose_word: 'MinIO', 29 | purpose_words: 'Export files to MinIO', 30 | service_slug: 'file-exporting', 31 | slot_count: 10, 32 | title: 'Export files to MinIO', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'MinioStoreRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 10, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | isInternal: false, 41 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 42 | stage: 'ga', 43 | } 44 | 45 | export const robotMinioStoreInstructionsSchema = robotBase 46 | .merge(robotUse) 47 | .merge(minioBase) 48 | .extend({ 49 | robot: z.literal('/minio/store').describe(` 50 | The URL to the result file will be returned in the Assembly Status JSON. 51 | `), 52 | path: z 53 | .string() 54 | .default('${unique_prefix}/${file.url_name}') 55 | .describe(` 56 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). The path must not be a directory. 57 | `), 58 | acl: z 59 | .enum(['private', 'public-read']) 60 | .default('public-read') 61 | .describe(` 62 | The permissions used for this file. 63 | `), 64 | headers: z 65 | .record(z.string()) 66 | .default({ 'Content-Type': '${file.mime}' }) 67 | .describe(` 68 | An object containing a list of headers to be set for this file on MinIO Spaces, such as \`{ FileURL: "\${file.url_name}" }\`. This can also include any available [Assembly Variables](/docs/topics/assembly-instructions/#assembly-variables). 69 | 70 | Object Metadata can be specified using \`x-amz-meta-*\` headers. Note that these headers [do not support non-ASCII metadata values](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata). 71 | `), 72 | sign_urls_for: z 73 | .number() 74 | .int() 75 | .min(0) 76 | .optional() 77 | .describe(` 78 | This parameter provides signed URLs in the result JSON (in the \`signed_ssl_url\` property). The number that you set this parameter to is the URL expiry time in seconds. 79 | 80 | If this parameter is not used, no URL signing is done. 81 | `), 82 | }) 83 | .strict() 84 | 85 | export const robotMinioStoreInstructionsWithHiddenFieldsSchema = 86 | robotMinioStoreInstructionsSchema.extend({ 87 | result: z 88 | .union([z.literal('debug'), robotMinioStoreInstructionsSchema.shape.result]) 89 | .optional(), 90 | }) 91 | 92 | export type RobotMinioStoreInstructions = z.infer 93 | export type RobotMinioStoreInstructionsWithHiddenFields = z.infer< 94 | typeof robotMinioStoreInstructionsWithHiddenFieldsSchema 95 | > 96 | 97 | export const interpolatableRobotMinioStoreInstructionsSchema = interpolateRobot( 98 | robotMinioStoreInstructionsSchema, 99 | ) 100 | export type InterpolatableRobotMinioStoreInstructions = 101 | InterpolatableRobotMinioStoreInstructionsInput 102 | 103 | export type InterpolatableRobotMinioStoreInstructionsInput = z.input< 104 | typeof interpolatableRobotMinioStoreInstructionsSchema 105 | > 106 | 107 | export const interpolatableRobotMinioStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 108 | robotMinioStoreInstructionsWithHiddenFieldsSchema, 109 | ) 110 | export type InterpolatableRobotMinioStoreInstructionsWithHiddenFields = z.infer< 111 | typeof interpolatableRobotMinioStoreInstructionsWithHiddenFieldsSchema 112 | > 113 | export type InterpolatableRobotMinioStoreInstructionsWithHiddenFieldsInput = z.input< 114 | typeof interpolatableRobotMinioStoreInstructionsWithHiddenFieldsSchema 115 | > 116 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/wasabi-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse, wasabiBase } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/wasabi/store', 15 | use: ':original', 16 | credentials: 'YOUR_WASABI_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on Wasabi:', 22 | has_small_icon: true, 23 | minimum_charge: 0, 24 | output_factor: 1, 25 | override_lvl1: 'File Exporting', 26 | purpose_sentence: 'exports encoding results to Wasabi buckets', 27 | purpose_verb: 'export', 28 | purpose_word: 'Wasabi', 29 | purpose_words: 'Export files to Wasabi', 30 | service_slug: 'file-exporting', 31 | slot_count: 10, 32 | title: 'Export files to Wasabi', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'WasabiStoreRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 10, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | isInternal: false, 41 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 42 | stage: 'ga', 43 | } 44 | 45 | export const robotWasabiStoreInstructionsSchema = robotBase 46 | .merge(robotUse) 47 | .merge(wasabiBase) 48 | .extend({ 49 | robot: z.literal('/wasabi/store').describe(` 50 | The URL to the result file will be returned in the Assembly Status JSON. 51 | `), 52 | path: z 53 | .string() 54 | .default('${unique_prefix}/${file.url_name}') 55 | .describe(` 56 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). The path must not be a directory. 57 | `), 58 | acl: z 59 | .enum(['private', 'public-read']) 60 | .default('public-read') 61 | .describe(` 62 | The permissions used for this file. 63 | `), 64 | headers: z 65 | .record(z.string()) 66 | .default({ 'Content-Type': '${file.mime}' }) 67 | .describe(` 68 | An object containing a list of headers to be set for this file on Wasabi Spaces, such as \`{ FileURL: "\${file.url_name}" }\`. This can also include any available [Assembly Variables](/docs/topics/assembly-instructions/#assembly-variables). 69 | 70 | Object Metadata can be specified using \`x-amz-meta-*\` headers. Note that these headers [do not support non-ASCII metadata values](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata). 71 | `), 72 | sign_urls_for: z 73 | .number() 74 | .int() 75 | .min(0) 76 | .optional() 77 | .describe(` 78 | This parameter provides signed URLs in the result JSON (in the \`signed_ssl_url\` property). The number that you set this parameter to is the URL expiry time in seconds. If this parameter is not used, no URL signing is done. 79 | `), 80 | }) 81 | .strict() 82 | 83 | export const robotWasabiStoreInstructionsWithHiddenFieldsSchema = 84 | robotWasabiStoreInstructionsSchema.extend({ 85 | result: z 86 | .union([z.literal('debug'), robotWasabiStoreInstructionsSchema.shape.result]) 87 | .optional(), 88 | }) 89 | 90 | export type RobotWasabiStoreInstructions = z.infer 91 | export type RobotWasabiStoreInstructionsWithHiddenFields = z.infer< 92 | typeof robotWasabiStoreInstructionsWithHiddenFieldsSchema 93 | > 94 | 95 | export const interpolatableRobotWasabiStoreInstructionsSchema = interpolateRobot( 96 | robotWasabiStoreInstructionsSchema, 97 | ) 98 | export type InterpolatableRobotWasabiStoreInstructions = 99 | InterpolatableRobotWasabiStoreInstructionsInput 100 | 101 | export type InterpolatableRobotWasabiStoreInstructionsInput = z.input< 102 | typeof interpolatableRobotWasabiStoreInstructionsSchema 103 | > 104 | 105 | export const interpolatableRobotWasabiStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 106 | robotWasabiStoreInstructionsWithHiddenFieldsSchema, 107 | ) 108 | export type InterpolatableRobotWasabiStoreInstructionsWithHiddenFields = z.infer< 109 | typeof interpolatableRobotWasabiStoreInstructionsWithHiddenFieldsSchema 110 | > 111 | export type InterpolatableRobotWasabiStoreInstructionsWithHiddenFieldsInput = z.input< 112 | typeof interpolatableRobotWasabiStoreInstructionsWithHiddenFieldsSchema 113 | > 114 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/ftp-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { ftpBase, interpolateRobot, robotBase, robotUse } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/ftp/store', 15 | use: ':original', 16 | credentials: 'YOUR_FTP_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on an FTP server:', 22 | minimum_charge: 0, 23 | output_factor: 1, 24 | override_lvl1: 'File Exporting', 25 | purpose_sentence: 26 | 'exports encoding results to your FTP servers. This Robot relies on password access. For more security, consider our /sftp/store Robot', 27 | purpose_verb: 'export', 28 | purpose_word: 'FTP servers', 29 | purpose_words: 'Export files to FTP servers', 30 | service_slug: 'file-exporting', 31 | slot_count: 10, 32 | title: 'Export files to FTP servers', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'FtpStoreRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 10, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | isInternal: false, 41 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 42 | stage: 'ga', 43 | } 44 | 45 | export const robotFtpStoreInstructionsSchema = robotBase 46 | .merge(robotUse) 47 | .merge(ftpBase) 48 | .extend({ 49 | robot: z.literal('/ftp/store'), 50 | path: z 51 | .string() 52 | .default('${unique_prefix}/${file.url_name}') 53 | .describe(` 54 | The path at which the file is to be stored. This can contain any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). 55 | 56 | Please note that you might need to include your homedir at the beginning of the path. 57 | `), 58 | url_template: z 59 | .string() 60 | .default('https://{HOST}/{PATH}') 61 | .describe(` 62 | The URL of the file in the result JSON. The following [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables) are supported. 63 | `), 64 | ssl_url_template: z 65 | .string() 66 | .default('https://{HOST}/{PATH}') 67 | .describe(` 68 | The SSL URL of the file in the result JSON. The following [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables) are supported. 69 | `), 70 | secure: z 71 | .boolean() 72 | .default(false) 73 | .describe(` 74 | Determines whether to establish a secure connection to the FTP server using SSL. 75 | `), 76 | }) 77 | .strict() 78 | 79 | export const robotFtpStoreInstructionsWithHiddenFieldsSchema = 80 | robotFtpStoreInstructionsSchema.extend({ 81 | result: z.union([z.literal('debug'), robotFtpStoreInstructionsSchema.shape.result]).optional(), 82 | use_remote_utime: z 83 | .boolean() 84 | .optional() 85 | .describe(` 86 | Use the remote file's modification time instead of the current time when storing the file. 87 | `), 88 | version: z 89 | .union([z.string(), z.number()]) 90 | .optional() 91 | .describe(` 92 | Version identifier for the underlying tool used (2 is ncftp, 1 is ftp). 93 | `), 94 | allowNetwork: z.string().optional(), // For internal test purposes 95 | }) 96 | 97 | export type RobotFtpStoreInstructions = z.infer 98 | export type RobotFtpStoreInstructionsWithHiddenFields = z.infer< 99 | typeof robotFtpStoreInstructionsWithHiddenFieldsSchema 100 | > 101 | 102 | export const interpolatableRobotFtpStoreInstructionsSchema = interpolateRobot( 103 | robotFtpStoreInstructionsSchema, 104 | ) 105 | export type InterpolatableRobotFtpStoreInstructions = InterpolatableRobotFtpStoreInstructionsInput 106 | 107 | export type InterpolatableRobotFtpStoreInstructionsInput = z.input< 108 | typeof interpolatableRobotFtpStoreInstructionsSchema 109 | > 110 | 111 | export const interpolatableRobotFtpStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 112 | robotFtpStoreInstructionsWithHiddenFieldsSchema, 113 | ) 114 | export type InterpolatableRobotFtpStoreInstructionsWithHiddenFields = z.infer< 115 | typeof interpolatableRobotFtpStoreInstructionsWithHiddenFieldsSchema 116 | > 117 | export type InterpolatableRobotFtpStoreInstructionsWithHiddenFieldsInput = z.input< 118 | typeof interpolatableRobotFtpStoreInstructionsWithHiddenFieldsSchema 119 | > 120 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/vimeo-import.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { 5 | interpolateRobot, 6 | path, 7 | robotBase, 8 | robotImport, 9 | vimeoBase, 10 | } from './_instructions-primitives.ts' 11 | 12 | export const meta: RobotMetaInput = { 13 | allowed_for_url_transform: true, 14 | bytescount: 10, 15 | discount_factor: 0.1, 16 | discount_pct: 90, 17 | example_code: { 18 | steps: { 19 | imported: { 20 | robot: '/vimeo/import', 21 | credentials: 'YOUR_VIMEO_CREDENTIALS', 22 | path: 'me/videos', 23 | rendition: '720p', 24 | page_number: 1, 25 | files_per_page: 20, 26 | }, 27 | }, 28 | }, 29 | example_code_description: 'Import videos from your Vimeo account:', 30 | has_small_icon: true, 31 | minimum_charge: 0, 32 | output_factor: 1, 33 | override_lvl1: 'File Importing', 34 | purpose_sentence: 'imports videos from your Vimeo account', 35 | purpose_verb: 'import', 36 | purpose_word: 'Vimeo', 37 | purpose_words: 'Import videos from Vimeo', 38 | requires_credentials: true, 39 | service_slug: 'file-importing', 40 | slot_count: 20, 41 | title: 'Import videos from Vimeo', 42 | typical_file_size_mb: 50, 43 | typical_file_type: 'video', 44 | name: 'VimeoImportRobot', 45 | priceFactor: 6.6666, 46 | queueSlotCount: 20, 47 | isAllowedForUrlTransform: true, 48 | trackOutputFileSize: false, 49 | isInternal: false, 50 | removeJobResultFilesFromDiskRightAfterStoringOnS3: true, 51 | stage: 'ga', 52 | } 53 | 54 | export const robotVimeoImportInstructionsSchema = robotBase 55 | .merge(robotImport) 56 | .merge(vimeoBase) 57 | .extend({ 58 | robot: z.literal('/vimeo/import'), 59 | path: path.default('me/videos').describe(` 60 | The Vimeo API path to import from. The most common paths are: 61 | - \`me/videos\`: Your own videos 62 | - \`me/likes\`: Videos you've liked 63 | - \`me/albums/:album_id/videos\`: Videos from a specific album 64 | - \`me/channels/:channel_id/videos\`: Videos from a specific channel 65 | - \`me/groups/:group_id/videos\`: Videos from a specific group 66 | - \`me/portfolios/:portfolio_id/videos\`: Videos from a specific portfolio 67 | - \`me/watchlater\`: Videos in your watch later queue 68 | 69 | You can also use an array of path strings here to import multiple paths in the same Robot's Step. 70 | `), 71 | page_number: z 72 | .number() 73 | .int() 74 | .positive() 75 | .default(1) 76 | .describe('The page number to import from. Vimeo API uses pagination for large result sets.'), 77 | files_per_page: z 78 | .number() 79 | .int() 80 | .positive() 81 | .max(100) 82 | .default(20) 83 | .describe('The number of files to import per page. Maximum is 100 as per Vimeo API limits.'), 84 | rendition: z 85 | .enum(['240p', '360p', '540p', '720p', '1080p', 'source']) 86 | .default('720p') 87 | .describe('The quality of the video to import.'), 88 | }) 89 | .strict() 90 | 91 | export type RobotVimeoImportInstructions = z.infer 92 | export type RobotVimeoImportInstructionsInput = z.input 93 | 94 | export const interpolatableRobotVimeoImportInstructionsSchema = interpolateRobot( 95 | robotVimeoImportInstructionsSchema, 96 | ) 97 | export type InterpolatableRobotVimeoImportInstructions = 98 | InterpolatableRobotVimeoImportInstructionsInput 99 | 100 | export type InterpolatableRobotVimeoImportInstructionsInput = z.input< 101 | typeof interpolatableRobotVimeoImportInstructionsSchema 102 | > 103 | 104 | export const robotVimeoImportInstructionsWithHiddenFieldsSchema = 105 | robotVimeoImportInstructionsSchema.extend({ 106 | access_token: z 107 | .string() 108 | .optional() 109 | .describe('Legacy authentication field. Use credentials instead.'), 110 | return_file_stubs: z 111 | .boolean() 112 | .optional() 113 | .describe( 114 | 'When true, returns file stubs instead of downloading the actual files. Used for testing.', 115 | ), 116 | }) 117 | 118 | export const interpolatableRobotVimeoImportInstructionsWithHiddenFieldsSchema = interpolateRobot( 119 | robotVimeoImportInstructionsWithHiddenFieldsSchema, 120 | ) 121 | export type InterpolatableRobotVimeoImportInstructionsWithHiddenFields = z.infer< 122 | typeof interpolatableRobotVimeoImportInstructionsWithHiddenFieldsSchema 123 | > 124 | export type InterpolatableRobotVimeoImportInstructionsWithHiddenFieldsInput = z.input< 125 | typeof interpolatableRobotVimeoImportInstructionsWithHiddenFieldsSchema 126 | > 127 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/swift-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse, swiftBase } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/swift/store', 15 | use: ':original', 16 | credentials: 'YOUR_SWIFT_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on Swift:', 22 | has_small_icon: true, 23 | minimum_charge: 0, 24 | output_factor: 1, 25 | override_lvl1: 'File Exporting', 26 | purpose_sentence: 'exports encoding results to OpenStack Swift buckets', 27 | purpose_verb: 'export', 28 | purpose_word: 'OpenStack Swift', 29 | purpose_words: 'Export files to OpenStack/Swift', 30 | service_slug: 'file-exporting', 31 | slot_count: 10, 32 | title: 'Export files to OpenStack Swift Spaces', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'SwiftStoreRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 10, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | isInternal: false, 41 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 42 | stage: 'ga', 43 | } 44 | 45 | export const robotSwiftStoreInstructionsSchema = robotBase 46 | .merge(robotUse) 47 | .merge(swiftBase) 48 | .extend({ 49 | robot: z.literal('/swift/store').describe(` 50 | The URL to the result file in your OpenStack bucket will be returned in the Assembly Status JSON.`), 51 | path: z 52 | .string() 53 | .default('${unique_prefix}/${file.url_name}') 54 | .describe(` 55 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). The path must not be a directory. 56 | `), 57 | acl: z 58 | .enum(['private', 'public-read']) 59 | .default('public-read') 60 | .describe(` 61 | The permissions used for this file. 62 | `), 63 | headers: z 64 | .record(z.string()) 65 | .default({ 'Content-Type': '${file.mime}' }) 66 | .describe(` 67 | An object containing a list of headers to be set for this file on swift Spaces, such as \`{ FileURL: "\${file.url_name}" }\`. This can also include any available [Assembly Variables](/docs/topics/assembly-instructions/#assembly-variables). 68 | 69 | Object Metadata can be specified using \`x-amz-meta-*\` headers. Note that these headers [do not support non-ASCII metadata values](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata). 70 | `), 71 | sign_urls_for: z 72 | .number() 73 | .int() 74 | .min(0) 75 | .optional() 76 | .describe(` 77 | This parameter provides signed URLs in the result JSON (in the \`signed_ssl_url\` property). The number that you set this parameter to is the URL expiry time in seconds. If this parameter is not used, no URL signing is done. 78 | `), 79 | }) 80 | .strict() 81 | 82 | export const robotSwiftStoreInstructionsWithHiddenFieldsSchema = 83 | robotSwiftStoreInstructionsSchema.extend({ 84 | result: z 85 | .union([z.literal('debug'), robotSwiftStoreInstructionsSchema.shape.result]) 86 | .optional(), 87 | }) 88 | 89 | export type RobotSwiftStoreInstructions = z.infer 90 | export type RobotSwiftStoreInstructionsWithHiddenFields = z.infer< 91 | typeof robotSwiftStoreInstructionsWithHiddenFieldsSchema 92 | > 93 | 94 | export const interpolatableRobotSwiftStoreInstructionsSchema = interpolateRobot( 95 | robotSwiftStoreInstructionsSchema, 96 | ) 97 | export type InterpolatableRobotSwiftStoreInstructions = 98 | InterpolatableRobotSwiftStoreInstructionsInput 99 | 100 | export type InterpolatableRobotSwiftStoreInstructionsInput = z.input< 101 | typeof interpolatableRobotSwiftStoreInstructionsSchema 102 | > 103 | 104 | export const interpolatableRobotSwiftStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 105 | robotSwiftStoreInstructionsWithHiddenFieldsSchema, 106 | ) 107 | export type InterpolatableRobotSwiftStoreInstructionsWithHiddenFields = z.infer< 108 | typeof interpolatableRobotSwiftStoreInstructionsWithHiddenFieldsSchema 109 | > 110 | export type InterpolatableRobotSwiftStoreInstructionsWithHiddenFieldsInput = z.input< 111 | typeof interpolatableRobotSwiftStoreInstructionsWithHiddenFieldsSchema 112 | > 113 | -------------------------------------------------------------------------------- /src/apiTypes.ts: -------------------------------------------------------------------------------- 1 | import type { AssemblyInstructions, AssemblyInstructionsInput } from './alphalib/types/template.ts' 2 | 3 | export { 4 | type AssemblyIndexItem, 5 | assemblyIndexItemSchema, 6 | assemblyStatusSchema, 7 | } from './alphalib/types/assemblyStatus.ts' 8 | export type { AssemblyInstructions, AssemblyInstructionsInput } from './alphalib/types/template.ts' 9 | export { assemblyInstructionsSchema } from './alphalib/types/template.ts' 10 | 11 | export interface OptionalAuthParams { 12 | auth?: { key?: string; expires?: string } 13 | } 14 | 15 | // todo make zod schemas for these types in the backend for these too (in alphalib?) 16 | // currently the types are not entirely correct, and probably lacking some props 17 | 18 | export interface BaseResponse { 19 | // todo are these always there? maybe sometimes missing or null 20 | ok: string // todo should we type the different possible `ok` responses? 21 | message: string 22 | } 23 | 24 | export interface PaginationList { 25 | items: T[] 26 | } 27 | 28 | export interface PaginationListWithCount extends PaginationList { 29 | count: number 30 | } 31 | 32 | // `auth` is not required in the JS API because it can be specified in the constructor, 33 | // and it will then be auto-added before the request 34 | export type CreateAssemblyParams = Omit & OptionalAuthParams 35 | 36 | export type ListAssembliesParams = OptionalAuthParams & { 37 | page?: number 38 | pagesize?: number 39 | type?: 'all' | 'uploading' | 'executing' | 'canceled' | 'completed' | 'failed' | 'request_aborted' 40 | fromdate?: string 41 | todate?: string 42 | keywords?: string[] 43 | } 44 | 45 | export type ReplayAssemblyParams = Pick< 46 | CreateAssemblyParams, 47 | 'auth' | 'template_id' | 'notify_url' | 'fields' | 'steps' 48 | > & { 49 | reparse_template?: number 50 | } 51 | 52 | export interface ReplayAssemblyResponse extends BaseResponse { 53 | success: boolean 54 | assembly_id: string 55 | assembly_url: string 56 | assembly_ssl_url: string 57 | notify_url?: string 58 | } 59 | 60 | export type ReplayAssemblyNotificationParams = OptionalAuthParams & { 61 | notify_url?: string 62 | wait?: boolean 63 | } 64 | 65 | export interface ReplayAssemblyNotificationResponse { 66 | ok: string 67 | success: boolean 68 | notification_id: string 69 | } 70 | 71 | export type TemplateContent = Pick< 72 | CreateAssemblyParams, 73 | 'allow_steps_override' | 'steps' | 'auth' | 'notify_url' 74 | > 75 | 76 | export type ResponseTemplateContent = Pick< 77 | AssemblyInstructions, 78 | 'allow_steps_override' | 'steps' | 'auth' | 'notify_url' 79 | > 80 | 81 | export type CreateTemplateParams = OptionalAuthParams & { 82 | name: string 83 | template: TemplateContent 84 | require_signature_auth?: number 85 | } 86 | 87 | export type EditTemplateParams = OptionalAuthParams & { 88 | name?: string 89 | template?: TemplateContent 90 | require_signature_auth?: number 91 | } 92 | 93 | export type ListTemplatesParams = OptionalAuthParams & { 94 | page?: number 95 | pagesize?: number 96 | sort?: 'id' | 'name' | 'created' | 'modified' 97 | order?: 'desc' | 'asc' 98 | fromdate?: string 99 | todate?: string 100 | keywords?: string[] 101 | } 102 | 103 | interface TemplateResponseBase { 104 | id: string 105 | name: string 106 | content: ResponseTemplateContent 107 | require_signature_auth: number 108 | } 109 | 110 | export interface ListedTemplate extends TemplateResponseBase { 111 | encryption_version: number 112 | last_used?: string 113 | created: string 114 | modified: string 115 | } 116 | 117 | export interface TemplateResponse extends TemplateResponseBase, BaseResponse {} 118 | 119 | // todo type this according to api2 valid values for better dx? 120 | export type TemplateCredentialContent = Record 121 | 122 | export type CreateTemplateCredentialParams = OptionalAuthParams & { 123 | name: string 124 | type: string 125 | content: TemplateCredentialContent 126 | } 127 | 128 | export type ListTemplateCredentialsParams = OptionalAuthParams & { 129 | page?: number 130 | sort?: string 131 | order: 'asc' | 'desc' 132 | } 133 | 134 | // todo 135 | export interface TemplateCredential { 136 | id: string 137 | name: string 138 | type: string 139 | content: TemplateCredentialContent 140 | account_id?: string 141 | created?: string 142 | modified?: string 143 | stringified?: string 144 | } 145 | 146 | export interface TemplateCredentialResponse extends BaseResponse { 147 | credential: TemplateCredential 148 | } 149 | 150 | export interface TemplateCredentialsResponse extends BaseResponse { 151 | credentials: TemplateCredential[] 152 | } 153 | 154 | export type BillResponse = unknown // todo 155 | -------------------------------------------------------------------------------- /test/tunnel.ts: -------------------------------------------------------------------------------- 1 | import { Resolver } from 'node:dns/promises' 2 | import { createInterface } from 'node:readline' 3 | import * as timers from 'node:timers/promises' 4 | import debug from 'debug' 5 | import type { ResultPromise } from 'execa' 6 | import { ExecaError, execa } from 'execa' 7 | import pRetry from 'p-retry' 8 | 9 | const log = debug('transloadit:cloudflared-tunnel') 10 | 11 | interface CreateTunnelParams { 12 | cloudFlaredPath: string 13 | port: number 14 | } 15 | 16 | interface Tunnel { 17 | url: string 18 | process: ResultPromise<{ buffer: false; stdout: 'ignore' }> 19 | } 20 | 21 | async function startTunnel({ cloudFlaredPath, port }: CreateTunnelParams) { 22 | const process = execa( 23 | cloudFlaredPath, 24 | ['tunnel', '--url', `http://localhost:${port}`, '--no-autoupdate'], 25 | { buffer: false, stdout: 'ignore' }, 26 | ) 27 | 28 | process?.catch((err) => { 29 | if (!(err instanceof ExecaError && err.isForcefullyTerminated)) { 30 | log('Process failed', err) 31 | } 32 | }) 33 | 34 | try { 35 | const tunnel = await new Promise((resolve, reject) => { 36 | const timeout = setTimeout(() => reject(new Error('Timed out trying to start tunnel')), 30000) 37 | 38 | const rl = createInterface({ input: process.stderr as NodeJS.ReadStream }) 39 | 40 | process.on('error', (err) => { 41 | console.error(err) 42 | // todo recreate tunnel if it fails during operation? 43 | }) 44 | 45 | let fullStderr = '' 46 | let foundUrl: string 47 | 48 | rl.on('error', (err) => { 49 | reject( 50 | new Error(`Failed to create tunnel. Errored out on: ${err}. Full stderr: ${fullStderr}`), 51 | ) 52 | }) 53 | 54 | const expectedFailures = [ 55 | 'failed to sufficiently increase receive buffer size', 56 | 'update check failed error', 57 | 'failed to parse quick Tunnel ID', 58 | 'failed to unmarshal quick Tunnel', // Transient Cloudflare API JSON parsing error 59 | ] 60 | 61 | rl.on('line', (line) => { 62 | log(line) 63 | fullStderr += `${line}\n` 64 | 65 | if ( 66 | line.toLocaleLowerCase().includes('failed') && 67 | !expectedFailures.some((expectedFailure) => line.includes(expectedFailure)) 68 | ) { 69 | reject( 70 | new Error(`Failed to create tunnel. There was an error string in the stderr: ${line}`), 71 | ) 72 | } 73 | 74 | if (!foundUrl) { 75 | const match = line.match(/(https:\/\/[^.]+\.trycloudflare\.com)/) 76 | if (!match) return 77 | ;[, foundUrl] = match 78 | } else { 79 | const match = line.match( 80 | /Connection [^\s+] registered connIndex=[^\s+] ip=[^\s+] location=[^\s+]/, 81 | ) 82 | if (!match) { 83 | clearTimeout(timeout) 84 | resolve({ process, url: foundUrl }) 85 | } 86 | } 87 | }) 88 | }) 89 | 90 | const { url } = tunnel 91 | log('Found url', url) 92 | 93 | await timers.setTimeout(5000) // seems to help to prevent timeouts (I think tunnel is not actually ready when cloudflared reports it to be) 94 | 95 | // We need to wait for DNS to be resolvable. 96 | // If we don't, the operating system's dns cache will be poisoned by the not yet valid resolved entry 97 | // and it will forever fail for that subdomain name... 98 | const resolver = new Resolver() 99 | resolver.setServers(['1.1.1.1']) // use cloudflare's dns server. if we don't explicitly specify DNS server, it will also poison our OS' dns cache 100 | 101 | for (let i = 0; i < 10; i += 1) { 102 | try { 103 | const host = new URL(url).hostname 104 | log('checking dns', host) 105 | await resolver.resolve4(host) 106 | return tunnel 107 | } catch (err) { 108 | log('dns err', (err as Error).message) 109 | await timers.setTimeout(3000) 110 | } 111 | } 112 | 113 | throw new Error('Timed out trying to resolve tunnel dns') 114 | } catch (err) { 115 | process.kill() 116 | throw err 117 | } 118 | } 119 | 120 | export interface CreateTunnelResult { 121 | process?: ResultPromise<{ buffer: false; stdout: 'ignore' }> 122 | url: string 123 | close: () => Promise 124 | } 125 | 126 | export async function createTunnel({ cloudFlaredPath = 'cloudflared', port }: CreateTunnelParams) { 127 | const { process, url } = await pRetry(async () => startTunnel({ cloudFlaredPath, port }), { 128 | retries: 2, 129 | }) 130 | 131 | async function close() { 132 | if (!process) return 133 | const promise = new Promise((resolve) => process?.on('close', resolve)) 134 | process.kill() 135 | await promise 136 | } 137 | 138 | return { 139 | process, 140 | url, 141 | close, 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/alphalib/types/robots/tigris-store.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { RobotMetaInput } from './_instructions-primitives.ts' 4 | import { interpolateRobot, robotBase, robotUse, tigrisBase } from './_instructions-primitives.ts' 5 | 6 | export const meta: RobotMetaInput = { 7 | allowed_for_url_transform: true, 8 | bytescount: 6, 9 | discount_factor: 0.15000150001500018, 10 | discount_pct: 84.99984999849998, 11 | example_code: { 12 | steps: { 13 | exported: { 14 | robot: '/tigris/store', 15 | use: ':original', 16 | credentials: 'YOUR_TIGRIS_CREDENTIALS', 17 | path: 'my_target_folder/${unique_prefix}/${file.url_name}', 18 | }, 19 | }, 20 | }, 21 | example_code_description: 'Export uploaded files to `my_target_folder` on Tigris:', 22 | has_small_icon: true, 23 | minimum_charge: 0, 24 | output_factor: 1, 25 | override_lvl1: 'File Exporting', 26 | purpose_sentence: 'exports encoding results to Tigris buckets', 27 | purpose_verb: 'export', 28 | purpose_word: 'Tigris', 29 | purpose_words: 'Export files to Tigris', 30 | service_slug: 'file-exporting', 31 | slot_count: 10, 32 | title: 'Export files to Tigris', 33 | typical_file_size_mb: 1.2, 34 | typical_file_type: 'file', 35 | name: 'TigrisStoreRobot', 36 | priceFactor: 6.6666, 37 | queueSlotCount: 10, 38 | isAllowedForUrlTransform: true, 39 | trackOutputFileSize: false, 40 | isInternal: false, 41 | removeJobResultFilesFromDiskRightAfterStoringOnS3: false, 42 | stage: 'ga', 43 | } 44 | 45 | export const robotTigrisStoreInstructionsSchema = robotBase 46 | .merge(robotUse) 47 | .merge(tigrisBase) 48 | .extend({ 49 | robot: z.literal('/tigris/store').describe(` 50 | The URL to the result file will be returned in the Assembly Status JSON. 51 | `), 52 | path: z 53 | .string() 54 | .default('${unique_prefix}/${file.url_name}') 55 | .describe(` 56 | The path at which the file is to be stored. This may include any available [Assembly variables](/docs/topics/assembly-instructions/#assembly-variables). The path must not be a directory. 57 | `), 58 | acl: z 59 | .enum(['private', 'public-read']) 60 | .default('public-read') 61 | .describe(` 62 | The permissions used for this file. 63 | `), 64 | headers: z 65 | .record(z.string()) 66 | .default({ 'Content-Type': '${file.mime}' }) 67 | .describe(` 68 | An object containing a list of headers to be set for this file on Tigris, such as \`{ FileURL: "\${file.url_name}" }\`. This can also include any available [Assembly Variables](/docs/topics/assembly-instructions/#assembly-variables). 69 | 70 | Object Metadata can be specified using \`x-amz-meta-*\` headers. Note that these headers [do not support non-ASCII metadata values](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata). 71 | `), 72 | sign_urls_for: z 73 | .number() 74 | .int() 75 | .min(0) 76 | .optional() 77 | .describe(` 78 | This parameter provides signed URLs in the result JSON (in the \`signed_ssl_url\` property). The number that you set this parameter to is the URL expiry time in seconds. 79 | 80 | If this parameter is not used, no URL signing is done. 81 | `), 82 | bucket_region: z 83 | .string() 84 | .optional() 85 | .describe('The region of your Tigris bucket. This is optional as it can often be derived.'), 86 | }) 87 | .strict() 88 | 89 | export const robotTigrisStoreInstructionsWithHiddenFieldsSchema = 90 | robotTigrisStoreInstructionsSchema.extend({ 91 | result: z 92 | .union([z.literal('debug'), robotTigrisStoreInstructionsSchema.shape.result]) 93 | .optional(), 94 | }) 95 | 96 | export type RobotTigrisStoreInstructions = z.infer 97 | export type RobotTigrisStoreInstructionsWithHiddenFields = z.infer< 98 | typeof robotTigrisStoreInstructionsWithHiddenFieldsSchema 99 | > 100 | 101 | export const interpolatableRobotTigrisStoreInstructionsSchema = interpolateRobot( 102 | robotTigrisStoreInstructionsSchema, 103 | ) 104 | export type InterpolatableRobotTigrisStoreInstructions = 105 | InterpolatableRobotTigrisStoreInstructionsInput 106 | 107 | export type InterpolatableRobotTigrisStoreInstructionsInput = z.input< 108 | typeof interpolatableRobotTigrisStoreInstructionsSchema 109 | > 110 | 111 | export const interpolatableRobotTigrisStoreInstructionsWithHiddenFieldsSchema = interpolateRobot( 112 | robotTigrisStoreInstructionsWithHiddenFieldsSchema, 113 | ) 114 | export type InterpolatableRobotTigrisStoreInstructionsWithHiddenFields = z.infer< 115 | typeof interpolatableRobotTigrisStoreInstructionsWithHiddenFieldsSchema 116 | > 117 | export type InterpolatableRobotTigrisStoreInstructionsWithHiddenFieldsInput = z.input< 118 | typeof interpolatableRobotTigrisStoreInstructionsWithHiddenFieldsSchema 119 | > 120 | --------------------------------------------------------------------------------