From 5e9a7b97b846ee85cb8a71704c8fcb47133c62ed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 00:13:26 +0200 Subject: [PATCH 01/12] feat: php sdk --- .github/workflows/ci.yml | 37 ++++++ .github/workflows/packagist.yml | 44 +++++++ .gitignore | 58 +++++---- .php-cs-fixer.php | 38 ++++++ README.md | 37 ++++++ composer.json | 44 +++++++ phpstan.neon | 7 ++ phpunit.xml | 27 +++++ src/Http/HttpClient.php | 124 ++++++++++++++++++++ src/InfisicalSDK.php | 67 +++++++++++ src/Models/CreateSecretParameters.php | 58 +++++++++ src/Models/DeleteSecretParameters.php | 48 ++++++++ src/Models/GetSecretParameters.php | 66 +++++++++++ src/Models/ListSecretsParameters.php | 77 ++++++++++++ src/Models/MachineIdentityCredential.php | 50 ++++++++ src/Models/Secret.php | 57 +++++++++ src/Models/SecretImport.php | 49 ++++++++ src/Models/UpdateSecretParameters.php | 56 +++++++++ src/Services/AuthService.php | 37 ++++++ src/Services/SecretsService.php | 143 +++++++++++++++++++++++ src/Services/UniversalAuthService.php | 54 +++++++++ 21 files changed, 1157 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/packagist.yml create mode 100644 .php-cs-fixer.php create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 src/Http/HttpClient.php create mode 100644 src/InfisicalSDK.php create mode 100644 src/Models/CreateSecretParameters.php create mode 100644 src/Models/DeleteSecretParameters.php create mode 100644 src/Models/GetSecretParameters.php create mode 100644 src/Models/ListSecretsParameters.php create mode 100644 src/Models/MachineIdentityCredential.php create mode 100644 src/Models/Secret.php create mode 100644 src/Models/SecretImport.php create mode 100644 src/Models/UpdateSecretParameters.php create mode 100644 src/Services/AuthService.php create mode 100644 src/Services/SecretsService.php create mode 100644 src/Services/UniversalAuthService.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..603377f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + php: [8.0, 8.1, 8.2, 8.3] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite + coverage: xdebug + + - name: Install dependencies + run: composer install + + - name: Run PHPStan + run: composer phpstan + + - name: Run CodeSniffer + run: composer cs + + - name: Validate composer.json + run: composer validate + + - name: Check for security vulnerabilities + run: composer audit --format=json --no-interaction || true diff --git a/.github/workflows/packagist.yml b/.github/workflows/packagist.yml new file mode 100644 index 0000000..35f573f --- /dev/null +++ b/.github/workflows/packagist.yml @@ -0,0 +1,44 @@ +name: Deploy to Packagist + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.0" + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite + + - name: Install dependencies + run: composer install --no-dev --optimize-autoloader + + - name: Validate composer.json + run: composer validate + + - name: Check for security vulnerabilities + run: composer audit --format=json --no-interaction || true + + - name: Deploy to Packagist + run: | + echo "Triggering Packagist update for package: infisical/php-sdk" + + # Get the release tag from the GitHub context + TAG_NAME=${GITHUB_REF#refs/tags/} + + # Trigger Packagist update via their API + curl -X POST \ + -H "Content-Type: application/json" \ + -d "{\"repository\":{\"url\":\"https://github.com/infisical/php-sdk\"}}" \ + "https://packagist.org/api/update-package?username=${{ secrets.PACKAGIST_USERNAME }}&apiToken=${{ secrets.PACKAGIST_TOKEN }}" + + echo "Packagist update triggered for tag: $TAG_NAME" + echo "Package will be available on Packagist within a few minutes." diff --git a/.gitignore b/.gitignore index d69f71f..b05cff3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,37 @@ -# the composer package lock file and install directory -# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# /composer.lock -/fuel/vendor - -# the fuelphp document -/docs/ - -# you may install these packages with `oil package`. -# http://fuelphp.com/docs/packages/oil/package.html -# /fuel/packages/auth/ -# /fuel/packages/email/ -# /fuel/packages/oil/ -# /fuel/packages/orm/ -# /fuel/packages/parser/ - -# dynamically generated files -/fuel/app/logs/*/*/* -/fuel/app/cache/*/* -/fuel/app/config/crypt.php +# Composer +/vendor/ +composer.lock + +# PHPUnit +.phpunit.cache/ +.phpunit.result.cache + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Environment files +.env +.env.local +.env.*.local + +# Build artifacts +/build/ +/dist/ + +# Coverage reports +/coverage/ +/html-coverage/ + +# Temporary files +*.tmp +*.temp diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..00d681a --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,38 @@ +in([__DIR__ . '/src', __DIR__ . '/tests']) + ->exclude('vendor'); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => [ + 'break', + 'continue', + 'declare', + 'return', + 'throw', + 'try', + ], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + ]) + ->setFinder($finder); diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ea9963 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +

+ infisical +

+

+

Infisical PHP SDK

+

+ | Slack | + Infisical | + Documentation | +

+ +

+ + Infisical PHP SDK is released under the MIT license. + + + Slack community channel + + + Infisical Twitter + +

+ +## Introduction + +**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations. + +## Documentation +You can find the documentation for the PHP SDK on our [SDK documentation page](https://infisical.com/docs/sdks/languages/php) + +## Security + +Please do not file GitHub issues or post on our public forum for security vulnerabilities, as they are public! + +Infisical takes security issues very seriously. If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible. + +Note that this security address should be used only for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..da09a58 --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "infisical/php-sdk", + "description": "Official PHP SDK for Infisical", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Daniel Hougaard", + "email": "daniel@infisical.com" + } + ], + "require": { + "php": ">=8.0", + "guzzlehttp/guzzle": "^7.0", + "cuyz/valinor": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "phpstan/phpstan": "^1.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "autoload": { + "psr-4": { + "Infisical\\SDK\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Infisical\\SDK\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit", + "phpstan": "phpstan analyse src tests", + "cs": "phpcs src tests", + "cs-fix": "phpcbf src tests" + }, + "config": { + "sort-packages": true, + "optimize-autoloader": true + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..9f76b18 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +parameters: + level: 8 + paths: + - src + - tests + excludePaths: + - vendor diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..49e2da9 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ + + + + + tests + + + + + src + + + vendor + tests + + + + + + diff --git a/src/Http/HttpClient.php b/src/Http/HttpClient.php new file mode 100644 index 0000000..30ed0df --- /dev/null +++ b/src/Http/HttpClient.php @@ -0,0 +1,124 @@ + $headers + */ + public function __construct(string $baseUrl, array $headers = []) + { + $this->baseUrl = rtrim($baseUrl, '/'); + $this->client = new Client( + [ + 'base_uri' => $this->baseUrl, + 'timeout' => 30, + 'headers' => array_merge( + [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], $headers + ), + ] + ); + } + + /** + * Send a GET request + * + * @param string $endpoint + * @param array $query + * @param array $headers + * @return ResponseInterface + * @throws GuzzleException + */ + public function get(string $endpoint, array $query = [], array $headers = []): ResponseInterface + { + return $this->client->get( + $endpoint, [ + 'query' => $query, + 'headers' => $headers, + ] + ); + } + + /** + * Send a POST request + * + * @param string $endpoint + * @param array $data + * @param array $headers + * @return ResponseInterface + * @throws GuzzleException + */ + public function post(string $endpoint, array $data = [], array $headers = []): ResponseInterface + { + return $this->client->post( + $endpoint, [ + 'json' => $data, + 'headers' => $headers, + ] + ); + } + + /** + * Send a PATCH request + * + * @param string $endpoint + * @param array $data + * @param array $headers + * @return ResponseInterface + * @throws GuzzleException + */ + public function patch(string $endpoint, array $data = [], array $headers = []): ResponseInterface + { + return $this->client->patch( + $endpoint, [ + 'json' => $data, + 'headers' => $headers, + ] + ); + } + + /** + * Send a DELETE request + * + * @param string $endpoint + * @param array $data + * @param array $headers + * @return ResponseInterface + * @throws GuzzleException + */ + public function delete(string $endpoint, array $data = [], array $headers = []): ResponseInterface + { + return $this->client->delete( + $endpoint, [ + 'json' => $data, + 'headers' => $headers, + ] + ); + } + + /** + * Get the base URL + * + * @return string + */ + public function getBaseUrl(): string + { + return $this->baseUrl; + } +} diff --git a/src/InfisicalSDK.php b/src/InfisicalSDK.php new file mode 100644 index 0000000..ec7d173 --- /dev/null +++ b/src/InfisicalSDK.php @@ -0,0 +1,67 @@ +host = $host; + $this->httpClient = new HttpClient($host); + + $this->secretsService = new SecretsService($this->httpClient); + $this->authService = new AuthService($this->httpClient, fn(string $token) => $this->onAuthenticate($token)); + } + + /** + * Get the secrets service + * + * @return SecretsService + */ + public function secrets(): SecretsService + { + return $this->secretsService; + } + + /** + * Get the auth service + * + * @return AuthService + */ + public function auth(): AuthService + { + return $this->authService; + } + + /** + * Handle authentication callback + */ + private function onAuthenticate(string $accessToken): void + { + $this->httpClient = new HttpClient($this->host, [ + 'Authorization' => 'Bearer ' . $accessToken, + ]); + + $this->secretsService = new SecretsService($this->httpClient); + $this->authService = new AuthService($this->httpClient, fn(string $token) => $this->onAuthenticate($token)); + } +} diff --git a/src/Models/CreateSecretParameters.php b/src/Models/CreateSecretParameters.php new file mode 100644 index 0000000..f995a43 --- /dev/null +++ b/src/Models/CreateSecretParameters.php @@ -0,0 +1,58 @@ + + */ + public function toArray(): array + { + $params = []; + + if ($this->environment !== null) { + $params['environment'] = $this->environment; + } + + if ($this->projectId !== null) { + $params['workspaceId'] = $this->projectId; + } + + if ($this->secretPath !== null) { + $params['secretPath'] = $this->secretPath; + } + + if ($this->secretKey !== null) { + $params['secretKey'] = $this->secretKey; + } + + if ($this->secretValue !== null) { + $params['secretValue'] = $this->secretValue; + } + + if ($this->secretComment !== null) { + $params['secretComment'] = $this->secretComment; + } + + return $params; + } + +} diff --git a/src/Models/DeleteSecretParameters.php b/src/Models/DeleteSecretParameters.php new file mode 100644 index 0000000..dd8046f --- /dev/null +++ b/src/Models/DeleteSecretParameters.php @@ -0,0 +1,48 @@ + + */ + public function toArray(): array + { + $params = []; + + if ($this->secretKey !== null) { + $params['secretKey'] = $this->secretKey; + } + + if ($this->environment !== null) { + $params['environment'] = $this->environment; + } + + if ($this->projectId !== null) { + $params['workspaceId'] = $this->projectId; + } + + if ($this->secretPath !== null) { + $params['secretPath'] = $this->secretPath; + } + + return $params; + } + +} diff --git a/src/Models/GetSecretParameters.php b/src/Models/GetSecretParameters.php new file mode 100644 index 0000000..4937a9c --- /dev/null +++ b/src/Models/GetSecretParameters.php @@ -0,0 +1,66 @@ + + */ + public function toArray(): array + { + $params = []; + + if ($this->secretKey !== null) { + $params['secretKey'] = $this->secretKey; + } + + if ($this->environment !== null) { + $params['environment'] = $this->environment; + } + + if ($this->projectId !== null) { + $params['workspaceId'] = $this->projectId; + } + + if ($this->secretPath !== null) { + $params['secretPath'] = $this->secretPath; + } + + if ($this->version !== null) { + $params['version'] = $this->version; + } + + if ($this->type !== null) { + $params['type'] = $this->type; + } + + if ($this->expandSecretReferences !== null) { + $params['expandSecretReferences'] = $this->boolToString($this->expandSecretReferences); + } + + return $params; + } + private function boolToString(bool $value): string + { + return $value ? 'true' : 'false'; + } +} diff --git a/src/Models/ListSecretsParameters.php b/src/Models/ListSecretsParameters.php new file mode 100644 index 0000000..398fdc9 --- /dev/null +++ b/src/Models/ListSecretsParameters.php @@ -0,0 +1,77 @@ +|null + */ + public readonly ?array $tagSlugs = null, + public readonly ?bool $attach_to_process_env = null, + public readonly ?bool $skipUniqueValidation = null, + ) { + } + + /** + * Convert to array for HTTP request + * + * @return array + */ + public function toArray(): array + { + $params = []; + + if ($this->environment !== null) { + $params['environment'] = $this->environment; + } + + if ($this->secretPath !== null) { + $params['secretPath'] = $this->secretPath; + } + + if ($this->projectId !== null) { + $params['workspaceId'] = $this->projectId; + } + + if ($this->expandSecretReferences !== null) { + $params['expandSecretReferences'] = $this->boolToString($this->expandSecretReferences); + } + + if ($this->recursive !== null) { + $params['recursive'] = $this->boolToString($this->recursive); + } + + if ($this->tagSlugs !== null) { + $params['tagSlugs'] = implode(',', $this->tagSlugs); + } + + if ($this->attach_to_process_env !== null) { + $params['attach_to_process_env'] = $this->attach_to_process_env; + } + + if ($this->skipUniqueValidation !== null) { + $params['skipUniqueValidation'] = $this->skipUniqueValidation; + } + + // We forcefully include imports as we're trying to move to a structure where users won't have to worry about imports vs. secrets. + $params["include_imports"] = "true"; + + return $params; + } + private function boolToString(bool $value): string + { + return $value ? 'true' : 'false'; + } +} diff --git a/src/Models/MachineIdentityCredential.php b/src/Models/MachineIdentityCredential.php new file mode 100644 index 0000000..f49fd73 --- /dev/null +++ b/src/Models/MachineIdentityCredential.php @@ -0,0 +1,50 @@ + $data + * @return self + */ + public static function fromArray(array $data): self + { + return new self( + $data['accessToken'] ?? '', + $data['expiresIn'] ?? 0, + $data['accessTokenMaxTTL'] ?? 0, + $data['tokenType'] ?? '' + ); + } + + /** + * Convert to array + * + * @return array + */ + public function toArray(): array + { + return [ + 'accessToken' => $this->accessToken, + 'expiresIn' => $this->expiresIn, + 'accessTokenMaxTTL' => $this->accessTokenMaxTTL, + 'tokenType' => $this->tokenType, + ]; + } +} diff --git a/src/Models/Secret.php b/src/Models/Secret.php new file mode 100644 index 0000000..9afbdc2 --- /dev/null +++ b/src/Models/Secret.php @@ -0,0 +1,57 @@ + $data + */ + public static function fromArray(array $data): self + { + return new self( + $data['secretKey'] ?? '', + $data['secretValue'] ?? '', + $data['id'] ?? '', + $data['workspaceId'] ?? '', + $data['environment'] ?? '', + $data['version'] ?? 0, + $data['type'] ?? '', + $data['secretComment'] ?? '', + $data['skipMultilineEncoding'] ?? '', + $data['secretPath'] ?? '', + $data['secretValueHidden'] ?? false, + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'secretKey' => $this->secretKey, + 'secretValue' => $this->secretValue, + ]; + } +} diff --git a/src/Models/SecretImport.php b/src/Models/SecretImport.php new file mode 100644 index 0000000..54bb9dd --- /dev/null +++ b/src/Models/SecretImport.php @@ -0,0 +1,49 @@ + $data + */ + public static function fromArray(array $data): self + { + return new self( + $data['secretPath'] ?? '', + $data['environment'] ?? '', + $data['folderId'] ?? '', + array_map(fn($data) => Secret::fromArray($data), $data['secrets'] ?? []), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'secretPath' => $this->secretPath, + 'environment' => $this->environment, + 'folderId' => $this->folderId, + 'secrets' => array_map(fn($secret) => $secret->toArray(), $this->secrets), + ]; + } +} diff --git a/src/Models/UpdateSecretParameters.php b/src/Models/UpdateSecretParameters.php new file mode 100644 index 0000000..376c217 --- /dev/null +++ b/src/Models/UpdateSecretParameters.php @@ -0,0 +1,56 @@ + + */ + public function toArray(): array + { + $params = []; + + if ($this->environment !== null) { + $params['environment'] = $this->environment; + } + + if ($this->projectId !== null) { + $params['workspaceId'] = $this->projectId; + } + + if ($this->secretPath !== null) { + $params['secretPath'] = $this->secretPath; + } + + if ($this->newSecretValue !== null) { + $params['secretValue'] = $this->newSecretValue; + } + + if ($this->newSecretComment !== null) { + $params['secretComment'] = $this->newSecretComment; + } + + $params['secretKey'] = $this->secretKey; + + return $params; + } +} diff --git a/src/Services/AuthService.php b/src/Services/AuthService.php new file mode 100644 index 0000000..33e00f1 --- /dev/null +++ b/src/Services/AuthService.php @@ -0,0 +1,37 @@ +httpClient = $httpClient; + $this->onAuthenticate = $onAuthenticate; + } + + /** + * Get the universal auth service + * + * @return UniversalAuthService + */ + public function universal_auth(): UniversalAuthService + { + return new UniversalAuthService($this->httpClient, $this->onAuthenticate); + } +} diff --git a/src/Services/SecretsService.php b/src/Services/SecretsService.php new file mode 100644 index 0000000..6aa7e73 --- /dev/null +++ b/src/Services/SecretsService.php @@ -0,0 +1,143 @@ +httpClient = $httpClient; + } + + /** + * List secrets with optional filtering and pagination + * + * @param ListSecretsParameters|null $parameters Optional parameters for filtering and pagination + * @return array Array of Secret objects + */ + public function list(?ListSecretsParameters $parameters = null): array + { + $response = $this->httpClient->get('/api/v3/secrets/raw', $parameters?->toArray() ?? []); + $responseData = json_decode($response->getBody()->getContents(), true); + + + $secrets = array_map(fn($data) => Secret::fromArray($data), $responseData['secrets'] ?? []); + + // Ensure unique secrets by key before processing imports + if ($parameters?->recursive) { + $secrets = $this->ensureUniqueSecretsByKey($secrets, $parameters?->skipUniqueValidation ?? false); + } + + + // Handle imports - imports take precedence over secrets + if (isset($responseData['imports'])) { + foreach ($responseData['imports'] as $importBlock) { + foreach ($importBlock['secrets'] as $importSecretData) { + $importSecret = Secret::fromArray($importSecretData); + + // Only append if not already in the list (imports take precedence) + if (!$this->containsSecret($secrets, $importSecret->secretKey)) { + $secrets[] = $importSecret; + } + } + } + } + + if ($parameters?->attach_to_process_env) { + foreach ($secrets as $secret) { + putenv($secret->secretKey . '=' . $secret->secretValue); + } + } + + return $secrets; + } + + /** + * Get a single secret by key + * + * @param GetSecretParameters|null $parameters Optional parameters for filtering and pagination + * @return Secret The secret + */ + public function get(?GetSecretParameters $parameters = null): Secret + { + $response = $this->httpClient->get('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $responseData = json_decode($response->getBody()->getContents(), true); + return Secret::fromArray($responseData['secret'] ?? []); + } + + public function update(?UpdateSecretParameters $parameters = null): Secret + { + $response = $this->httpClient->patch('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $responseData = json_decode($response->getBody()->getContents(), true); + + + return Secret::fromArray($responseData['secret'] ?? []); + } + + public function delete(?DeleteSecretParameters $parameters = null): Secret + { + $response = $this->httpClient->delete('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $responseData = json_decode($response->getBody()->getContents(), true); + return Secret::fromArray($responseData['secret'] ?? []); + } + + public function create(?CreateSecretParameters $parameters = null): Secret + { + $response = $this->httpClient->post('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $responseData = json_decode($response->getBody()->getContents(), true); + return Secret::fromArray($responseData['secret'] ?? []); + } + + /** + * @param Secret[] $secrets + */ + private function containsSecret(array $secrets, string $secretKey): bool + { + foreach ($secrets as $secret) { + if ($secret->secretKey === $secretKey) { + return true; + } + } + return false; + } + + /** + * @param Secret[] $secrets + * @return Secret[] + */ + private function ensureUniqueSecretsByKey(array $secrets, bool $skipUniqueValidation): array + { + $secretMap = []; + + // Move secrets to a map to ensure uniqueness + foreach ($secrets as $secret) { + if ($skipUniqueValidation) { + // Create a composite key using both secretPath and secretKey + $key = $secret->secretPath . ':' . $secret->secretKey; + } else { + // Use only secretKey for global uniqueness + $key = $secret->secretKey; + } + $secretMap[$key] = $secret; + } + + // Return array with unique secrets + return array_values($secretMap); + } + +} diff --git a/src/Services/UniversalAuthService.php b/src/Services/UniversalAuthService.php new file mode 100644 index 0000000..e7788a8 --- /dev/null +++ b/src/Services/UniversalAuthService.php @@ -0,0 +1,54 @@ +httpClient = $httpClient; + $this->onAuthenticate = $onAuthenticate; + } + + /** + * Login with client ID and client secret + * + * @param string $clientId The client ID + * @param string $clientSecret The client secret + * @return MachineIdentityCredential The machine identity credential + */ + public function login(string $clientId, string $clientSecret): MachineIdentityCredential + { + $response = $this->httpClient->post("/api/v1/auth/universal-auth/login", [ + 'clientId' => $clientId, + 'clientSecret' => $clientSecret, + ]); + + // Parse the JSON response body + $responseData = json_decode($response->getBody()->getContents(), true); + + if (!$responseData) { + throw new \RuntimeException('Invalid response from authentication server'); + } + + $credential = MachineIdentityCredential::fromArray($responseData); + call_user_func($this->onAuthenticate, $credential->accessToken); + + return $credential; + } +} From 2782dd055f5fbc19988cfb70f2988cd01d1d0ebc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 00:27:11 +0200 Subject: [PATCH 02/12] Update packagist.yml --- .github/workflows/packagist.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/packagist.yml b/.github/workflows/packagist.yml index 35f573f..324f06e 100644 --- a/.github/workflows/packagist.yml +++ b/.github/workflows/packagist.yml @@ -1,8 +1,9 @@ name: Deploy to Packagist on: - release: - types: [published] + push: + tags: + - "v*.*.*" jobs: deploy: @@ -27,18 +28,17 @@ jobs: - name: Check for security vulnerabilities run: composer audit --format=json --no-interaction || true - - name: Deploy to Packagist - run: | - echo "Triggering Packagist update for package: infisical/php-sdk" - - # Get the release tag from the GitHub context - TAG_NAME=${GITHUB_REF#refs/tags/} - - # Trigger Packagist update via their API - curl -X POST \ - -H "Content-Type: application/json" \ - -d "{\"repository\":{\"url\":\"https://github.com/infisical/php-sdk\"}}" \ - "https://packagist.org/api/update-package?username=${{ secrets.PACKAGIST_USERNAME }}&apiToken=${{ secrets.PACKAGIST_TOKEN }}" + publish-to-packagist: + name: publish + runs-on: ubuntu-latest + needs: deploy - echo "Packagist update triggered for tag: $TAG_NAME" - echo "Package will be available on Packagist within a few minutes." + steps: + - uses: actions/checkout@v4 + + - name: Publish to Packagist + run: |- + curl --fail-with-body -X POST -H 'Content-Type: application/json' "https://packagist.org/api/update-package?username=${PACKAGIST_USERNAME}&apiToken=${PACKAGIST_API_KEY}" -d '{"repository":"https://www.github.com/infisical/php-sdk"}' + env: + PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }} + PACKAGIST_API_KEY: ${{ secrets.PACKAGIST_API_KEY }} From 9354d796f036cbc873bd10cb1717430c88603f1c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 00:29:34 +0200 Subject: [PATCH 03/12] Update packagist.yml --- .github/workflows/packagist.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packagist.yml b/.github/workflows/packagist.yml index 324f06e..01699dc 100644 --- a/.github/workflows/packagist.yml +++ b/.github/workflows/packagist.yml @@ -38,7 +38,7 @@ jobs: - name: Publish to Packagist run: |- - curl --fail-with-body -X POST -H 'Content-Type: application/json' "https://packagist.org/api/update-package?username=${PACKAGIST_USERNAME}&apiToken=${PACKAGIST_API_KEY}" -d '{"repository":"https://www.github.com/infisical/php-sdk"}' + curl --fail-with-body -X POST -H 'Content-Type: application/json' "https://packagist.org/api/update-package?username=${PACKAGIST_USERNAME}&apiToken=${PACKAGIST_API_KEY}" -d '{"repository":"github.com/Infisical/php-sdk"}' env: PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }} PACKAGIST_API_KEY: ${{ secrets.PACKAGIST_API_KEY }} From 4e801e8321ab2c0411539d4a53ab018918ddd438 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 00:55:41 +0200 Subject: [PATCH 04/12] Update .php-cs-fixer.php --- .php-cs-fixer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 00d681a..2171e83 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -3,7 +3,7 @@ require_once __DIR__ . '/vendor/autoload.php'; $finder = PhpCsFixer\Finder::create() - ->in([__DIR__ . '/src', __DIR__ . '/tests']) + ->in([__DIR__ . '/src']) ->exclude('vendor'); return (new PhpCsFixer\Config()) From 06770a2127613c92b0de8a1dcc478903dc07bbcd Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 00:56:24 +0200 Subject: [PATCH 05/12] Update phpstan.neon --- phpstan.neon | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 9f76b18..97e114f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,5 @@ parameters: level: 8 paths: - src - - tests excludePaths: - vendor From d86575d4c8ddd9cefb95aaf23eb0470d9308717d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 01:09:26 +0200 Subject: [PATCH 06/12] requested changes --- .github/workflows/ci.yml | 2 +- .github/workflows/packagist.yml | 2 +- src/Models/GetSecretParameters.php | 2 +- src/Models/ListSecretsParameters.php | 6 +++--- src/Models/Secret.php | 2 +- src/Models/SecretImport.php | 2 +- src/Models/UpdateSecretParameters.php | 8 +++++++- src/Services/AuthService.php | 2 +- src/Services/SecretsService.php | 10 +++++----- 9 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 603377f..df20211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,4 +34,4 @@ jobs: run: composer validate - name: Check for security vulnerabilities - run: composer audit --format=json --no-interaction || true + run: composer audit --format=json --no-interaction diff --git a/.github/workflows/packagist.yml b/.github/workflows/packagist.yml index 01699dc..5e0e4ed 100644 --- a/.github/workflows/packagist.yml +++ b/.github/workflows/packagist.yml @@ -26,7 +26,7 @@ jobs: run: composer validate - name: Check for security vulnerabilities - run: composer audit --format=json --no-interaction || true + run: composer audit --format=json --no-interaction publish-to-packagist: name: publish diff --git a/src/Models/GetSecretParameters.php b/src/Models/GetSecretParameters.php index 4937a9c..b330b09 100644 --- a/src/Models/GetSecretParameters.php +++ b/src/Models/GetSecretParameters.php @@ -15,7 +15,7 @@ public function __construct( public readonly ?string $projectId = null, public readonly ?string $secretPath = null, public readonly ?int $version = null, - public readonly ?bool $type = null, + public readonly ?string $type = null, public readonly ?bool $expandSecretReferences = true, ) { } diff --git a/src/Models/ListSecretsParameters.php b/src/Models/ListSecretsParameters.php index 398fdc9..ee6ed88 100644 --- a/src/Models/ListSecretsParameters.php +++ b/src/Models/ListSecretsParameters.php @@ -19,7 +19,7 @@ public function __construct( * @var array|null */ public readonly ?array $tagSlugs = null, - public readonly ?bool $attach_to_process_env = null, + public readonly ?bool $attachToProcessEnv = null, public readonly ?bool $skipUniqueValidation = null, ) { } @@ -57,8 +57,8 @@ public function toArray(): array $params['tagSlugs'] = implode(',', $this->tagSlugs); } - if ($this->attach_to_process_env !== null) { - $params['attach_to_process_env'] = $this->attach_to_process_env; + if ($this->attachToProcessEnv !== null) { + $params['attachToProcessEnv'] = $this->attachToProcessEnv; } if ($this->skipUniqueValidation !== null) { diff --git a/src/Models/Secret.php b/src/Models/Secret.php index 9afbdc2..6beef73 100644 --- a/src/Models/Secret.php +++ b/src/Models/Secret.php @@ -38,7 +38,7 @@ public static function fromArray(array $data): self $data['version'] ?? 0, $data['type'] ?? '', $data['secretComment'] ?? '', - $data['skipMultilineEncoding'] ?? '', + $data['skipMultilineEncoding'] ?? false, $data['secretPath'] ?? '', $data['secretValueHidden'] ?? false, ); diff --git a/src/Models/SecretImport.php b/src/Models/SecretImport.php index 54bb9dd..1987a79 100644 --- a/src/Models/SecretImport.php +++ b/src/Models/SecretImport.php @@ -30,7 +30,7 @@ public static function fromArray(array $data): self $data['secretPath'] ?? '', $data['environment'] ?? '', $data['folderId'] ?? '', - array_map(fn($data) => Secret::fromArray($data), $data['secrets'] ?? []), + array_map(fn($secretData) => Secret::fromArray($secretData), $data['secrets'] ?? []), ); } diff --git a/src/Models/UpdateSecretParameters.php b/src/Models/UpdateSecretParameters.php index 376c217..abed330 100644 --- a/src/Models/UpdateSecretParameters.php +++ b/src/Models/UpdateSecretParameters.php @@ -49,7 +49,13 @@ public function toArray(): array $params['secretComment'] = $this->newSecretComment; } - $params['secretKey'] = $this->secretKey; + if ($this->newSecretKey !== null) { + $params['newSecretName'] = $this->newSecretKey; + } + + if ($this->secretKey !== null) { + $params['secretKey'] = $this->secretKey; + } return $params; } diff --git a/src/Services/AuthService.php b/src/Services/AuthService.php index 33e00f1..e447fb2 100644 --- a/src/Services/AuthService.php +++ b/src/Services/AuthService.php @@ -30,7 +30,7 @@ public function __construct(HttpClient $httpClient, $onAuthenticate) * * @return UniversalAuthService */ - public function universal_auth(): UniversalAuthService + public function universalAuth(): UniversalAuthService { return new UniversalAuthService($this->httpClient, $this->onAuthenticate); } diff --git a/src/Services/SecretsService.php b/src/Services/SecretsService.php index 6aa7e73..5fce80e 100644 --- a/src/Services/SecretsService.php +++ b/src/Services/SecretsService.php @@ -58,7 +58,7 @@ public function list(?ListSecretsParameters $parameters = null): array } } - if ($parameters?->attach_to_process_env) { + if ($parameters?->attachToProcessEnv) { foreach ($secrets as $secret) { putenv($secret->secretKey . '=' . $secret->secretValue); } @@ -75,14 +75,14 @@ public function list(?ListSecretsParameters $parameters = null): array */ public function get(?GetSecretParameters $parameters = null): Secret { - $response = $this->httpClient->get('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $response = $this->httpClient->get('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); $responseData = json_decode($response->getBody()->getContents(), true); return Secret::fromArray($responseData['secret'] ?? []); } public function update(?UpdateSecretParameters $parameters = null): Secret { - $response = $this->httpClient->patch('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $response = $this->httpClient->patch('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); $responseData = json_decode($response->getBody()->getContents(), true); @@ -91,14 +91,14 @@ public function update(?UpdateSecretParameters $parameters = null): Secret public function delete(?DeleteSecretParameters $parameters = null): Secret { - $response = $this->httpClient->delete('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $response = $this->httpClient->delete('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); $responseData = json_decode($response->getBody()->getContents(), true); return Secret::fromArray($responseData['secret'] ?? []); } public function create(?CreateSecretParameters $parameters = null): Secret { - $response = $this->httpClient->post('/api/v3/secrets/raw/' . $parameters?->secretKey, $parameters?->toArray() ?? []); + $response = $this->httpClient->post('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); $responseData = json_decode($response->getBody()->getContents(), true); return Secret::fromArray($responseData['secret'] ?? []); } From 606f2c237ac734015407f853b00d645d20ab7c08 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 01:13:41 +0200 Subject: [PATCH 07/12] testing --- .php-cs-fixer.php | 2 +- src/Services/SecretsService.php | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 2171e83..00d681a 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -3,7 +3,7 @@ require_once __DIR__ . '/vendor/autoload.php'; $finder = PhpCsFixer\Finder::create() - ->in([__DIR__ . '/src']) + ->in([__DIR__ . '/src', __DIR__ . '/tests']) ->exclude('vendor'); return (new PhpCsFixer\Config()) diff --git a/src/Services/SecretsService.php b/src/Services/SecretsService.php index 5fce80e..f78d49b 100644 --- a/src/Services/SecretsService.php +++ b/src/Services/SecretsService.php @@ -75,30 +75,45 @@ public function list(?ListSecretsParameters $parameters = null): array */ public function get(?GetSecretParameters $parameters = null): Secret { - $response = $this->httpClient->get('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); + if ($parameters?->secretKey === null) { + throw new \InvalidArgumentException('secretKey is required'); + } + + $response = $this->httpClient->get('/api/v3/secrets/raw/' . urlencode($parameters->secretKey), $parameters->toArray()); $responseData = json_decode($response->getBody()->getContents(), true); return Secret::fromArray($responseData['secret'] ?? []); } public function update(?UpdateSecretParameters $parameters = null): Secret { - $response = $this->httpClient->patch('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); + if ($parameters?->secretKey === null) { + throw new \InvalidArgumentException('secretKey is required'); + } + + $response = $this->httpClient->patch('/api/v3/secrets/raw/' . urlencode($parameters->secretKey), $parameters->toArray()); $responseData = json_decode($response->getBody()->getContents(), true); - return Secret::fromArray($responseData['secret'] ?? []); } public function delete(?DeleteSecretParameters $parameters = null): Secret { - $response = $this->httpClient->delete('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); + if ($parameters?->secretKey === null) { + throw new \InvalidArgumentException('secretKey is required'); + } + + $response = $this->httpClient->delete('/api/v3/secrets/raw/' . urlencode($parameters->secretKey), $parameters->toArray()); $responseData = json_decode($response->getBody()->getContents(), true); return Secret::fromArray($responseData['secret'] ?? []); } public function create(?CreateSecretParameters $parameters = null): Secret { - $response = $this->httpClient->post('/api/v3/secrets/raw/' . urlencode($parameters?->secretKey), $parameters?->toArray() ?? []); + if ($parameters?->secretKey === null) { + throw new \InvalidArgumentException('secretKey is required'); + } + + $response = $this->httpClient->post('/api/v3/secrets/raw/' . urlencode($parameters->secretKey), $parameters->toArray()); $responseData = json_decode($response->getBody()->getContents(), true); return Secret::fromArray($responseData['secret'] ?? []); } From df4e685af63ff986523440e9de19e9943e6aaf52 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 01:15:03 +0200 Subject: [PATCH 08/12] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index da09a58..0bc14e5 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ }, "scripts": { "test": "phpunit", - "phpstan": "phpstan analyse src tests", + "phpstan": "phpstan analyse src", "cs": "phpcs src tests", "cs-fix": "phpcbf src tests" }, From 8f772e5a382247b23613d5ce9a81dd1fca5ef0b5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 01:16:27 +0200 Subject: [PATCH 09/12] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df20211..af5cbf7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [8.0, 8.1, 8.2, 8.3] + php: [8.1, 8.2, 8.3] steps: - name: Checkout code From ea147b7e5d138c30c3688801c53b0553f7efc386 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 01:17:20 +0200 Subject: [PATCH 10/12] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0bc14e5..6699177 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,8 @@ "scripts": { "test": "phpunit", "phpstan": "phpstan analyse src", - "cs": "phpcs src tests", - "cs-fix": "phpcbf src tests" + "cs": "phpcs src", + "cs-fix": "phpcbf src" }, "config": { "sort-packages": true, From 1d4dbc9937982d076c496476fe540485d1d99aa9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 01:18:05 +0200 Subject: [PATCH 11/12] further testing --- .github/workflows/packagist.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/packagist.yml b/.github/workflows/packagist.yml index 5e0e4ed..37728a1 100644 --- a/.github/workflows/packagist.yml +++ b/.github/workflows/packagist.yml @@ -16,7 +16,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.0" + php-version: "8.1" extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite - name: Install dependencies diff --git a/composer.json b/composer.json index 6699177..159525c 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": ">=8.0", + "php": ">=8.1", "guzzlehttp/guzzle": "^7.0", "cuyz/valinor": "^1.0" }, From ea3408a34d272ea1b835e92b843c0a04e0d56e12 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 3 Sep 2025 01:23:53 +0200 Subject: [PATCH 12/12] minor fixes --- .github/workflows/ci.yml | 3 --- src/InfisicalSDK.php | 6 ++++-- src/Services/AuthService.php | 4 +++- src/Services/UniversalAuthService.php | 14 +++++++++----- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af5cbf7..8d3201a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,9 +27,6 @@ jobs: - name: Run PHPStan run: composer phpstan - - name: Run CodeSniffer - run: composer cs - - name: Validate composer.json run: composer validate diff --git a/src/InfisicalSDK.php b/src/InfisicalSDK.php index ec7d173..1728286 100644 --- a/src/InfisicalSDK.php +++ b/src/InfisicalSDK.php @@ -57,9 +57,11 @@ public function auth(): AuthService */ private function onAuthenticate(string $accessToken): void { - $this->httpClient = new HttpClient($this->host, [ + $this->httpClient = new HttpClient( + $this->host, [ 'Authorization' => 'Bearer ' . $accessToken, - ]); + ] + ); $this->secretsService = new SecretsService($this->httpClient); $this->authService = new AuthService($this->httpClient, fn(string $token) => $this->onAuthenticate($token)); diff --git a/src/Services/AuthService.php b/src/Services/AuthService.php index e447fb2..b8fe629 100644 --- a/src/Services/AuthService.php +++ b/src/Services/AuthService.php @@ -13,7 +13,9 @@ class AuthService { private HttpClient $httpClient; - /** @var callable(string): void */ + /** + * @var callable(string): void + */ private $onAuthenticate; /** diff --git a/src/Services/UniversalAuthService.php b/src/Services/UniversalAuthService.php index e7788a8..d137bfb 100644 --- a/src/Services/UniversalAuthService.php +++ b/src/Services/UniversalAuthService.php @@ -13,7 +13,9 @@ class UniversalAuthService { private HttpClient $httpClient; - /** @var callable(string): void */ + /** + * @var callable(string): void + */ private $onAuthenticate; /** @@ -28,16 +30,18 @@ public function __construct(HttpClient $httpClient, $onAuthenticate) /** * Login with client ID and client secret * - * @param string $clientId The client ID - * @param string $clientSecret The client secret + * @param string $clientId The client ID + * @param string $clientSecret The client secret * @return MachineIdentityCredential The machine identity credential */ public function login(string $clientId, string $clientSecret): MachineIdentityCredential { - $response = $this->httpClient->post("/api/v1/auth/universal-auth/login", [ + $response = $this->httpClient->post( + "/api/v1/auth/universal-auth/login", [ 'clientId' => $clientId, 'clientSecret' => $clientSecret, - ]); + ] + ); // Parse the JSON response body $responseData = json_decode($response->getBody()->getContents(), true);