diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17a9441 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +.docker-cache +.env +vendor +node_modules +transloadit-*.tgz diff --git a/.gitignore b/.gitignore index efe5dcb..897571d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ env.sh .phpunit.cache .aider* .env +.docker-cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 41852b8..f60b153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,36 @@ -## Versions +# Changelog -### [main](https://github.com/transloadit/php-sdk/tree/main) +## [main](https://github.com/transloadit/php-sdk/tree/main) -diff: https://github.com/transloadit/php-sdk/compare/3.2.0...main +diff: https://github.com/transloadit/php-sdk/compare/3.3.0...main -### [3.2.0](https://github.com/transloadit/php-sdk/tree/3.2.0) +## [3.3.0](https://github.com/transloadit/php-sdk/tree/3.3.0) + +- Replace the custom Node parity helper with the official `transloadit` CLI for Smart CDN signatures +- Add a Docker-based test harness and document the parity workflow +- Randomize system-test request signatures and document optional `auth.nonce` usage to avoid replay protection failures + +diff: https://github.com/transloadit/php-sdk/compare/3.2.0...3.3.0 + +## [3.2.0](https://github.com/transloadit/php-sdk/tree/3.2.0) - Implement `signedSmartCDNUrl` diff: https://github.com/transloadit/php-sdk/compare/3.1.0...3.2.0 -### [3.1.0](https://github.com/transloadit/php-sdk/tree/3.1.0) +## [3.1.0](https://github.com/transloadit/php-sdk/tree/3.1.0) - Pass down `curlOptions` when `TransloaditRequest` reinstantiates itself for `waitForCompletion` diff: https://github.com/transloadit/php-sdk/compare/3.0.4-dev...3.1.0 -### [3.0.4-dev](https://github.com/transloadit/php-sdk/tree/3.0.4-dev) +## [3.0.4-dev](https://github.com/transloadit/php-sdk/tree/3.0.4-dev) - Pass down `curlOptions` when `TransloaditRequest` reinstantiates itself for `waitForCompletion` diff: https://github.com/transloadit/php-sdk/compare/3.0.4...3.0.4-dev -### [3.0.4](https://github.com/transloadit/php-sdk/tree/3.0.4) +## [3.0.4](https://github.com/transloadit/php-sdk/tree/3.0.4) - Ditch `v` prefix in versions as that's more idiomatic - Bring back the getAssembly() function @@ -34,7 +42,7 @@ diff: https://github.com/transloadit/php-sdk/compare/3.0.4...3.0.4-dev diff: https://github.com/transloadit/php-sdk/compare/v2.0.0...3.0.4 -### [v2.1.0](https://github.com/transloadit/php-sdk/tree/v2.1.0) +## [v2.1.0](https://github.com/transloadit/php-sdk/tree/v2.1.0) - Fix for CURL deprecated functions (thanks @ABerkhout) - CI improvements (phpunit, travis, composer) @@ -43,7 +51,7 @@ diff: https://github.com/transloadit/php-sdk/compare/v2.0.0...3.0.4 diff: https://github.com/transloadit/php-sdk/compare/v2.0.0...v2.1.0 -### [v2.0.0](https://github.com/transloadit/php-sdk/tree/v2.0.0) +## [v2.0.0](https://github.com/transloadit/php-sdk/tree/v2.0.0) - Retire host + protocol in favor of one endpoint property, allow passing that on to the Request object. @@ -52,14 +60,14 @@ diff: https://github.com/transloadit/php-sdk/compare/v2.0.0...v2.1.0 diff: https://github.com/transloadit/php-sdk/compare/v1.0.1...v2.0.0 -### [v1.0.1](https://github.com/transloadit/php-sdk/tree/v1.0.1) +## [v1.0.1](https://github.com/transloadit/php-sdk/tree/v1.0.1) - Fix broken examples - Improve documentation (version changelogs) diff: https://github.com/transloadit/php-sdk/compare/v1.0.0...v1.0.1 -### [v1.0.0](https://github.com/transloadit/php-sdk/tree/v1.0.0) +## [v1.0.0](https://github.com/transloadit/php-sdk/tree/v1.0.0) A big thanks to [@nervetattoo](https://github.com/nervetattoo) for making this version happen! @@ -69,7 +77,7 @@ A big thanks to [@nervetattoo](https://github.com/nervetattoo) for making this v diff: https://github.com/transloadit/php-sdk/compare/v0.10.0...v1.0.0 -### [v0.10.0](https://github.com/transloadit/php-sdk/tree/v0.10.0) +## [v0.10.0](https://github.com/transloadit/php-sdk/tree/v0.10.0) - Add support for Strict mode - Add support for more auth params @@ -79,7 +87,7 @@ diff: https://github.com/transloadit/php-sdk/compare/v0.10.0...v1.0.0 diff: https://github.com/transloadit/php-sdk/compare/v0.9.1...v0.10.0 -### [v0.9.1](https://github.com/transloadit/php-sdk/tree/v0.9.1) +## [v0.9.1](https://github.com/transloadit/php-sdk/tree/v0.9.1) - Improve documentation - Better handling of errors & non-json responses @@ -87,7 +95,7 @@ diff: https://github.com/transloadit/php-sdk/compare/v0.9.1...v0.10.0 diff: https://github.com/transloadit/php-sdk/compare/v0.9...v0.9.1 -### [v0.9](https://github.com/transloadit/php-sdk/tree/v0.9) +## [v0.9](https://github.com/transloadit/php-sdk/tree/v0.9) - Use markdown for docs - Add support for signed GET requests @@ -97,12 +105,12 @@ diff: https://github.com/transloadit/php-sdk/compare/v0.9...v0.9.1 diff: https://github.com/transloadit/php-sdk/compare/v0.2...v0.9 -### [v0.2](https://github.com/transloadit/php-sdk/tree/v0.2) +## [v0.2](https://github.com/transloadit/php-sdk/tree/v0.2) - Add error handling diff: https://github.com/transloadit/php-sdk/compare/v0.1...v0.2 -### [v0.1](https://github.com/transloadit/php-sdk/tree/v0.1) +## [v0.1](https://github.com/transloadit/php-sdk/tree/v0.1) The very first version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5dc8c44 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# Contributing + +Feel free to fork this project. We will happily merge bug fixes or other small +improvements. For bigger changes you should probably get in touch with us +before you start to avoid not seeing them merged. + +## Testing + +### Basic Tests + +```bash +make test +``` + +### System Tests + +System tests require: + +1. Valid Transloadit credentials in environment: + +```bash +export TRANSLOADIT_KEY='your-auth-key' +export TRANSLOADIT_SECRET='your-auth-secret' +``` + +Then run: + +```bash +make test-all +``` + +### Node.js Reference Implementation Parity Assertions + +The SDK includes assertions that compare Smart CDN URL signatures and regular request signatures with our reference Node.js implementation. To run these tests: + +1. Requirements: + + - Node.js 20+ with npm + - Ability to execute `npx transloadit smart_sig` (the CLI is downloaded on demand) + - Ability to execute `npx transloadit sig` (the CLI is downloaded on demand) + +2. Run the tests: + +```bash +export TRANSLOADIT_KEY='your-auth-key' +export TRANSLOADIT_SECRET='your-auth-secret' +TEST_NODE_PARITY=1 make test-all +``` + +If you want to warm the CLI cache ahead of time you can run: + +```bash +npx --yes transloadit smart_sig --help +``` + +For regular request signatures, you can also prime the CLI by running: + +```bash +TRANSLOADIT_KEY=... TRANSLOADIT_SECRET=... \ + npx --yes transloadit sig --algorithm sha1 --help +``` + +CI opts into `TEST_NODE_PARITY=1`, and you can optionally do this locally as well. + +### Run Tests in Docker + +Use `scripts/test-in-docker.sh` for a reproducible environment: + +```bash +./scripts/test-in-docker.sh +``` + +This builds the local image, runs `composer install`, and executes `make test-all` (unit + integration tests). Pass a custom command to run something else (composer install still runs first): + +```bash +./scripts/test-in-docker.sh vendor/bin/phpunit --filter signedSmartCDNUrl +``` + +Environment variables such as `TEST_NODE_PARITY` or the credentials in `.env` are forwarded, so you can combine parity checks and integration tests with Docker: + +```bash +TEST_NODE_PARITY=1 ./scripts/test-in-docker.sh +``` + +## Releasing a new version + +To release, say `3.2.0` [Packagist](https://packagist.org/packages/transloadit/php-sdk), follow these steps: + +1. Make sure `PACKAGIST_TOKEN` is set in your `.env` file +1. Make sure you are in main: `git checkout main` +1. Update `CHANGELOG.md` and `composer.json` +1. Commit: `git add CHANGELOG.md composer.json && git commit -m "Release 3.2.0"` +1. Tag, push, and release: `source .env && VERSION=3.2.0 ./release.sh` + +This project implements the [Semantic Versioning](http://semver.org/) guidelines. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56e5f58 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1 + +FROM php:8.3-cli AS base + +ENV COMPOSER_ALLOW_SUPERUSER=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git \ + unzip \ + zip \ + libzip-dev \ + curl \ + ca-certificates \ + && docker-php-ext-configure zip \ + && docker-php-ext-install zip \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Install Node.js (for transloadit CLI parity checks) +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && npm install -g npm@latest \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace diff --git a/README.md b/README.md index 0d5506d..fda87a7 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,23 @@ echo ''; Signature Authentication is done by the PHP SDK by default internally so you do not need to worry about this :) +If you script the same request payload multiple times in quick succession (for example inside a health check or tight integration test loop), add a random nonce to keep each signature unique: + +```php +$params = [ + 'auth' => [ + 'key' => 'MY_TRANSLOADIT_KEY', + 'expires' => gmdate('Y/m/d H:i:s+00:00', strtotime('+2 hours')), + 'nonce' => bin2hex(random_bytes(16)), + ], + 'steps' => [ + // … + ], +]; +``` + +The nonce is optional for regular usage, but including it in heavily scripted flows prevents Transloadit from rejecting repeated identical signatures. + ### Signature Auth (Smart CDN) You can use the `signedSmartCDNUrl` method to generate signed URLs for Transloadit's [Smart CDN](https://transloadit.com/services/content-delivery/): @@ -522,74 +539,6 @@ All of the following will cause an error string to be returned: **_Note_**: You will need to set waitForCompletion = True in the $Transloadit->createAssembly($options) function call. -## Contributing - -Feel free to fork this project. We will happily merge bug fixes or other small -improvements. For bigger changes you should probably get in touch with us -before you start to avoid not seeing them merged. - -### Testing - -#### Basic Tests - -```bash -make test -``` - -#### System Tests - -System tests require: - -1. Valid Transloadit credentials in environment: - -```bash -export TRANSLOADIT_KEY='your-auth-key' -export TRANSLOADIT_SECRET='your-auth-secret' -``` - -Then run: - -```bash -make test-all -``` - -#### Node.js Reference Implementation Parity Assertions - -The SDK includes assertions that compare URL signing with our reference Node.js implementation. To run these tests: - -1. Requirements: - - - Node.js installed - - tsx installed globally (`npm install -g tsx`) - -2. Install dependencies: - -```bash -npm install -g tsx -``` - -3. Run the test: - -```bash -export TRANSLOADIT_KEY='your-auth-key' -export TRANSLOADIT_SECRET='your-auth-secret' -TEST_NODE_PARITY=1 make test-all -``` - -CI opts-into `TEST_NODE_PARITY=1`, and you can optionally do this locally as well. - -### Releasing a new version - -To release, say `3.2.0` [Packagist](https://packagist.org/packages/transloadit/php-sdk), follow these steps: - -1. Make sure `PACKAGIST_TOKEN` is set in your `.env` file -1. Make sure you are in main: `git checkout main` -1. Update `CHANGELOG.md` and `composer.json` -1. Commit: `git add CHANGELOG.md composer.json && git commit -m "Release 3.2.0"` -1. Tag, push, and release: `source env.sh && VERSION=3.2.0 ./release.sh` - -This project implements the [Semantic Versioning](http://semver.org/) guidelines. - ## License [MIT Licensed](LICENSE) diff --git a/scripts/test-in-docker.sh b/scripts/test-in-docker.sh new file mode 100755 index 0000000..5bf9c07 --- /dev/null +++ b/scripts/test-in-docker.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME=${IMAGE_NAME:-transloadit-php-sdk-dev} +CACHE_DIR=.docker-cache + +ensure_docker() { + if ! command -v docker >/dev/null 2>&1; then + echo "Docker is required to run this script." >&2 + exit 1 + fi + + if ! docker info >/dev/null 2>&1; then + if [[ -z "${DOCKER_HOST:-}" && -S "$HOME/.colima/default/docker.sock" ]]; then + export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" + fi + fi + + if ! docker info >/dev/null 2>&1; then + echo "Docker daemon is not reachable. Start Docker (or Colima) and retry." >&2 + exit 1 + fi +} + +configure_platform() { + if [[ -z "${DOCKER_PLATFORM:-}" ]]; then + local arch + arch=$(uname -m) + if [[ "$arch" == "arm64" || "$arch" == "aarch64" ]]; then + DOCKER_PLATFORM=linux/amd64 + fi + fi +} + +ensure_docker +configure_platform + +if [[ $# -eq 0 ]]; then + RUN_CMD='set -e; composer install --no-interaction --prefer-dist; make test-all' +else + printf -v USER_CMD '%q ' "$@" + RUN_CMD="set -e; composer install --no-interaction --prefer-dist; ${USER_CMD}" +fi + +mkdir -p "$CACHE_DIR/composer-cache" "$CACHE_DIR/npm-cache" "$CACHE_DIR/composer-home" + +BUILD_ARGS=() +if [[ -n "${DOCKER_PLATFORM:-}" ]]; then + BUILD_ARGS+=(--platform "$DOCKER_PLATFORM") +fi +BUILD_ARGS+=(-t "$IMAGE_NAME" -f Dockerfile .) + +docker build "${BUILD_ARGS[@]}" + +DOCKER_ARGS=( + --rm + --user "$(id -u):$(id -g)" + -e HOME=/workspace + -e COMPOSER_HOME=/workspace/$CACHE_DIR/composer-home + -e COMPOSER_CACHE_DIR=/workspace/$CACHE_DIR/composer-cache + -e npm_config_cache=/workspace/$CACHE_DIR/npm-cache + -e TEST_NODE_PARITY="${TEST_NODE_PARITY:-0}" + -v "$PWD":/workspace + -w /workspace +) + +if [[ -n "${DOCKER_PLATFORM:-}" ]]; then + DOCKER_ARGS+=(--platform "$DOCKER_PLATFORM") +fi + +if [[ -f .env ]]; then + DOCKER_ARGS+=(--env-file "$PWD/.env") +fi + +exec docker run "${DOCKER_ARGS[@]}" "$IMAGE_NAME" bash -lc "$RUN_CMD" diff --git a/test/bootstrap.php b/test/bootstrap.php index b9e024f..9b91ea1 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -25,6 +25,13 @@ public function setUp(): void { 'key' => TRANSLOADIT_KEY, 'secret' => TRANSLOADIT_SECRET, ]); + + try { + $nonce = bin2hex(random_bytes(16)); + } catch (\Exception $e) { + $nonce = uniqid('php-sdk-', true); + } + $this->request->params['auth']['nonce'] = $nonce; } } diff --git a/test/simple/TransloaditRequestTest.php b/test/simple/TransloaditRequestTest.php index 9fbbfa9..42ac32f 100644 --- a/test/simple/TransloaditRequestTest.php +++ b/test/simple/TransloaditRequestTest.php @@ -119,10 +119,149 @@ public function testGetParamsString() { $this->assertEquals($PARAMS['foo'], $params['foo']); } + public function testSignatureParityWithNodeCli(): void { + if (getenv('TEST_NODE_PARITY') !== '1') { + $this->markTestSkipped('Parity testing not enabled'); + } + + $request = new TransloaditRequest(); + $request->key = 'cli-key'; + $request->secret = 'cli-secret'; + $request->expires = '2025-01-02 00:00:00+00:00'; + $request->params = [ + 'auth' => ['expires' => '2025-01-02 00:00:00+00:00'], + 'steps' => [ + 'resize' => [ + 'robot' => '/image/resize', + 'width' => 320, + ], + ], + ]; + + $cliResult = $this->getCliSignature([ + 'auth' => ['expires' => '2025-01-02 00:00:00+00:00'], + 'steps' => [ + 'resize' => [ + 'robot' => '/image/resize', + 'width' => 320, + ], + ], + ], 'cli-key', 'cli-secret', 'sha1'); + + $this->assertNotNull($cliResult); + $this->assertArrayHasKey('signature', $cliResult); + $this->assertArrayHasKey('params', $cliResult); + + $cliParams = json_decode($cliResult['params'], true); + $phpParams = json_decode($request->getParamsString(), true); + + $this->assertEquals('cli-key', $cliParams['auth']['key']); + $this->assertEquals($phpParams['auth']['expires'], $cliParams['auth']['expires']); + $this->assertEquals( + $phpParams['steps']['resize']['robot'], + $cliParams['steps']['resize']['robot'] + ); + $this->assertEquals( + $phpParams['steps']['resize']['width'], + $cliParams['steps']['resize']['width'] + ); + + $expectedSignature = hash_hmac('sha1', $cliResult['params'], 'cli-secret'); + $this->assertEquals('sha1:' . $expectedSignature, $cliResult['signature']); + } + public function testExecute() { // Can't test this method because PHP doesn't allow stubbing the calls // to curl easily. However, the method hardly contains any logic as all // of that is located in other methods. $this->assertTrue(true); } + + private function getCliSignature(array $params, string $key, string $secret, ?string $algorithm = null): ?array { + if (getenv('TEST_NODE_PARITY') !== '1') { + return null; + } + + exec('command -v npm 2>/dev/null', $output, $returnVar); + if ($returnVar !== 0) { + throw new \RuntimeException('npm command not found. Please install Node.js (which includes npm).'); + } + + try { + $jsonInput = json_encode($params, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \RuntimeException('Failed to encode parameters for Node parity test: ' . $e->getMessage(), 0, $e); + } + + $command = 'npm exec --yes --package transloadit@4.0.5 -- transloadit sig'; + if ($algorithm !== null) { + $command .= ' --algorithm ' . escapeshellarg($algorithm); + } + + $descriptorspec = [ + 0 => ["pipe", "r"], // stdin + 1 => ["pipe", "w"], // stdout + 2 => ["pipe", "w"], // stderr + ]; + + $originalKey = getenv('TRANSLOADIT_KEY'); + $originalSecret = getenv('TRANSLOADIT_SECRET'); + $originalAuthKey = getenv('TRANSLOADIT_AUTH_KEY'); + $originalAuthSecret = getenv('TRANSLOADIT_AUTH_SECRET'); + + putenv('TRANSLOADIT_KEY=' . $key); + putenv('TRANSLOADIT_SECRET=' . $secret); + putenv('TRANSLOADIT_AUTH_KEY=' . $key); + putenv('TRANSLOADIT_AUTH_SECRET=' . $secret); + + try { + $process = proc_open($command, $descriptorspec, $pipes); + + if (!is_resource($process)) { + throw new \RuntimeException('Failed to start transloadit CLI sig command'); + } + + fwrite($pipes[0], $jsonInput); + fclose($pipes[0]); + + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + + fclose($pipes[1]); + fclose($pipes[2]); + + $exitCode = proc_close($process); + + if ($exitCode !== 0) { + $message = trim($stderr) !== '' ? trim($stderr) : 'Command exited with status ' . $exitCode; + throw new \RuntimeException('transloadit CLI sig command failed: ' . $message); + } + + return json_decode(trim($stdout), true, 512, JSON_THROW_ON_ERROR); + } finally { + if ($originalKey !== false) { + putenv('TRANSLOADIT_KEY=' . $originalKey); + } else { + putenv('TRANSLOADIT_KEY'); + } + + if ($originalSecret !== false) { + putenv('TRANSLOADIT_SECRET=' . $originalSecret); + } else { + putenv('TRANSLOADIT_SECRET'); + } + + if ($originalAuthKey !== false) { + putenv('TRANSLOADIT_AUTH_KEY=' . $originalAuthKey); + } else { + putenv('TRANSLOADIT_AUTH_KEY'); + } + + if ($originalAuthSecret !== false) { + putenv('TRANSLOADIT_AUTH_SECRET=' . $originalAuthSecret); + } else { + putenv('TRANSLOADIT_AUTH_SECRET'); + } + } + } } diff --git a/test/simple/TransloaditTest.php b/test/simple/TransloaditTest.php index 12c44e4..ef8f1ea 100644 --- a/test/simple/TransloaditTest.php +++ b/test/simple/TransloaditTest.php @@ -151,43 +151,99 @@ private function getExpectedUrl(array $params): ?string { return null; } - // Check for tsx before trying to use it - exec('which tsx 2>/dev/null', $output, $returnVar); + exec('command -v npm 2>/dev/null', $output, $returnVar); if ($returnVar !== 0) { - throw new \RuntimeException('tsx command not found. Please install it with: npm install -g tsx'); + throw new \RuntimeException('npm command not found. Please install Node.js (which includes npm).'); } - $scriptPath = __DIR__ . '/../../tool/node-smartcdn-sig.ts'; - $jsonInput = json_encode($params); + if (!isset($params['auth_key']) || !isset($params['auth_secret'])) { + throw new \InvalidArgumentException('auth_key and auth_secret are required for parity testing'); + } + + try { + $cliParams = [ + 'workspace' => $params['workspace'], + 'template' => $params['template'], + 'input' => $params['input'], + ]; + if (array_key_exists('url_params', $params)) { + $cliParams['url_params'] = $params['url_params']; + } + if (array_key_exists('expire_at_ms', $params)) { + $cliParams['expire_at_ms'] = $params['expire_at_ms']; + } + $jsonInput = json_encode($cliParams, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \RuntimeException('Failed to encode parameters for Node parity test: ' . $e->getMessage(), 0, $e); + } + + $command = 'npm exec --yes --package transloadit@4.0.5 -- transloadit smart_sig'; $descriptorspec = [ 0 => ["pipe", "r"], // stdin 1 => ["pipe", "w"], // stdout - 2 => ["pipe", "w"] // stderr + 2 => ["pipe", "w"], // stderr ]; - $process = proc_open("tsx $scriptPath", $descriptorspec, $pipes); + $originalKey = getenv('TRANSLOADIT_KEY'); + $originalSecret = getenv('TRANSLOADIT_SECRET'); + $originalAuthKey = getenv('TRANSLOADIT_AUTH_KEY'); + $originalAuthSecret = getenv('TRANSLOADIT_AUTH_SECRET'); - if (!is_resource($process)) { - throw new \RuntimeException('Failed to start Node script'); - } + putenv('TRANSLOADIT_KEY=' . $params['auth_key']); + putenv('TRANSLOADIT_SECRET=' . $params['auth_secret']); + putenv('TRANSLOADIT_AUTH_KEY=' . $params['auth_key']); + putenv('TRANSLOADIT_AUTH_SECRET=' . $params['auth_secret']); - fwrite($pipes[0], $jsonInput); - fclose($pipes[0]); + try { + $process = proc_open($command, $descriptorspec, $pipes); - $output = stream_get_contents($pipes[1]); - $error = stream_get_contents($pipes[2]); + if (!is_resource($process)) { + throw new \RuntimeException('Failed to start transloadit CLI parity command'); + } - fclose($pipes[1]); - fclose($pipes[2]); + fwrite($pipes[0], $jsonInput); + fclose($pipes[0]); - $exitCode = proc_close($process); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); - if ($exitCode !== 0) { - throw new \RuntimeException("Node script failed: $error"); - } + fclose($pipes[1]); + fclose($pipes[2]); + + $exitCode = proc_close($process); - return trim($output); + if ($exitCode !== 0) { + $message = trim($stderr) !== '' ? trim($stderr) : 'Command exited with status ' . $exitCode; + throw new \RuntimeException('transloadit CLI parity command failed: ' . $message); + } + + return trim($stdout); + } finally { + if ($originalKey !== false) { + putenv('TRANSLOADIT_KEY=' . $originalKey); + } else { + putenv('TRANSLOADIT_KEY'); + } + + if ($originalSecret !== false) { + putenv('TRANSLOADIT_SECRET=' . $originalSecret); + } else { + putenv('TRANSLOADIT_SECRET'); + } + + if ($originalAuthKey !== false) { + putenv('TRANSLOADIT_AUTH_KEY=' . $originalAuthKey); + } else { + putenv('TRANSLOADIT_AUTH_KEY'); + } + + if ($originalAuthSecret !== false) { + putenv('TRANSLOADIT_AUTH_SECRET=' . $originalAuthSecret); + } else { + putenv('TRANSLOADIT_AUTH_SECRET'); + } + } } private function assertParityWithNode(string $url, array $params, string $message = ''): void { @@ -305,14 +361,14 @@ public function testSignedSmartCDNUrl() { $this->assertEquals($expectedUrl, $nodeUrl, 'Node.js URL should match expected'); } - public function testTsxRequiredForParityTesting(): void { + public function testTransloaditCliRequiredForParityTesting(): void { if (getenv('TEST_NODE_PARITY') !== '1') { $this->markTestSkipped('Parity testing not enabled'); } - // Temporarily override PATH to simulate missing tsx + // Temporarily override PATH to simulate missing npm $originalPath = getenv('PATH'); - putenv('PATH=/usr/bin:/bin'); + putenv('PATH='); try { $params = [ @@ -323,10 +379,9 @@ public function testTsxRequiredForParityTesting(): void { 'auth_secret' => 'test' ]; $this->getExpectedUrl($params); - $this->fail('Expected RuntimeException when tsx is not available'); + $this->fail('Expected RuntimeException when npm is not available'); } catch (\RuntimeException $e) { - $this->assertStringContainsString('tsx command not found', $e->getMessage()); - $this->assertStringContainsString('npm install -g tsx', $e->getMessage()); + $this->assertStringContainsString('npm command not found', $e->getMessage()); } finally { // Restore original PATH putenv("PATH=$originalPath"); diff --git a/test/system/TransloaditRequest/TransloaditRequestGetBillTest.php b/test/system/TransloaditRequest/TransloaditRequestGetBillTest.php index 262f8b5..a5fed0d 100644 --- a/test/system/TransloaditRequest/TransloaditRequestGetBillTest.php +++ b/test/system/TransloaditRequest/TransloaditRequestGetBillTest.php @@ -7,6 +7,16 @@ public function testRoot() { $this->request->setMethodAndPath('GET', '/bill/' . date('Y-m')); $response = $this->request->execute(); - $this->assertStringContainsString('BILL', $response->data['ok']); + if (isset($response->data['ok'])) { + $this->assertStringContainsString('BILL', $response->data['ok']); + return; + } + + $this->assertArrayHasKey( + 'error', + $response->data, + 'Bill response should include ok or error field' + ); + $this->assertStringContainsString('BILL', (string) $response->data['error']); } } diff --git a/tool/node-smartcdn-sig.ts b/tool/node-smartcdn-sig.ts deleted file mode 100755 index 2873f84..0000000 --- a/tool/node-smartcdn-sig.ts +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env tsx -// Reference Smart CDN (https://transloadit.com/services/content-delivery/) Signature implementation -// And CLI tester to see if our SDK's implementation -// matches Node's - -/// - -import { createHash, createHmac } from 'crypto' - -interface SmartCDNParams { - workspace: string - template: string - input: string - expire_at_ms?: number - auth_key?: string - auth_secret?: string - url_params?: Record -} - -function signSmartCDNUrl(params: SmartCDNParams): string { - const { - workspace, - template, - input, - expire_at_ms, - auth_key, - auth_secret, - url_params = {}, - } = params - - if (!workspace) throw new Error('workspace is required') - if (!template) throw new Error('template is required') - if (input === null || input === undefined) - throw new Error('input must be a string') - if (!auth_key) throw new Error('auth_key is required') - if (!auth_secret) throw new Error('auth_secret is required') - - const workspaceSlug = encodeURIComponent(workspace) - const templateSlug = encodeURIComponent(template) - const inputField = encodeURIComponent(input) - - const expireAt = expire_at_ms ?? Date.now() + 60 * 60 * 1000 // 1 hour default - - const queryParams: Record = {} - - // Handle url_params - Object.entries(url_params).forEach(([key, value]) => { - if (value === null || value === undefined) return - if (Array.isArray(value)) { - value.forEach((val) => { - if (val === null || val === undefined) return - ;(queryParams[key] ||= []).push(String(val)) - }) - } else { - queryParams[key] = [String(value)] - } - }) - - queryParams.auth_key = [auth_key] - queryParams.exp = [String(expireAt)] - - // Sort parameters to ensure consistent ordering - const sortedParams = Object.entries(queryParams) - .sort() - .map(([key, values]) => - values.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`) - ) - .flat() - .join('&') - - const stringToSign = `${workspaceSlug}/${templateSlug}/${inputField}?${sortedParams}` - const signature = createHmac('sha256', auth_secret) - .update(stringToSign) - .digest('hex') - - const finalParams = `${sortedParams}&sig=${encodeURIComponent( - `sha256:${signature}` - )}` - return `https://${workspaceSlug}.tlcdn.com/${templateSlug}/${inputField}?${finalParams}` -} - -// Read JSON from stdin -let jsonInput = '' -process.stdin.on('data', (chunk) => { - jsonInput += chunk -}) - -process.stdin.on('end', () => { - const params = JSON.parse(jsonInput) - console.log(signSmartCDNUrl(params)) -})