Skip to content

Commit 8c4af32

Browse files
committed
first stab at merging in transloadify
1 parent 5307302 commit 8c4af32

29 files changed

+4103
-325
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,33 @@
2020
"@aws-sdk/client-s3": "^3.891.0",
2121
"@aws-sdk/s3-request-presigner": "^3.891.0",
2222
"@transloadit/sev-logger": "^0.0.15",
23+
"clipanion": "^4.0.0-rc.4",
2324
"debug": "^4.4.3",
2425
"form-data": "^4.0.4",
2526
"got": "14.4.9",
2627
"into-stream": "^9.0.0",
2728
"is-stream": "^4.0.1",
29+
"node-watch": "^0.7.4",
2830
"p-map": "^7.0.3",
31+
"recursive-readdir": "^2.2.3",
2932
"tus-js-client": "^4.3.1",
3033
"type-fest": "^4.41.0",
3134
"zod": "3.25.76"
3235
},
3336
"devDependencies": {
3437
"@biomejs/biome": "^2.2.4",
3538
"@types/debug": "^4.1.12",
39+
"@types/recursive-readdir": "^2.2.4",
3640
"@types/temp": "^0.9.4",
3741
"@vitest/coverage-v8": "^3.2.4",
3842
"badge-maker": "^5.0.2",
39-
"dotenv": "^17.2.2",
43+
"dotenv": "^17.2.3",
4044
"execa": "9.6.0",
45+
"image-size": "^2.0.2",
4146
"nock": "^14.0.10",
4247
"npm-run-all": "^4.1.5",
4348
"p-retry": "^7.0.0",
49+
"rimraf": "^6.1.2",
4450
"temp": "^0.9.4",
4551
"tsx": "4.20.5",
4652
"typescript": "5.9.2",
@@ -65,6 +71,7 @@
6571
"prepack": "rm -f tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo && tsc --build tsconfig.build.json",
6672
"test:unit": "vitest run --coverage ./test/unit",
6773
"test:integration": "vitest run ./test/integration",
74+
"test:e2e": "vitest run ./test/e2e",
6875
"test:all": "vitest run --coverage",
6976
"test": "yarn test:unit"
7077
},

src/cli.ts

Lines changed: 12 additions & 305 deletions
Original file line numberDiff line numberDiff line change
@@ -4,310 +4,8 @@ import { realpathSync } from 'node:fs'
44
import path from 'node:path'
55
import process from 'node:process'
66
import { fileURLToPath } from 'node:url'
7-
import { type ZodIssue, z } from 'zod'
8-
import {
9-
assemblyAuthInstructionsSchema,
10-
assemblyInstructionsSchema,
11-
} from './alphalib/types/template.ts'
12-
import type { OptionalAuthParams } from './apiTypes.ts'
13-
import { Transloadit } from './Transloadit.ts'
14-
15-
type UrlParamPrimitive = string | number | boolean
16-
type UrlParamArray = UrlParamPrimitive[]
17-
type NormalizedUrlParams = Record<string, UrlParamPrimitive | UrlParamArray>
18-
19-
interface RunSigOptions {
20-
providedInput?: string
21-
algorithm?: string
22-
}
23-
24-
interface RunSmartSigOptions {
25-
providedInput?: string
26-
}
27-
28-
const smartCdnParamsSchema = z
29-
.object({
30-
workspace: z.string().min(1, 'workspace is required'),
31-
template: z.string().min(1, 'template is required'),
32-
input: z.union([z.string(), z.number(), z.boolean()]),
33-
url_params: z.record(z.unknown()).optional(),
34-
expire_at_ms: z.union([z.number(), z.string()]).optional(),
35-
})
36-
.passthrough()
37-
38-
const cliSignatureParamsSchema = assemblyInstructionsSchema
39-
.extend({ auth: assemblyAuthInstructionsSchema.partial().optional() })
40-
.partial()
41-
.passthrough()
42-
43-
export async function readStdin(): Promise<string> {
44-
if (process.stdin.isTTY) return ''
45-
46-
process.stdin.setEncoding('utf8')
47-
let data = ''
48-
49-
for await (const chunk of process.stdin) {
50-
data += chunk
51-
}
52-
53-
return data
54-
}
55-
56-
function fail(message: string): void {
57-
console.error(message)
58-
process.exitCode = 1
59-
}
60-
61-
function formatIssues(issues: ZodIssue[]): string {
62-
return issues
63-
.map((issue) => {
64-
const path = issue.path.join('.') || '(root)'
65-
return `${path}: ${issue.message}`
66-
})
67-
.join('; ')
68-
}
69-
70-
function normalizeUrlParam(value: unknown): UrlParamPrimitive | UrlParamArray | undefined {
71-
if (value == null) return undefined
72-
if (Array.isArray(value)) {
73-
const normalized = value.filter(
74-
(item): item is UrlParamPrimitive =>
75-
typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean',
76-
)
77-
return normalized.length > 0 ? normalized : undefined
78-
}
79-
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
80-
return value
81-
}
82-
return undefined
83-
}
84-
85-
function normalizeUrlParams(params?: Record<string, unknown>): NormalizedUrlParams | undefined {
86-
if (params == null) return undefined
87-
let normalized: NormalizedUrlParams | undefined
88-
for (const [key, value] of Object.entries(params)) {
89-
const normalizedValue = normalizeUrlParam(value)
90-
if (normalizedValue === undefined) continue
91-
if (normalized == null) normalized = {}
92-
normalized[key] = normalizedValue
93-
}
94-
return normalized
95-
}
96-
97-
function ensureCredentials(): { authKey: string; authSecret: string } | null {
98-
const authKey = process.env.TRANSLOADIT_KEY || process.env.TRANSLOADIT_AUTH_KEY
99-
const authSecret = process.env.TRANSLOADIT_SECRET || process.env.TRANSLOADIT_AUTH_SECRET
100-
101-
if (!authKey || !authSecret) {
102-
fail(
103-
'Missing credentials. Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET environment variables.',
104-
)
105-
return null
106-
}
107-
108-
return { authKey, authSecret }
109-
}
110-
111-
export async function runSig(options: RunSigOptions = {}): Promise<void> {
112-
const credentials = ensureCredentials()
113-
if (credentials == null) return
114-
const { authKey, authSecret } = credentials
115-
const { providedInput, algorithm } = options
116-
117-
const rawInput = providedInput ?? (await readStdin())
118-
const input = rawInput.trim()
119-
let params: Record<string, unknown>
120-
121-
if (input === '') {
122-
params = { auth: { key: authKey } }
123-
} else {
124-
let parsed: unknown
125-
try {
126-
parsed = JSON.parse(input)
127-
} catch (error) {
128-
fail(`Failed to parse JSON from stdin: ${(error as Error).message}`)
129-
return
130-
}
131-
132-
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
133-
fail('Invalid params provided via stdin. Expected a JSON object.')
134-
return
135-
}
136-
137-
const parsedResult = cliSignatureParamsSchema.safeParse(parsed)
138-
if (!parsedResult.success) {
139-
fail(`Invalid params: ${formatIssues(parsedResult.error.issues)}`)
140-
return
141-
}
142-
143-
const parsedParams = parsedResult.data as Record<string, unknown>
144-
const existingAuth =
145-
typeof parsedParams.auth === 'object' &&
146-
parsedParams.auth != null &&
147-
!Array.isArray(parsedParams.auth)
148-
? (parsedParams.auth as Record<string, unknown>)
149-
: {}
150-
151-
params = {
152-
...parsedParams,
153-
auth: {
154-
...existingAuth,
155-
key: authKey,
156-
},
157-
}
158-
}
159-
160-
const client = new Transloadit({ authKey, authSecret })
161-
try {
162-
const signature = client.calcSignature(params as OptionalAuthParams, algorithm)
163-
process.stdout.write(`${JSON.stringify(signature)}\n`)
164-
} catch (error) {
165-
fail(`Failed to generate signature: ${(error as Error).message}`)
166-
}
167-
}
168-
169-
export async function runSmartSig(options: RunSmartSigOptions = {}): Promise<void> {
170-
const credentials = ensureCredentials()
171-
if (credentials == null) return
172-
const { authKey, authSecret } = credentials
173-
174-
const rawInput = options.providedInput ?? (await readStdin())
175-
const input = rawInput.trim()
176-
if (input === '') {
177-
fail(
178-
'Missing params provided via stdin. Expected a JSON object with workspace, template, input, and optional Smart CDN parameters.',
179-
)
180-
return
181-
}
182-
183-
let parsed: unknown
184-
try {
185-
parsed = JSON.parse(input)
186-
} catch (error) {
187-
fail(`Failed to parse JSON from stdin: ${(error as Error).message}`)
188-
return
189-
}
190-
191-
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
192-
fail('Invalid params provided via stdin. Expected a JSON object.')
193-
return
194-
}
195-
196-
const parsedResult = smartCdnParamsSchema.safeParse(parsed)
197-
if (!parsedResult.success) {
198-
fail(`Invalid params: ${formatIssues(parsedResult.error.issues)}`)
199-
return
200-
}
201-
202-
const { workspace, template, input: inputFieldRaw, url_params, expire_at_ms } = parsedResult.data
203-
const urlParams = normalizeUrlParams(url_params as Record<string, unknown> | undefined)
204-
205-
let expiresAt: number | undefined
206-
if (typeof expire_at_ms === 'string') {
207-
const parsedNumber = Number.parseInt(expire_at_ms, 10)
208-
if (Number.isNaN(parsedNumber)) {
209-
fail('Invalid params: expire_at_ms must be a number.')
210-
return
211-
}
212-
expiresAt = parsedNumber
213-
} else {
214-
expiresAt = expire_at_ms
215-
}
216-
217-
const inputField = typeof inputFieldRaw === 'string' ? inputFieldRaw : String(inputFieldRaw)
218-
219-
const client = new Transloadit({ authKey, authSecret })
220-
try {
221-
const signedUrl = client.getSignedSmartCDNUrl({
222-
workspace,
223-
template,
224-
input: inputField,
225-
urlParams,
226-
expiresAt,
227-
})
228-
process.stdout.write(`${signedUrl}\n`)
229-
} catch (error) {
230-
fail(`Failed to generate Smart CDN URL: ${(error as Error).message}`)
231-
}
232-
}
233-
234-
function parseSigArguments(args: string[]): { algorithm?: string } {
235-
let algorithm: string | undefined
236-
let index = 0
237-
while (index < args.length) {
238-
const arg = args[index]
239-
if (arg === '--algorithm' || arg === '-a') {
240-
const next = args[index + 1]
241-
if (next == null || next.startsWith('-')) {
242-
throw new Error('Missing value for --algorithm option')
243-
}
244-
algorithm = next
245-
index += 2
246-
continue
247-
}
248-
if (arg.startsWith('--algorithm=')) {
249-
const [, value] = arg.split('=', 2)
250-
if (value === undefined || value === '') {
251-
throw new Error('Missing value for --algorithm option')
252-
}
253-
algorithm = value
254-
index += 1
255-
continue
256-
}
257-
throw new Error(`Unknown option: ${arg}`)
258-
}
259-
260-
return { algorithm }
261-
}
262-
263-
export async function main(args = process.argv.slice(2)): Promise<void> {
264-
const [command, ...commandArgs] = args
265-
266-
switch (command) {
267-
case 'smart_sig': {
268-
await runSmartSig()
269-
break
270-
}
271-
272-
case 'sig': {
273-
try {
274-
const { algorithm } = parseSigArguments(commandArgs)
275-
await runSig({ algorithm })
276-
} catch (error) {
277-
fail((error as Error).message)
278-
}
279-
break
280-
}
281-
282-
case '-h':
283-
case '--help':
284-
case undefined: {
285-
process.stdout.write(
286-
[
287-
'Usage:',
288-
' npx transloadit smart_sig Read Smart CDN params JSON from stdin and output a signed URL.',
289-
' npx transloadit sig [--algorithm <name>] Read params JSON from stdin and output signed payload JSON.',
290-
'',
291-
'Required JSON fields:',
292-
' smart_sig: workspace, template, input',
293-
' sig: none (object is optional)',
294-
'Optional JSON fields:',
295-
' smart_sig: expire_at_ms, url_params',
296-
' sig: auth.expires and any supported assembly params',
297-
'',
298-
'Environment variables:',
299-
' TRANSLOADIT_KEY, TRANSLOADIT_SECRET',
300-
].join('\n'),
301-
)
302-
if (command === undefined) process.exitCode = 1
303-
break
304-
}
305-
306-
default: {
307-
fail(`Unknown command: ${command}`)
308-
}
309-
}
310-
}
7+
import 'dotenv/config'
8+
import { createCli } from './cli/commands/index.ts'
3119

31210
const currentFile = realpathSync(fileURLToPath(import.meta.url))
31311

@@ -326,11 +24,20 @@ export function shouldRunCli(invoked?: string): boolean {
32624
return resolved === currentFile
32725
}
32826

27+
export async function main(args = process.argv.slice(2)): Promise<void> {
28+
const cli = createCli()
29+
const exitCode = await cli.run(args)
30+
if (exitCode !== 0) {
31+
process.exitCode = exitCode
32+
}
33+
}
34+
32935
export function runCliWhenExecuted(): void {
33036
if (!shouldRunCli(process.argv[1])) return
33137

33238
void main().catch((error) => {
333-
fail((error as Error).message)
39+
console.error((error as Error).message)
40+
process.exitCode = 1
33441
})
33542
}
33643

0 commit comments

Comments
 (0)