Skip to content

Commit 8bf0e4a

Browse files
committed
npx smart_sig
1 parent 982d972 commit 8bf0e4a

File tree

4 files changed

+221
-1
lines changed

4 files changed

+221
-1
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,17 @@ Calculates a signature for the given `params` JSON object. If the `params` objec
385385

386386
This function returns an object with the key `signature` (containing the calculated signature string) and a key `params`, which contains the stringified version of the passed `params` object (including the set expires and authKey keys).
387387

388+
#### CLI smart_sig
389+
390+
Generate a signature from the command line without writing any JavaScript. The CLI reads a JSON object from stdin, injects credentials from `TRANSLOADIT_KEY`/`TRANSLOADIT_SECRET`, and prints the payload returned by `calcSignature()`.
391+
392+
```sh
393+
TRANSLOADIT_KEY=... TRANSLOADIT_SECRET=... \
394+
printf '{"assembly_id":"12345"}' | npx transloadit smart_sig
395+
```
396+
397+
You can also use `TRANSLOADIT_AUTH_KEY`/`TRANSLOADIT_AUTH_SECRET` as aliases for the environment variables.
398+
388399
#### getSignedSmartCDNUrl(params)
389400

390401
Constructs a signed Smart CDN URL, as defined in the [API documentation](https://transloadit.com/docs/topics/signature-authentication/#smart-cdn). `params` must be an object with the following properties:

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,8 @@
7676
"files": [
7777
"dist",
7878
"src"
79-
]
79+
],
80+
"bin": {
81+
"transloadit": "./dist/cli.js"
82+
}
8083
}

src/cli.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env node
2+
3+
import path from 'node:path'
4+
import process from 'node:process'
5+
import { fileURLToPath } from 'node:url'
6+
import { Transloadit } from './Transloadit.ts'
7+
import type { OptionalAuthParams } from './apiTypes.ts'
8+
9+
export async function readStdin(): Promise<string> {
10+
if (process.stdin.isTTY) return ''
11+
12+
process.stdin.setEncoding('utf8')
13+
let data = ''
14+
15+
for await (const chunk of process.stdin) {
16+
data += chunk
17+
}
18+
19+
return data
20+
}
21+
22+
function fail(message: string): void {
23+
console.error(message)
24+
process.exitCode = 1
25+
}
26+
27+
export async function runSmartSig(providedInput?: string): Promise<void> {
28+
const authKey = process.env.TRANSLOADIT_KEY || process.env.TRANSLOADIT_AUTH_KEY
29+
const authSecret = process.env.TRANSLOADIT_SECRET || process.env.TRANSLOADIT_AUTH_SECRET
30+
31+
if (!authKey || !authSecret) {
32+
fail(
33+
'Missing credentials. Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET environment variables.',
34+
)
35+
return
36+
}
37+
38+
const rawInput = providedInput ?? (await readStdin())
39+
const input = rawInput.trim()
40+
let params: OptionalAuthParams = {}
41+
42+
if (input !== '') {
43+
try {
44+
const parsed = JSON.parse(input)
45+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
46+
fail('Invalid params provided via stdin. Expected a JSON object.')
47+
return
48+
}
49+
50+
params = parsed as OptionalAuthParams
51+
} catch (error: unknown) {
52+
fail(`Failed to parse JSON from stdin: ${(error as Error).message}`)
53+
return
54+
}
55+
}
56+
57+
const client = new Transloadit({ authKey, authSecret })
58+
const signature = client.calcSignature(params)
59+
process.stdout.write(`${JSON.stringify(signature)}\n`)
60+
}
61+
62+
export async function main(args = process.argv.slice(2)): Promise<void> {
63+
const [command] = args
64+
65+
switch (command) {
66+
case 'smart_sig': {
67+
await runSmartSig()
68+
break
69+
}
70+
71+
case '-h':
72+
case '--help':
73+
case undefined: {
74+
process.stdout.write(
75+
[
76+
'Usage:',
77+
' npx transloadit smart_sig Read params JSON from stdin and output signed payload.',
78+
'',
79+
'Environment variables:',
80+
' TRANSLOADIT_KEY, TRANSLOADIT_SECRET',
81+
].join('\n'),
82+
)
83+
if (command === undefined) process.exitCode = 1
84+
break
85+
}
86+
87+
default: {
88+
fail(`Unknown command: ${command}`)
89+
}
90+
}
91+
}
92+
93+
const currentFile = path.resolve(fileURLToPath(import.meta.url))
94+
const invokedFile = typeof process.argv[1] === 'string' ? path.resolve(process.argv[1]) : ''
95+
96+
if (currentFile === invokedFile) {
97+
void main().catch((error) => {
98+
fail((error as Error).message)
99+
})
100+
}

test/unit/test-cli.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest'
2+
import { main, runSmartSig } from '../../src/cli.ts'
3+
import { Transloadit } from '../../src/Transloadit.ts'
4+
5+
const mockedExpiresDate = '2025-01-01T00:00:00.000Z'
6+
const mockExpires = () =>
7+
vi
8+
.spyOn(Transloadit.prototype as unknown as { _getExpiresDate: () => string }, '_getExpiresDate')
9+
.mockReturnValue(mockedExpiresDate)
10+
11+
const resetExitCode = () => {
12+
process.exitCode = undefined
13+
}
14+
15+
afterEach(() => {
16+
vi.restoreAllMocks()
17+
vi.unstubAllEnvs()
18+
resetExitCode()
19+
})
20+
21+
describe('cli smart_sig', () => {
22+
it('prints signature JSON built from stdin params', async () => {
23+
mockExpires()
24+
vi.stubEnv('TRANSLOADIT_KEY', 'key')
25+
vi.stubEnv('TRANSLOADIT_SECRET', 'secret')
26+
27+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
28+
const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
29+
30+
const params = { template_id: '123' }
31+
await runSmartSig(JSON.stringify(params))
32+
33+
expect(stderrSpy).not.toHaveBeenCalled()
34+
expect(stdoutSpy).toHaveBeenCalledTimes(1)
35+
const output = stdoutSpy.mock.calls[0]?.[0]
36+
const parsed = JSON.parse(`${output}`.trim())
37+
38+
const client = new Transloadit({ authKey: 'key', authSecret: 'secret' })
39+
const expected = client.calcSignature({ template_id: '123' })
40+
expect(parsed).toEqual(expected)
41+
expect(process.exitCode).toBeUndefined()
42+
43+
})
44+
45+
it('fails when credentials are missing', async () => {
46+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
47+
const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
48+
49+
await runSmartSig('{}')
50+
51+
expect(stdoutSpy).not.toHaveBeenCalled()
52+
expect(stderrSpy).toHaveBeenCalledWith(
53+
'Missing credentials. Please set TRANSLOADIT_KEY and TRANSLOADIT_SECRET environment variables.',
54+
)
55+
expect(process.exitCode).toBe(1)
56+
57+
})
58+
59+
it('fails when stdin is not valid JSON', async () => {
60+
mockExpires()
61+
vi.stubEnv('TRANSLOADIT_KEY', 'key')
62+
vi.stubEnv('TRANSLOADIT_SECRET', 'secret')
63+
64+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
65+
const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
66+
67+
await runSmartSig('this is not json')
68+
69+
expect(stdoutSpy).not.toHaveBeenCalled()
70+
expect(stderrSpy).toHaveBeenCalled()
71+
expect(stderrSpy.mock.calls[0]?.[0]).toContain('Failed to parse JSON from stdin')
72+
expect(process.exitCode).toBe(1)
73+
74+
})
75+
76+
it('fails when params are not an object', async () => {
77+
mockExpires()
78+
vi.stubEnv('TRANSLOADIT_KEY', 'key')
79+
vi.stubEnv('TRANSLOADIT_SECRET', 'secret')
80+
81+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
82+
const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
83+
84+
await runSmartSig('[]')
85+
86+
expect(stdoutSpy).not.toHaveBeenCalled()
87+
expect(stderrSpy).toHaveBeenCalledWith('Invalid params provided via stdin. Expected a JSON object.')
88+
expect(process.exitCode).toBe(1)
89+
90+
})
91+
92+
it('prints usage when no command is provided', async () => {
93+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
94+
const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
95+
96+
await main([])
97+
98+
expect(stderrSpy).not.toHaveBeenCalled()
99+
expect(stdoutSpy).toHaveBeenCalled()
100+
const message = `${stdoutSpy.mock.calls[0]?.[0]}`
101+
expect(message).toContain('Usage:')
102+
expect(message).toContain('npx transloadit smart_sig')
103+
expect(process.exitCode).toBe(1)
104+
105+
})
106+
})

0 commit comments

Comments
 (0)