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))
-})