Session
in package
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
httpOnlycookies, 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|nulladoptBearerCredential()
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
stringcaptureCsrfTokenFromResponse()
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
CookieJarcsrfToken()
§3 non-browser CSRF: the most recently captured `X-CSRF-Token` response header.
public
csrfToken() : string|null
Return values
string|nullrefreshGuard()
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 === falseandkindequals the kind THIS caller passed in: the guard is busy with ANOTHER caller of the SAME operation (e.g. two concurrentoidcRefresh()calls). The returnedpromiseresolves 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 failinvalid_grant. Await it with RefreshGuard::join(), never withPromiseInterface::wait(): only the leader — theran === truecaller — maywait(), becausewait()consumes the promise's wait function and drives the call rather than observing it, so a secondwait()from a concurrent fiber/coroutine destroys the leader's in-flight refresh (F-06).ran === falseandkinddiffers from THIS caller's kind: the guard is busy with a DIFFERENT operation (e.g. the §1 cookie-session refresh occupying the slot while anoidcRefresh()call arrives). The existing promise's resolved value cannot satisfy this caller (a PSR-7ResponseInterfaceis 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
PromiseInterfaceresetCsrf()
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