diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8d3201a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ pull_request:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php: [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: Validate composer.json
+ run: composer validate
+
+ - name: Check for security vulnerabilities
+ run: composer audit --format=json --no-interaction
diff --git a/.github/workflows/packagist.yml b/.github/workflows/packagist.yml
new file mode 100644
index 0000000..37728a1
--- /dev/null
+++ b/.github/workflows/packagist.yml
@@ -0,0 +1,44 @@
+name: Deploy to Packagist
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+
+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.1"
+ 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
+
+ publish-to-packagist:
+ name: publish
+ runs-on: ubuntu-latest
+ needs: deploy
+
+ 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":"github.com/Infisical/php-sdk"}'
+ env:
+ PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }}
+ PACKAGIST_API_KEY: ${{ secrets.PACKAGIST_API_KEY }}
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 PHP SDK
+
+
+
+
+## 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..159525c
--- /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.1",
+ "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",
+ "cs": "phpcs src",
+ "cs-fix": "phpcbf src"
+ },
+ "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..97e114f
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,6 @@
+parameters:
+ level: 8
+ paths:
+ - src
+ 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..1728286
--- /dev/null
+++ b/src/InfisicalSDK.php
@@ -0,0 +1,69 @@
+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..b330b09
--- /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..ee6ed88
--- /dev/null
+++ b/src/Models/ListSecretsParameters.php
@@ -0,0 +1,77 @@
+|null
+ */
+ public readonly ?array $tagSlugs = null,
+ public readonly ?bool $attachToProcessEnv = 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->attachToProcessEnv !== null) {
+ $params['attachToProcessEnv'] = $this->attachToProcessEnv;
+ }
+
+ 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..6beef73
--- /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'] ?? false,
+ $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..1987a79
--- /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($secretData) => Secret::fromArray($secretData), $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..abed330
--- /dev/null
+++ b/src/Models/UpdateSecretParameters.php
@@ -0,0 +1,62 @@
+
+ */
+ 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;
+ }
+
+ 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
new file mode 100644
index 0000000..b8fe629
--- /dev/null
+++ b/src/Services/AuthService.php
@@ -0,0 +1,39 @@
+httpClient = $httpClient;
+ $this->onAuthenticate = $onAuthenticate;
+ }
+
+ /**
+ * Get the universal auth service
+ *
+ * @return UniversalAuthService
+ */
+ public function universalAuth(): 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..f78d49b
--- /dev/null
+++ b/src/Services/SecretsService.php
@@ -0,0 +1,158 @@
+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?->attachToProcessEnv) {
+ 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
+ {
+ 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
+ {
+ 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
+ {
+ 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
+ {
+ 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'] ?? []);
+ }
+
+ /**
+ * @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..d137bfb
--- /dev/null
+++ b/src/Services/UniversalAuthService.php
@@ -0,0 +1,58 @@
+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;
+ }
+}