> ## Documentation Index
> Fetch the complete documentation index at: https://docs.westyx.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP SDK - Workload Identity Federation

> WifConfig setup, provider auto-detection, custom token source, and token exchange flow for the PHP SDK.

For deployments that prefer not to manage static API keys, the PHP SDK can authenticate via **Workload Identity Federation (WIF)**. The SDK fetches a workload OIDC token from the running environment, exchanges it for a Nexus session JWT, and uses `Authorization: Bearer <jwt>` on every subsequent API call.

WIF is opt-in. When `NexusConfig::$wif` is `null` (the default), the SDK uses the static `apiKey` flow and behaves identically to a client without WIF configuration.

## Enabling WIF

Pass a `WifConfig` to `NexusConfig::$wif`:

```php theme={null}
use WestyxNexus\NexusClient;
use WestyxNexus\NexusConfig;
use WestyxNexus\WifConfig;

$client = NexusClient::create(new NexusConfig(
    baseUrl: 'https://blue-ocean-a5rx7.westyx.dev',
    wif: new WifConfig(provider: 'auto'),
));
```

`apiKey` becomes optional when `wif` is set. You can provide both - the SDK uses WIF and falls back to the static key if a session refresh fails after the initial connect.

<Note>
  **Security note (v0.9.0):** the `baseUrl` must use `https://` - `NexusClient::create()` rejects plain-http endpoints (loopback/localhost excepted for development), since credentials travel on every request.
</Note>

## WifConfig reference

```php theme={null}
new WifConfig(
    provider:    string   = 'auto',   // 'kubernetes' | 'aws' | 'aws_iam' | 'gcp' | 'azure' | 'auto'
    tokenSource: ?Closure = null,     // override OIDC token fetch entirely
    awsRegion:   string   = '',       // optional - pins the aws_iam STS region (else AWS chain + EC2 IMDS)
)
```

| Field         | Type       | Default  | Description                                                                                              |
| ------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `provider`    | `string`   | `'auto'` | Which provider to use for the workload OIDC token. `'auto'` probes the environment in order.             |
| `tokenSource` | `?Closure` | `null`   | Custom closure that returns a workload OIDC JWT string. When non-null, takes precedence over `provider`. |

## Provider auto-detection table

When `provider` is `'auto'`, the SDK probes the environment in this order, checking for actual credential material (the token files are stat-ed, not just env-var presence):

| Provider string | Auto-detect signal                                                   | Credential                                                                                                                                        |
| --------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `'kubernetes'`  | `/var/run/secrets/kubernetes.io/serviceaccount/token` exists         | Reads the projected service-account token file (rotated by kubelet)                                                                               |
| `'aws'`         | `AWS_WEB_IDENTITY_TOKEN_FILE` set **and the file exists** (EKS IRSA) | Reads the token file at `AWS_WEB_IDENTITY_TOKEN_FILE`                                                                                             |
| `'azure'`       | `AZURE_FEDERATED_TOKEN_FILE` set **and the file exists** (AKS)       | Reads the federated token file at `AZURE_FEDERATED_TOKEN_FILE`                                                                                    |
| `'gcp'`         | GCE metadata server reachable                                        | Fetches a Google-signed OIDC identity token from the GCE metadata server (`.../instance/service-accounts/default/identity?audience=westyx-nexus`) |

The first matching provider is used. If none match and no `tokenSource` closure is provided, `NexusClient::create` throws a `NexusException`.

`aws_iam` is **not** auto-detected - select it explicitly (resolvable AWS credentials alone, e.g. a dev laptop's `~/.aws/credentials`, are too weak a signal).

## AWS IAM (non-EKS AWS compute)

ECS/Fargate tasks, Lambda functions, and plain EC2 instances have IAM credentials but no OIDC token, so the OIDC providers cannot serve them. The `aws_iam` provider closes this gap. It requires the optional `aws/aws-sdk-php` package:

```sh theme={null}
composer require aws/aws-sdk-php
```

```php theme={null}
$client = NexusClient::create(new NexusConfig(
    baseUrl: $_ENV['NEXUS_URL'],
    wif: new WifConfig(provider: 'aws_iam', awsRegion: 'eu-central-1'),
));
```

The SDK SigV4-signs an STS `GetCallerIdentity` request with the standard AWS credential chain (task role, instance profile, env) - **never sending it to AWS** - and posts the signed headers + body to `/v1/auth/token-exchange` as `{"provider":"aws_iam","aws_sts_request":...}`. Nexus replays it against a pinned STS endpoint and matches the caller's IAM role against an `aws_iam` trust policy (subject = `arn:aws:iam::<account>:role/<name>`).

Nothing else to configure: the SDK signs your service's own host (the `baseUrl` host) into `X-Nexus-Server-ID` inside the signature, and Nexus verifies it against the host the request arrived on - so a captured signed request is valid for that ONE service only. Selecting `aws_iam` without `aws/aws-sdk-php` installed throws an actionable `NexusException`.

## Explicit provider

Pin a specific provider to skip auto-detection:

```php theme={null}
$client = NexusClient::create(new NexusConfig(
    baseUrl: $_ENV['NEXUS_URL'],
    wif: new WifConfig(provider: 'kubernetes'),
));
```

## Custom tokenSource closure

Provide a `Closure` to bypass auto-detection entirely. This is useful for tests, custom CI flows, or environments the SDK does not natively recognise:

```php theme={null}
use WestyxNexus\WifConfig;

$client = NexusClient::create(new NexusConfig(
    baseUrl: $_ENV['NEXUS_URL'],
    wif: new WifConfig(
        tokenSource: function (): string {
            // Any logic that returns a workload OIDC JWT string.
            return file_get_contents('/run/secrets/oidc-token');
        },
    ),
));
```

When `tokenSource` is non-null it takes precedence over `provider`.

## Token exchange flow

Internally, the SDK runs this protocol at startup and on each session refresh:

1. **OIDC token fetch** - calls `tokenSource()` (or the auto-detected built-in equivalent) to get a workload-identity JWT.
2. **Token exchange** - POSTs `{"oidc_token": "<jwt>"}` to `/v1/auth/token-exchange`. **No auth header is sent on this call.** The backend validates the OIDC issuer and signature, then returns:
   ```json theme={null}
   {
     "session_token": "<short-lived-nexus-jwt>",
     "token_type":    "Bearer",
     "expires_in":    3600,
     "wif_provider":  "kubernetes"
   }
   ```
3. **Bearer auth on subsequent calls** - every `/v1/*` request carries `Authorization: Bearer <session_token>`.
4. **Pre-emptive refresh** - when a request finds the session expires within 60 seconds, the SDK refreshes (re-running steps 1-2) before issuing the call.

## Session refresh within 60 s of expiry

The SDK stores the session expiry time after each token exchange. Before each sync, write, or A/B test call, it checks whether the JWT expires within 60 seconds. If yes, it runs a token exchange first, then proceeds with the original call.

`connectStream()`'s `auth_expiring` SSE event triggers a non-blocking early refresh \~5 minutes before expiry, reducing the chance of a mid-stream expiry.

## Error cases

| Scenario                                            | Behaviour                                                                                                                          |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `provider = 'auto'` but no environment signal found | `NexusClient::create` throws `NexusException` (WIF not configured)                                                                 |
| `/v1/auth/token-exchange` returns non-2xx           | `NexusClient::create` throws `NexusException` (token exchange failed); check for audience mismatch or OIDC issuer misconfiguration |
| Session JWT expired and refresh failed              | Subsequent sync attempts throw `NexusException`; existing cache continues to be served                                             |

## Project-level vs service-level trust policies

The Nexus backend supports trust policies at two scopes:

| Scope             | What it covers                               | Recommended for                                   |
| ----------------- | -------------------------------------------- | ------------------------------------------------- |
| **Project-level** | All services in the project share one policy | Most teams - no per-service cloud identity needed |
| **Service-level** | A specific policy bound to one service only  | High-security services (payments, PII)            |

At token exchange time the backend applies this resolution order:

1. Does a **service-level** trust policy match this workload's OIDC token claims? - if yes, authorise.
2. Does a **project-level** trust policy match? - if yes, authorise.
3. Neither matches - return 401.

This means you can configure **one project-level trust policy** and all services in the project accept that workload's tokens. Each service client still uses its own `baseUrl`, so each gets its own service's data.

<Warning>
  A project-level trust policy is less restrictive than a service-level one. Any workload satisfying the policy can connect to any service in the project by targeting its `baseUrl`. For services handling sensitive data (payments, credentials, PII) consider a dedicated service-level trust policy instead.
</Warning>
