AXIAM PHP SDK

Session
in package

FinalYes

Per-`AxiamClient` session state (CONTRACT.md §3/§4/§5/§9): owns the shared Guzzle `CookieJar` (§4), captures/exposes the non-browser CSRF token (§3), and is the single-flight home for the shared refresh `Promise` (§9, D-06).

tenant is a required constructor parameter with no nullable default (D-13) — there is no default-tenant fallback anywhere in this class or in any of its callers.

refreshIfNeeded() returns the SAME PromiseInterface to every concurrent caller until it settles. The check-and-store below (the null check immediately followed by the assignment) executes synchronously — nothing in between calls ->wait() or yields — so it is safe without a mutex even under N concurrent async callers sharing one Session instance (D-06's "fiber-safe by construction" claim; PHP Fibers are cooperative/non-preemptive, and Guzzle's own promise resolution never interleaves mid-statement).

The $http client passed in is used directly for the refresh POST. In production wiring (a later plan assembles AxiamClient), that client is expected to be constructed WITHOUT RefreshMiddleware attached, so a 401 response to the refresh call itself can never recursively re-enter the single-flight guard; this plan does not need to solve that wiring, only provide the guard itself.

Table of Contents

Constants

REFRESH_KIND_OIDC  : mixed = 'oidc'
Guard-slot kind for the §12 `oidc_refresh` OAuth2 token-endpoint path (CONTRACT.md §9 rule 5 / F-06). Used by {@see \Axiam\Sdk\Oidc\OidcClient::oidcRefresh()} so a second concurrent `oidcRefresh` caller can recognise the guard is busy with ANOTHER `oidcRefresh` (same kind) and share its single outcome, instead of re-acquiring the guard and issuing its own wire call that would replay an already-consumed (single-use, rotating) refresh token.
REFRESH_KIND_SESSION  : mixed = 'session'
Guard-slot kind for the §1 cookie-session refresh path ({@see self::refreshIfNeeded()}) — the default `refreshGuard()` kind.

Methods

__construct()  : mixed
accessToken()  : string|null
The current access token, read live from the shared cookie jar's `axiam_access` entry rather than cached separately — avoids a second, potentially-stale, copy of the token (mirrors the Java SDK's `SessionState::cachedAccessToken()` and the Go SDK's `cookieValue()` helper).
adoptBearerCredential()  : void
CONTRACT.md §12.1 "`login_client_credentials` as a credential source" (a MAY): adopt `$accessToken` as this session's bearer credential for subsequent same-origin REST calls (never `/oauth2/*` — {@see \Axiam\Sdk\Rest\AuthMiddleware} excludes that path unconditionally). A cookie-sourced access token from a real `login()`/`verifyMfa()` session always takes precedence over an adopted one — see {@see self::accessToken()}.
baseUrl()  : string
AXIAM server base URL this session is bound to.
captureCsrfTokenFromResponse()  : void
Public seam for {@see \Axiam\Sdk\Oidc\OidcClient::ssoComplete()} (CONTRACT.md §12.1 note 6): `ssoComplete` establishes the session as `Set-Cookie` rather than a response body, but the server ALSO freshly sets `X-CSRF-Token` on that same response (exactly as `login()`'s response does), so this wraps the same private capture logic {@see self::refreshIfNeeded()} already uses.
cookieJar()  : CookieJar
§4: the single cookie jar every REST-facing Guzzle client MUST share.
csrfToken()  : string|null
§3 non-browser CSRF: the most recently captured `X-CSRF-Token` response header.
refreshGuard()  : PromiseInterface, kind: string}
The generic single-flight primitive behind {@see self::refreshIfNeeded()} (CONTRACT.md §9) — and, additively, behind `oidc_refresh`'s own single-flight requirement (§12.1: "`oidc_refresh` MUST be governed by a §9-conformant single-flight guard"). Both share the SAME `$refreshPromise` slot, so a cookie-session refresh and an `oidcRefresh()` call can never race each other independently — whichever gets here first "owns" the slot until it settles.
refreshIfNeeded()  : PromiseInterface
Returns the SAME `PromiseInterface` to every caller until it resolves (SC#2, D-06). On success: captures the `X-CSRF-Token` response header, then clears the stored promise. On failure: clears the stored promise and rejects with `AuthError` — no retry loop (§9.3). Clear-on-both-paths bookkeeping and the failure-to-`AuthError` translation are factored into {@see RefreshGuard::settle()} so REST and (later) gRPC never re-implement — or drift on — that one mechanism; {@see RefreshGuard::settle()}'s `$onClear` closure below is invoked on EITHER outcome, never on both, never on neither.
resetCsrf()  : void
Clears the captured CSRF token — called by {@see \Axiam\Sdk\AxiamClient::logout()} so a logged-out session never echoes a stale `X-CSRF-Token` on a subsequent (re-authenticated) request. Purely additive: does not change {@see self::csrfToken()}'s or {@see self::refreshIfNeeded()}'s existing behavior in any other way.
tenant()  : string
Tenant slug every request in this session is scoped to.

Constants

REFRESH_KIND_OIDC

Guard-slot kind for the §12 `oidc_refresh` OAuth2 token-endpoint path (CONTRACT.md §9 rule 5 / F-06). Used by {@see \Axiam\Sdk\Oidc\OidcClient::oidcRefresh()} so a second concurrent `oidcRefresh` caller can recognise the guard is busy with ANOTHER `oidcRefresh` (same kind) and share its single outcome, instead of re-acquiring the guard and issuing its own wire call that would replay an already-consumed (single-use, rotating) refresh token.

public mixed REFRESH_KIND_OIDC = 'oidc'

REFRESH_KIND_SESSION

Guard-slot kind for the §1 cookie-session refresh path ({@see self::refreshIfNeeded()}) — the default `refreshGuard()` kind.

public mixed REFRESH_KIND_SESSION = 'session'

Methods

__construct()

public __construct(string $baseUrl, string $tenant, Client $http[, CookieJar|null $cookieJar = null ]) : mixed
Parameters
$baseUrl : string

AXIAM server base URL (HTTPS; http:// is rejected except on loopback).

$tenant : string

Tenant slug every request is scoped to.

$http : Client

Guzzle client carrying this session's middleware stack.

$cookieJar : CookieJar|null = null

Persistent cookie store (CONTRACT.md §4). Defaults to a fresh in-memory jar — REQUIRED, because AXIAM delivers the access and refresh tokens as httpOnly cookies, so a client that does not persist them fails every request after login.

accessToken()

The current access token, read live from the shared cookie jar's `axiam_access` entry rather than cached separately — avoids a second, potentially-stale, copy of the token (mirrors the Java SDK's `SessionState::cachedAccessToken()` and the Go SDK's `cookieValue()` helper).

public accessToken() : string|null
Return values
string|null

adoptBearerCredential()

CONTRACT.md §12.1 "`login_client_credentials` as a credential source" (a MAY): adopt `$accessToken` as this session's bearer credential for subsequent same-origin REST calls (never `/oauth2/*` — {@see \Axiam\Sdk\Rest\AuthMiddleware} excludes that path unconditionally). A cookie-sourced access token from a real `login()`/`verifyMfa()` session always takes precedence over an adopted one — see {@see self::accessToken()}.

public adoptBearerCredential(Sensitive $accessToken) : void
Parameters
$accessToken : Sensitive

baseUrl()

AXIAM server base URL this session is bound to.

public baseUrl() : string
Return values
string

captureCsrfTokenFromResponse()

Public seam for {@see \Axiam\Sdk\Oidc\OidcClient::ssoComplete()} (CONTRACT.md §12.1 note 6): `ssoComplete` establishes the session as `Set-Cookie` rather than a response body, but the server ALSO freshly sets `X-CSRF-Token` on that same response (exactly as `login()`'s response does), so this wraps the same private capture logic {@see self::refreshIfNeeded()} already uses.

public captureCsrfTokenFromResponse(ResponseInterface $response) : void
Parameters
$response : ResponseInterface

cookieJar()

§4: the single cookie jar every REST-facing Guzzle client MUST share.

public cookieJar() : CookieJar
Return values
CookieJar

csrfToken()

§3 non-browser CSRF: the most recently captured `X-CSRF-Token` response header.

public csrfToken() : string|null
Return values
string|null

refreshGuard()

The generic single-flight primitive behind {@see self::refreshIfNeeded()} (CONTRACT.md §9) — and, additively, behind `oidc_refresh`'s own single-flight requirement (§12.1: "`oidc_refresh` MUST be governed by a §9-conformant single-flight guard"). Both share the SAME `$refreshPromise` slot, so a cookie-session refresh and an `oidcRefresh()` call can never race each other independently — whichever gets here first "owns" the slot until it settles.

public refreshGuard(callable(): PromiseInterface $startRefresh[, callable(mixed): mixed|null $onSuccess = null ][, string $kind = self::REFRESH_KIND_SESSION ]) : PromiseInterface, kind: string}

If no refresh is currently in flight, invokes $startRefresh (which must return the wire-call PromiseInterface) as THIS refresh, publishing it into the shared slot together with $kind. If a refresh is already in flight, $startRefresh is NOT invoked at all and the existing promise is returned instead, tagged with the kind that started it.

Result sharing across same-kind callers (CONTRACT.md §9 rule 2, F-06). The caller distinguishes three cases via the returned ran/kind pair:

  • ran === true: this call itself started the refresh — its own outcome IS the shared outcome.
  • ran === false and kind equals the kind THIS caller passed in: the guard is busy with ANOTHER caller of the SAME operation (e.g. two concurrent oidcRefresh() calls). The returned promise resolves to that one leader's exact outcome — the caller MUST await and reuse it rather than re-acquiring the guard and issuing its own wire call. This matters because AXIAM refresh tokens are opaque, server-stored, and single-use with rotation: a second wire call would replay an already-consumed token and fail invalid_grant. Await it with RefreshGuard::join(), never with PromiseInterface::wait(): only the leader — the ran === true caller — may wait(), because wait() consumes the promise's wait function and drives the call rather than observing it, so a second wait() from a concurrent fiber/coroutine destroys the leader's in-flight refresh (F-06).
  • ran === false and kind differs from THIS caller's kind: the guard is busy with a DIFFERENT operation (e.g. the §1 cookie-session refresh occupying the slot while an oidcRefresh() call arrives). The existing promise's resolved value cannot satisfy this caller (a PSR-7 ResponseInterface is not an OAuth2 token array, or vice versa), so the caller should wait for it to settle and then retry acquiring the guard for its own kind.
Parameters
$startRefresh : callable(): PromiseInterface

Produces the wire-call promise for THIS refresh attempt. Invoked only when the guard is free.

$onSuccess : callable(mixed): mixed|null = null

Runs after the shared promise resolves successfully, before the settled value is handed back (e.g. CSRF capture) — same contract as RefreshGuard::settle()'s own parameter.

$kind : string = self::REFRESH_KIND_SESSION

One of self::REFRESH_KIND_SESSION / self::REFRESH_KIND_OIDC (or a future operation-specific kind), identifying which operation this call is performing/waiting for.

Return values
PromiseInterface, kind: string}

ran is true only when $startRefresh was actually invoked by THIS call; kind is always the kind that OWNS the returned promise (THIS call's $kind when ran is true, otherwise whichever kind is already in flight).

refreshIfNeeded()

Returns the SAME `PromiseInterface` to every caller until it resolves (SC#2, D-06). On success: captures the `X-CSRF-Token` response header, then clears the stored promise. On failure: clears the stored promise and rejects with `AuthError` — no retry loop (§9.3). Clear-on-both-paths bookkeeping and the failure-to-`AuthError` translation are factored into {@see RefreshGuard::settle()} so REST and (later) gRPC never re-implement — or drift on — that one mechanism; {@see RefreshGuard::settle()}'s `$onClear` closure below is invoked on EITHER outcome, never on both, never on neither.

public refreshIfNeeded() : PromiseInterface
Return values
PromiseInterface

resetCsrf()

Clears the captured CSRF token — called by {@see \Axiam\Sdk\AxiamClient::logout()} so a logged-out session never echoes a stale `X-CSRF-Token` on a subsequent (re-authenticated) request. Purely additive: does not change {@see self::csrfToken()}'s or {@see self::refreshIfNeeded()}'s existing behavior in any other way.

public resetCsrf() : void

tenant()

Tenant slug every request in this session is scoped to.

public tenant() : string
Return values
string
On this page

Search results