> ## 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.

# Go SDK - API Reference

> Every public function and method exported by the Westyx Nexus Go SDK.

Every public symbol exported by `gitlab.com/westyx/nexus/sdk/go`. All read methods are non-blocking - they hit the in-memory cache. Only `NewClient` and `Sync` make network calls.

## Construction

### `NewClient(ctx context.Context, cfg Config) (*Client, error)`

Creates a client and runs a **blocking** initial sync. Returns the populated client, or an error wrapping `ErrSyncFailed`.

```go theme={null}
client, err := nexus.NewClient(ctx, nexus.Config{
    BaseURL: "...",
    APIKey:  "...",
})
if err != nil {
    log.Fatal(err)
}
```

The `ctx` controls the timeout / cancellation of the initial sync. Use `context.WithTimeout` if you want a hard deadline.

| Returned error                  | When                                                                                                 |
| ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `nil`                           | Success - cache populated                                                                            |
| `errors.Is(err, ErrSyncFailed)` | Initial sync failed; the wrapped cause carries detail (often `ErrUnauthorized` or a transport error) |

## Configs

### `(c *Client) GetConfig(key string) (any, bool)`

Returns the resolved config value as `any` (the wire-level decoded JSON) and a `found` flag.

```go theme={null}
val, ok := client.GetConfig("database.url")
if !ok {
    return errors.New("not configured")
}
url := val.(string)
```

The value is whatever the server stored - `string`, `float64`, `bool`, `[]any`, or `map[string]any`. Use a type assertion or `encoding/json.Unmarshal(json.Marshal(val))` round-trip for typed structs. Object (`map[string]any`) and array (`[]any`) values are deep-copied on return, so you may freely mutate the result without affecting the cache.

### `(c *Client) GetAllConfigs() map[string]any`

Snapshot copy of every config key-value pair currently in the cache. The returned map - including nested object/array values - is deep-copied and fully independent of the SDK's internal storage; mutating it (at any depth) has no effect on the cache.

Public-key (`wxp_`) clients receive the configs the service exposes to public keys; there is no separate visibility flag to check.

## Feature flags

### `(c *Client) GetFlag(key string, defaultValue bool) bool`

Returns the `is_active` state of a flag, or `defaultValue` when the key is absent.

```go theme={null}
isOn := client.GetFlag("checkout.v2", false)
```

The server computes `is_active` (combining `is_enabled`, `starts_at`, `ends_at`); the SDK never makes that call itself.

### `(c *Client) GetAllFlags() map[string]bool`

Snapshot copy of every flag key-is\_active pair in the cache.

## AB Testing

### `(c *Client) EvaluateAB(ctx context.Context, keys []string, userID string, attributes map[string]any) (map[string]bool, error)`

Batch-evaluates feature flags through the **AB Testing** add-on against the supplied user context. Issues a single `POST /v1/flags/evaluate-ab` and returns the inner `results` map - flag key to evaluated boolean. Unlike `GetFlag`, this method **always** performs a network call and does **not** consult the local cache.

```go theme={null}
results, err := client.EvaluateAB(ctx,
    []string{"checkout.v2", "homepage.banner"},
    "user-42",
    map[string]any{
        "country": "HU",
        "plan":    "pro",
    },
)
if err != nil {
    if errors.Is(err, nexus.ErrABAddonNotAvailable) {
        // Project does not have the AB Testing add-on enabled.
        return
    }
    return
}
if results["checkout.v2"] {
    // bucketed into the new checkout
}
```

Missing keys are returned as `false` by the server. Both `keys` and `attributes` are serialised as empty (not `null`) when passed as `nil`, so the backend never rejects the payload.

| Error                    | When                                                                                |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `ErrABAddonNotAvailable` | Backend returned HTTP 403 - the project does not have the AB Testing add-on enabled |
| `ErrUnauthorized`        | Backend returned HTTP 401 - invalid key or slug mismatch                            |
| `ErrBilling`             | Backend returned HTTP 402 - tenant has overdue invoices                             |
| `ErrNotFound`            | Backend returned HTTP 404                                                           |

## Secrets (secret key only)

Secrets are typed: each entry has a `Type` field of either `SecretTypeText` (the default - a plain string returned via `GetSecret`) or `SecretTypeFile` (file content the SDK additionally writes to a temp file on every sync, whose path is exposed via `GetSecretFilePath`).

```go theme={null}
const (
    SecretTypeText = "text"
    SecretTypeFile = "file"
)
```

Backends that don't yet emit `type` are treated as `"text"` for backwards compatibility.

### `(c *Client) GetSecret(key string) (string, error)`

Returns the plaintext value of a secret. Works for both text and file secrets - for file secrets it returns the raw file contents as a string.

```go theme={null}
val, err := client.GetSecret("DB_PASSWORD")
if err != nil {
    if errors.Is(err, nexus.ErrPublicKeyRestricted) { /* ... */ }
    if errors.Is(err, nexus.ErrServiceKindMismatch) { /* ... */ }
    if errors.Is(err, nexus.ErrSecretNotFound)     { /* ... */ }
}
```

| Error                    | When                                                                 |
| ------------------------ | -------------------------------------------------------------------- |
| `ErrPublicKeyRestricted` | The client uses a public key                                         |
| `ErrServiceKindMismatch` | The service's `kind` is `"frontend"` - secrets are not allowed there |
| `ErrSecretNotFound`      | The key is not in the cache                                          |

### `(c *Client) GetSecretFilePath(key string) (string, bool)`

Returns the temp-file path that holds the value of a **file-type** secret, together with a `found` flag. The SDK materialises every `SecretTypeFile` entry to disk on every sync at:

```
<os.TempDir()>/nexus-<sanitised-key>-<sha256-prefix>
```

The hash prefix is derived from the secret value, so the path changes whenever the value changes and stale files are removed automatically. Files are written with mode `0o600`.

```go theme={null}
path, ok := client.GetSecretFilePath("TLS_CERT")
if !ok {
    log.Fatal("nexus: TLS_CERT is missing or not a file-type secret")
}
cert, err := tls.LoadX509KeyPair(path, keyPath)
```

The boolean is `false` when:

* the key does not exist in the cache
* the secret's `Type` is `"text"` (or empty / non-`"file"`)
* the client uses a public key - secrets are not delivered
* the service `kind` is `"frontend"` - secrets are not allowed there

### `(c *Client) Close() error`

Releases resources held by the client. After `Close`, the client must not be used.

`Close` removes every temp file written for a file-type secret and marks the client as closed so subsequent calls are no-ops. Future versions may also cancel background goroutines or close the streaming HTTP client; callers should always `defer client.Close()`.

```go theme={null}
client, err := nexus.NewClient(ctx, cfg)
if err != nil {
    log.Fatal(err)
}
defer client.Close()
```

`Close` is safe to call multiple times and always returns `nil` today.

## Write API (secret key only)

The write API lets you create, update, and delete secrets programmatically. All three methods check the key type before making any network call - public keys return `ErrPublicKeyRestricted` immediately.

### `(c *Client) SetSecret(ctx context.Context, key, value string) error`

Creates or updates a secret (`POST /v1/secrets`, `type: "text"`).

```go theme={null}
err := client.SetSecret(ctx, "stripe.key", newKey)
if err != nil {
    if errors.Is(err, nexus.ErrPublicKeyRestricted) { /* ... */ }
    if errors.Is(err, nexus.ErrRateLimited) { /* ... */ }
}
```

| Error                    | When                      |
| ------------------------ | ------------------------- |
| `ErrPublicKeyRestricted` | public key (no HTTP call) |
| `ErrUnauthorized`        | 401                       |
| `ErrBilling`             | 402                       |
| `ErrRateLimited`         | 429                       |

### `(c *Client) DeleteSecret(ctx context.Context, key string) error`

Deletes all versions of a secret (`DELETE /v1/secrets/:key`). Key is URL-encoded automatically.

```go theme={null}
err := client.DeleteSecret(ctx, "old.api.key")
```

| Error                    | When                      |
| ------------------------ | ------------------------- |
| `ErrPublicKeyRestricted` | public key (no HTTP call) |
| `ErrNotFound`            | 404 - key doesn't exist   |
| `ErrRateLimited`         | 429                       |

### `(c *Client) DeleteSecretVersion(ctx context.Context, key string, version int) error`

Deletes a specific version of a secret (`DELETE /v1/secrets/:key?version=N`).

```go theme={null}
err := client.DeleteSecretVersion(ctx, "db.password", 2)
```

| Error                    | When                               |
| ------------------------ | ---------------------------------- |
| `ErrPublicKeyRestricted` | public key (no HTTP call)          |
| `ErrNotFound`            | 404 - key or version doesn't exist |
| `ErrRateLimited`         | 429                                |

## Metadata

### `(c *Client) KeyType() string`

`"sk"` or `"pk"` - derived from the configured `APIKey`. Returns `""` when authentication is exclusively via Workload Identity Federation (no static `APIKey` set).

### `(c *Client) Kind() string`

The service kind reported by the backend: `"frontend"`, `"backend"`, or `""` if not yet determined. Available after the first successful sync (or token exchange when WIF is enabled).

### `(c *Client) SyncedAt() time.Time`

Timestamp of the last successful sync. Useful for "last refreshed" UIs and health checks.

### `(c *Client) BillingOverdue() bool`

Reports `true` if the most recent sync attempt returned `402 Payment Required`. While the flag is set, the SDK suspends the background refresh loop and serves the last cached snapshot. The flag clears when a subsequent sync succeeds.

## Sync / lifecycle

### `(c *Client) Sync(ctx context.Context) error`

Forces a sync now, bypassing the TTL. Atomically replaces the cache. ETag / `If-None-Match` is used automatically; a 304 resets the TTL without downloading data. Rarely needed - the SDK syncs automatically.

### `(c *Client) RunStream(ctx context.Context)`

Opens the SSE live-update connection and **blocks** until either:

* The context is cancelled
* 3 consecutive transport errors occur (the SDK falls back to TTL polling and returns)

`NewClient` starts this automatically. You only need to call it manually to re-launch the stream after a MAX\_ERRORS fallback:

```go theme={null}
for {
    client.RunStream(ctx)
    select {
    case <-ctx.Done():
        return
    case <-time.After(5 * time.Minute):
        // retry SSE after fallback
    }
}
```

## Sentinel errors

```go theme={null}
var (
    ErrSyncFailed             // wraps the cause when NewClient's initial sync fails
    ErrUnauthorized           // HTTP 401
    ErrNotFound               // HTTP 404
    ErrPublicKeyRestricted    // GetSecret with public key
    ErrSecretNotFound         // unknown secret key
    ErrBilling                // HTTP 402 - tenant has overdue invoices
    ErrServiceKindMismatch    // GetSecret on a kind=frontend service
    ErrABAddonNotAvailable    // EvaluateAB on a project without the AB Testing add-on
    ErrWIFNotConfigured       // WIF enabled but no usable provider matched
    ErrWIFTokenExchangeFailed // POST /v1/auth/token-exchange returned non-2xx
    ErrSessionExpired         // WIF session JWT expired and refresh failed
)

// QuarantinedError is returned when the server responds with 429 + quarantine body.
// Inspect ExpiresAt to know when normal sync resumes.
type QuarantinedError struct {
    Reason    string
    ExpiresAt time.Time
}
```

Always check with `errors.Is(err, nexus.ErrX)` rather than equality or string matching.

## Logger interface

```go theme={null}
type Logger interface {
    Debugf(format string, args ...any)
    Infof(format string, args ...any)
    Errorf(format string, args ...any)
}
```

Printf-style - matches `log.Logger` and `slog.Logger` style.

## StreamObserver interface

```go theme={null}
type StreamObserver interface {
    OnConnected()
    OnDisconnected(err error)
    OnEvent(name string)
    OnReconnectAttempt(attempt int)
    OnFallback(reason string)
    OnQuarantined(reason string, expiresAt time.Time)
}
```

Opt-in via `Config.Observer` for structured SSE-lifecycle callbacks. Default is `NoopStreamObserver`. See [Stream observer](stream-observer) for full semantics and examples.

## Workload Identity Federation types

```go theme={null}
type WIFConfig struct {
    Enabled     bool
    Provider    string         // "kubernetes" | "aws" | "gcp" | "azure" | "auto"
    TokenSource TokenSourceFunc
    Audience    string         // default "westyx-nexus"
}

type TokenSourceFunc func(ctx context.Context) (string, error)
```

Set `Config.WIF` to swap the static `X-Nexus-API-Key` header for a session-JWT bearer flow. See [Workload identity](workload-identity).
