├── 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 |
--------------------------------------------------------------------------------