diff --git a/README.md b/README.md index 26f1c98e..1edc5736 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Use the Volcano Python SDK to access authentication, databases, storage, functions, locks, logs, and realtime events. Requires Python 3.11 or later. Start with the [Python quickstart](https://github.com/Kong/volcano-sdk-python/blob/main/docs/README.md). +See the [functions guide](https://github.com/Kong/volcano-sdk-python/blob/main/docs/functions.md) for invocation identity and response handling. + +See [Authentication](https://github.com/Kong/volcano-sdk-python/blob/main/docs/authentication.md) for account, session, email, and OAuth workflows. ## Install @@ -32,13 +35,15 @@ client = VolcanoClient( sign_up = client.auth.sign_up( email="new-user@example.com", - password="secret", + password="correct-horse-battery-staple", metadata={"display_name": "New User"}, ) if sign_up.confirmation_required: print(sign_up.message) -session = client.auth.sign_in(email="user@example.com", password="secret") +session = client.auth.sign_in( + email="user@example.com", password="correct-horse-battery-staple" +) current_session = client.auth.get_session() assert current_session == session @@ -46,7 +51,7 @@ user = client.auth.get_user() assert user.id == session.user_id updated_user = client.auth.update_user( - password="new-secret", + password="new-correct-horse-battery-staple", metadata={"display_name": "Grace", "avatar": None}, ) assert updated_user.id == session.user_id @@ -196,7 +201,7 @@ The request goes to the function's own domain rather than to `api_url`, so an egress rule that allows only the API host will block it; the resolved endpoint is cached for the lifetime the platform gives it. Deployments with no public function domain invoke through the API host instead. -It uses the active user session when present, then a configured service key, +It uses the current session token, including a supplied `access_token`, then a configured service key, then the anonymous key. An anonymous key can invoke a public function without a user session; the function receives no user identity. The immutable result includes the response body, status, headers, and `X-Volcano-Version`. The body @@ -205,7 +210,6 @@ JSON arrays become immutable tuples. Invalid JSON is returned as text. A function's own non-2xx response is returned when Volcano confirms it ran; non-success platform HTTP responses raise typed SDK errors. - Function resolution and invocation recover from a platform HTTP 401 before dispatch: the SDK refreshes the captured session and retries the rejected request once. Concurrent calls share successful recovery. Replacing or signing out that session @@ -304,7 +308,9 @@ The signup acknowledgement is identical for new and existing email addresses. Pa ```python result = client.auth.sign_up( - email="new-user@example.com", password="secret", sign_in_when_allowed=True + email="new-user@example.com", + password="correct-horse-battery-staple", + sign_in_when_allowed=True, ) session = result.session # None when no follow-up sign-in ran. ``` @@ -320,21 +326,23 @@ request. It is cached data, not proof of authentication; use `auth.get_user()` to fetch the server-validated profile. Existing three-field `Session` construction still works, with `user=None`. An adopted snapshot must have the same user ID. Successful `get_user()`, `update_user()`, `convert_anonymous()`, and -`confirm_email_change()` calls update that local snapshot without changing tokens -or emitting an auth-state event. Previously returned sessions remain immutable. +`confirm_email_change()` calls update that local snapshot. Automatic HTTP 401 recovery +can rotate credentials and emit `TOKEN_REFRESHED`; the profile update itself does not. Previously returned sessions remain immutable. Profile identity checks compare UUID values; the session retains its original user ID spelling, including in the cached snapshot. `get_user()` sends the active access token to Volcano and returns an immutable, server-validated profile with the complete public AuthUser fields. Profile timestamps are timezone-aware `datetime` values, and nested user and application metadata are immutable. The request updates the cached -profile without changing credentials. If another authentication operation replaces the session +profile without changing credentials unless HTTP 401 recovery requires a refresh. +Successful recovery rotates credentials and emits `TOKEN_REFRESHED`. If another authentication operation replaces the session while the request is in flight, `get_user()` raises `SessionChangedError` instead of returning a profile for stale credentials. `update_user()` updates the current user's password, metadata, or both. Metadata is a shallow patch: omitted keys remain unchanged, and setting a key to `None` removes it. The method returns the same -immutable profile type as `get_user()` and updates the cached profile without changing credentials. +immutable profile type as `get_user()` and updates the cached profile without changing credentials unless HTTP 401 recovery requires a refresh. +Successful recovery rotates credentials and emits `TOKEN_REFRESHED`. It also rejects a response if another authentication operation replaces the session while the update is in flight. @@ -422,7 +430,9 @@ hosted_url = client.auth.get_hosted_auth_url( ``` Store `hosted_state` in the user's signed server-side session before redirecting to `hosted_url`. -After parsing the returned fragment into a `Session`, validate and adopt it atomically: +In the callback, atomically fetch and delete the stored state before validation, +even if validation or adoption fails. Reject a missing or already-consumed state. +After parsing the returned fragment into a `Session`, validate and adopt it: ```python session = client.auth.adopt_hosted_auth_session( @@ -451,8 +461,9 @@ authorization_url = client.auth.sign_in_with_oauth( ``` Store `oauth_state` in the user's signed server-side session, then redirect the user to the returned -URL. In the callback, pass the returned and stored states to the SDK so it rejects login CSRF before -exchanging the one-time code: +URL. In the callback, atomically fetch and delete the stored nonce as `stored_oauth_state`; +reject a missing or already-consumed nonce. Pass the returned and consumed states to +the SDK so it rejects login CSRF before exchanging the one-time code: ```python session = client.auth.exchange_oauth_code( @@ -546,9 +557,11 @@ Revoke one session by ID: client.auth.delete_session(session_id="00000000-0000-4000-8000-000000000099") ``` -The request uses the current access token. Deleting that token's own session clears local -credentials, including when the request outcome is uncertain; deleting another session preserves -them. If another authentication operation replaces the session before deletion finishes, the method +The request uses the current access token. When its JWT contains a readable UUID `session_id`, +deleting that session clears local credentials even if the request outcome is uncertain. +Without that identifier, the SDK cannot recognize self-deletion. Deleting another session does not +itself clear local state. HTTP 401 recovery can rotate credentials and emit `TOKEN_REFRESHED`; +a server-rejected refresh clears the captured session before the operation raises. If another authentication operation replaces the session before deletion finishes, the method raises `SessionChangedError` instead of clearing the replacement or acknowledging a stale result. Create an anonymous account and make its tokens the current session: @@ -560,12 +573,12 @@ session = client.auth.sign_in_anonymously(metadata={"device": "mobile"}) Anonymous sign-ins must be enabled for the project. Convert the account before signing out if the user needs to recover it later. -Attach email credentials without changing the anonymous user's ID or current session: +Attach email credentials while preserving the anonymous user's ID: ```python user = client.auth.convert_anonymous( email="user@example.com", - password="secure-password", + password="a-long-example-password-2026", metadata={"display_name": "Ada"}, ) ``` @@ -577,7 +590,7 @@ Set a new password with the recovery token from that email: ```python client.auth.reset_password( token="recovery-token", - new_password="new-secret", + new_password="new-correct-horse-battery-staple", ) ``` @@ -588,10 +601,13 @@ when the reset flow completes. To start with only a supplied user access token, pass `access_token` to `VolcanoClient`. Construction makes no request and leaves `refresh_token`, `user_id`, and `user` as `None` until supplied or validated by the server. -`get_user()` validates and caches the profile without changing credentials. +`get_user()` validates and caches the profile without changing credentials unless HTTP 401 recovery requires a refresh. +Successful recovery rotates credentials and emits `TOKEN_REFRESHED`. Without a refresh token, `refresh_session()` raises `AuthenticationError` and -`sign_out()` revokes the server session using the access token and clears local state. -Supply `refresh_token` with `access_token` to enable refresh. See the [token bootstrap example](https://github.com/Kong/volcano-sdk-python/blob/main/docs/README.md#use-a-supplied-access-token). +`sign_out()` clears local state and revokes the server session when the access JWT +contains a readable UUID `session_id`. +Supplied credentials require both a refresh token and an access JWT with a readable UUID +`session_id` to enable refresh. See the [token bootstrap example](https://github.com/Kong/volcano-sdk-python/blob/main/docs/README.md#use-a-supplied-access-token). Copy a complete native session into another client's memory: @@ -616,7 +632,8 @@ assert client.auth.get_session() is refreshed ``` On success, `refresh_session()` replaces the in-memory session and returns the immutable new -snapshot. An authentication failure clears the session that initiated the request. Server and +snapshot. An authentication rejection from the refresh endpoint clears the captured session. +Missing refresh credentials, failed session-continuity checks, server errors, and transport failures preserve it, and a late response never replaces a newer session. The SDK does not persist sessions. @@ -638,8 +655,8 @@ subscription.unsubscribe() Registration queues `INITIAL_SESSION`. It normally arrives before registration returns, but an existing notification dispatch may deliver it afterward. Successful session creation, refresh, and local clearing emit `SIGNED_IN`, `TOKEN_REFRESHED`, and `SIGNED_OUT`. Callbacks are delivered locally -in transition order after the state lock is released, and callback failures cannot interrupt auth -operations. Unsubscribing prevents queued and future delivery; a callback already selected for +in transition order after the state lock is released. Ordinary callback `Exception` failures are +isolated; exceptions such as `KeyboardInterrupt` propagate after the session transition has committed. Unsubscribing prevents queued and future delivery; a callback already selected for delivery may finish after `unsubscribe()` returns. The SDK does not broadcast between processes or persist sessions. @@ -651,8 +668,10 @@ assert client.auth.get_session() is None ``` Sign-out uses the refresh token directly when the SDK received both credentials together from -sign-in or a validated refresh. Supplied credentials use the access-token session; on HTTP 401, -the SDK can refresh once and revoke that same session without adopting the renewed credentials. +sign-in or a validated refresh. Supplied credentials use the access-token session when its JWT +contains a readable UUID `session_id`; on HTTP 401, the SDK can refresh once and revoke that +same session without adopting the renewed credentials. Without that identifier, sign-out uses +the supplied refresh token, or only clears local state if no refresh token is available. Calling `sign_out()` without a session succeeds without a request. A revocation failure is raised after the captured local session is cleared. Sign-out waits for an already-running refresh and uses its validated credentials. Later refresh attempts raise `SessionChangedError` without a request. Concurrent sign-out calls diff --git a/docs/README.md b/docs/README.md index 05ee39b9..9cebba12 100644 --- a/docs/README.md +++ b/docs/README.md @@ -127,3 +127,7 @@ See [Logs](./logs.md) for project-token authentication, search, pagination, and See [Realtime](./realtime.md) for broadcasts, presence, database changes, and shutdown. + +See [Authentication](./authentication.md) for account, session, email, and OAuth workflows. + +See [Functions](./functions.md) for invocation identity, response values, and error handling. diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 00000000..e94e3011 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,453 @@ +--- +title: Authentication +description: Manage Volcano user accounts, sessions, email flows, and OAuth from Python. +order: 2 +--- + +Sign in with a project's anonymous key and an existing user's credentials. +Enable the required [authentication methods](/platform/authentication/configuring-auth-methods) for the project first. +Install the SDK using the [quickstart](./README.md). + +```python +import os + +from volcano_sdk import VolcanoClient + +client = VolcanoClient( + anon_key=os.environ["VOLCANO_ANON_KEY"], + api_url=os.environ.get("VOLCANO_API_URL", "https://api.volcano.dev"), +) +session = client.auth.sign_in( + email=os.environ["VOLCANO_USER_EMAIL"], + password=os.environ["VOLCANO_USER_PASSWORD"], +) +user = client.auth.get_user() +assert user.id == session.user_id +print(user.email) +``` + +Use a separate client for each independent user session. +The examples below describe separate account workflows using this client. +Keep access and refresh tokens out of logs; the SDK stores sessions in memory only. + +## Create an account + +`sign_up()` returns an immutable acknowledgement without changing the session by default. +The signup acknowledgement is identical for new and existing email addresses. Pass +`sign_in_when_allowed=True` to follow it with `sign_in()` only when confirmation is not required: + +```python +result = client.auth.sign_up( + email="new-user@example.com", + password="correct-horse-battery-staple", + sign_in_when_allowed=True, +) +session = result.session # None when no follow-up sign-in ran. +``` + +A successful follow-up stores the session and emits the normal sign-in event. A failed +follow-up raises its usual typed error; it does not undo the successful signup. + +## Read the local session and user + +`get_session()` reads immutable local state. It does not refresh or validate the token. + +Sessions returned by authentication retain the user payload in `session.user`, +including metadata. The snapshot is deeply immutable and available without a +request. It is cached data, not proof of authentication; use `auth.get_user()` +to fetch the server-validated profile. Existing three-field `Session` construction +still works, with `user=None`. An adopted snapshot must have the same user ID. +Successful `get_user()`, `update_user()`, `convert_anonymous()`, and +`confirm_email_change()` calls update that local snapshot. Automatic HTTP 401 recovery +can rotate credentials and emit `TOKEN_REFRESHED`; the profile update itself does not. Previously returned sessions remain immutable. +Profile identity checks compare UUID values; the session retains its original +user ID spelling, including in the cached snapshot. + +## Validate and update a profile + +`get_user()` sends the active access token to Volcano and returns an immutable, server-validated +profile with the complete public AuthUser fields. Profile timestamps are timezone-aware `datetime` +values, and nested user and application metadata are immutable. The request updates the cached +profile without changing credentials unless HTTP 401 recovery requires a refresh. +Successful recovery rotates credentials and emits `TOKEN_REFRESHED`. If another authentication operation replaces the session +while the request is in flight, `get_user()` raises `SessionChangedError` instead of returning a +profile for stale credentials. + +```python +user = client.auth.update_user(metadata={"display_name": "Ada", "avatar": None}) +print(user.id) +``` + +`update_user()` updates the current user's password, metadata, or both. Metadata is a shallow patch: +omitted keys remain unchanged, and setting a key to `None` removes it. The method returns the same +immutable profile type as `get_user()` and updates the cached profile without changing credentials unless HTTP 401 recovery requires a refresh. +Successful recovery rotates credentials and emits `TOKEN_REFRESHED`. +It also rejects a response if another authentication operation replaces the session while the +update is in flight. + +## Recover a password + +Request a password reset email without creating or changing a session: + +```python +client.auth.reset_password_for_email(email="user@example.com") +``` + +When transactional email is configured, Volcano sends the reset link. Success returns `None`, and +the response is intentionally identical whether or not the email belongs to an account. Failures +raise the same typed Volcano errors as other authentication operations. + +## Confirm an email address + +Confirm an email address with the token from its confirmation link: + +```python +client.auth.confirm_email(token="confirmation-token") +``` + +Success returns `None`. Confirmation does not sign in the confirmed account or change an unrelated +local session. + +## Resend confirmation + +Request another confirmation email without revealing account state: + +```python +client.auth.resend_confirmation(email="user@example.com") +``` + +Success returns `None` whether the account is unknown, already confirmed, or eligible. Volcano sends +mail only for an existing unconfirmed account when transactional email is configured. Rate limits +raise `RateLimitedError` with `retry_after` when the server supplies it. + +## Change an email address + +Request confirmation for a new email address while keeping the current session: + +```python +result = client.auth.request_email_change(new_email="new@example.com") +print(result.new_email) +``` + +The immutable result contains the server acknowledgement. Its `message` and `new_email` fields may +be `None`. The request fails if there is no active session or that session changes in flight. + +Cancel the pending change while keeping the current session: + +```python +client.auth.cancel_email_change() +``` + +Success returns `None`. A successful stale response is rejected if another authentication operation +replaces the session while cancellation is in flight. + +Confirm the pending change with the token delivered to the new address: + +```python +user = client.auth.confirm_email_change(token="email-change-token") +print(user.email) +``` + +The method returns the immutable updated user without replacing the active session. A successful +stale response is rejected if another authentication operation replaces that session in flight. + +## List server sessions + +List sessions using the stable activity-ordered offset pagination: + +```python +page = client.auth.list_sessions(page=1, limit=20) +for session in page.sessions: + print(session.id, session.user_agent, session.is_current) +``` + +The method returns immutable `SessionPage` and `AuthSession` values. It raises +`SessionChangedError` instead of returning a page for a session that was replaced while the request +was in flight. Sort, filter, and cursor controls are not yet exposed by this facade. + +## Use hosted authentication + +Build a managed hosted-auth URL: + +```python +import secrets + +hosted_state = secrets.token_urlsafe(32) +hosted_url = client.auth.get_hosted_auth_url( + project_id="00000000-0000-4000-8000-000000000020", + action="signup", + state=hosted_state, +) +``` + +Store `hosted_state` in the user's signed server-side session before redirecting to `hosted_url`. +In the callback, atomically fetch and delete the stored state before validation, +even if validation or adoption fails. Reject a missing or already-consumed state. +After parsing the returned fragment into a `Session`, validate and adopt it: + +```python +session = client.auth.adopt_hosted_auth_session( + returned_session, + state=returned_state, + expected_state=hosted_state, +) +``` + +The SDK rejects a mismatched state before changing local authentication. It builds and adopts the +flow but does not parse browser URLs, navigate, or persist state. The `action` deep link applies to +Volcano's built-in page; a customized login page must implement its own signup or forgot-password +flow. + +## Sign in with OAuth + +Build the URL that starts an OAuth sign-in flow: + +```python +import secrets + +oauth_state = secrets.token_urlsafe(32) +authorization_url = client.auth.sign_in_with_oauth( + provider="github", + redirect_to="https://app.example.com/auth/callback", + state=oauth_state, +) +``` + +Store `oauth_state` in the user's signed server-side session, then redirect the user to the returned +URL. In the callback, atomically fetch and delete the stored nonce as `stored_oauth_state`; +reject a missing or already-consumed nonce. Pass the returned and consumed states to +the SDK so it rejects login CSRF before exchanging the one-time code: + +```python +session = client.auth.exchange_oauth_code( + code=callback_code, + redirect_to="https://app.example.com/auth/callback", + state=callback_state, + expected_state=stored_oauth_state, +) +``` + +The callback URL must exactly match a registered project redirect. The exchange stores the returned +Volcano session on the client. The SDK does not open a browser or persist OAuth state between +requests; use your framework's signed session or equivalent storage for that state. + +List the OAuth providers linked to the current account: + +```python +providers = client.auth.list_linked_oauth_providers() +for provider in providers: + print(provider.provider, provider.linked_at) +``` + +The method returns an immutable tuple of `LinkedOAuthProvider` values and raises +`SessionChangedError` if the active session changes while the request is in flight. + +## Link and unlink providers + +Start linking another OAuth provider to the current account: + +```python +authorization_url = client.auth.link_oauth_provider(provider="github") +``` + +Redirect the user to the returned URL to complete the provider flow. The method accepts `apple`, +`github`, `google`, or `microsoft` and raises `SessionChangedError` if the active session changes +while the request is in flight. + +Unlink an OAuth provider from the current account: + +```python +client.auth.unlink_oauth_provider(provider="github") +``` + +The server rejects removal of the account's only authentication method. A successful stale response +raises `SessionChangedError` instead of acknowledging work authorized by a replaced session. + +## Use provider APIs + +Check whether Volcano has a valid server-held provider token: + +```python +status = client.auth.get_oauth_provider_token(provider="github") +print(status.provider, status.expires_in) +``` + +The immutable `OAuthProviderTokenStatus` contains provider and expiry metadata, not the credential. +Volcano refreshes an expired token on the server. A stale result raises `SessionChangedError`. + +Refresh a provider token explicitly: + +```python +status = client.auth.refresh_oauth_provider_token(provider="github") +print(status.provider, status.expires_in) +``` + +The refresh credential and new access token remain on the server. A stale result raises +`SessionChangedError`. + +Call a provider API through Volcano's fixed-host server proxy: + +```python +repos = client.auth.call_oauth_api( + provider="github", + endpoint="/user/repos", +) +print(repos[0]["name"]) +``` + +The method returns an immutable copy of the provider's JSON value. Volcano owns token refresh and +host validation. A stale result raises `SessionChangedError`. + +## Revoke other sessions + +Sign out every other device while keeping the current session active: + +```python +client.auth.delete_all_other_sessions() +``` + +Success returns `None`. Do not replace the client's session while this request is in flight: the +server may revoke that replacement as an "other" session. If replacement occurs, the method raises +`SessionChangedError` instead of acknowledging a stale result. + +## Revoke one session + +Revoke one session by ID: + +```python +client.auth.delete_session(session_id="00000000-0000-4000-8000-000000000099") +``` + +The request uses the current access token. When its JWT contains a readable UUID `session_id`, +deleting that session clears local credentials even if the request outcome is uncertain. +Without that identifier, the SDK cannot recognize self-deletion. Deleting another session does not +itself clear local state. HTTP 401 recovery can rotate credentials and emit `TOKEN_REFRESHED`; +a server-rejected refresh clears the captured session before the operation raises. If another authentication operation replaces the session before deletion finishes, the method +raises `SessionChangedError` instead of clearing the replacement or acknowledging a stale result. + +## Use anonymous accounts + +Create an anonymous account and make its tokens the current session: + +```python +session = client.auth.sign_in_anonymously(metadata={"device": "mobile"}) +``` + +Anonymous sign-ins must be enabled for the project. Convert the account before signing out if the +user needs to recover it later. + +Attach email credentials while preserving the anonymous user's ID: + +```python +user = client.auth.convert_anonymous( + email="user@example.com", + password="a-long-example-password-2026", + metadata={"display_name": "Ada"}, +) +``` + +When email confirmation is required, confirm the new address before treating it as verified. + +## Complete a password reset + +Set a new password with the recovery token from that email: + +```python +client.auth.reset_password( + token="recovery-token", + new_password="new-correct-horse-battery-staple", +) +``` + +Success returns `None`. The reset revokes the recovered account's existing sessions and does not +sign it in. The client keeps any unrelated local session unchanged; sign in with the new password +when the reset flow completes. + +## Use supplied credentials + +To start with only a supplied user access token, pass `access_token` to +`VolcanoClient`. Construction makes no request and leaves `refresh_token`, +`user_id`, and `user` as `None` until supplied or validated by the server. +`get_user()` validates and caches the profile without changing credentials unless HTTP 401 recovery requires a refresh. +Successful recovery rotates credentials and emits `TOKEN_REFRESHED`. +Without a refresh token, `refresh_session()` raises `AuthenticationError` and +`sign_out()` clears local state and revokes the server session when the access JWT +contains a readable UUID `session_id`. +Supplied credentials require both a refresh token and an access JWT with a readable UUID +`session_id` to enable refresh. See the [token bootstrap example](./README.md#use-a-supplied-access-token). + +## Adopt an existing session + +Copy a complete native session into another client's memory: + +```python +session = source.auth.get_session() +if session is not None: + fresh.auth.set_session(session) +``` + +`set_session()` copies the session without making a request, persisting credentials, or notifying +auth-state subscribers. It raises `ValueError` when the session type or any credential field is +incomplete. + +Password sign-in raises `SessionChangedError` if local session state changes while the request is +in flight. The late response does not replace the newer state or emit a sign-in notification. + +## Refresh the current session + +Refresh the session with its current refresh token: + +```python +refreshed = client.auth.refresh_session() +assert client.auth.get_session() is refreshed +``` + +On success, `refresh_session()` replaces the in-memory session and returns the immutable new +snapshot. An authentication rejection from the refresh endpoint clears the captured session. +Missing refresh credentials, failed session-continuity checks, server errors, and +transport failures preserve it, and a late response never replaces a newer session. The SDK does +not persist sessions. + +## Observe authentication changes + +Observe local session transitions: + +```python +from volcano_sdk import AuthChangeEvent, Session + + +def handle_auth_change(event: AuthChangeEvent, session: Session | None) -> None: + print(event, session is not None) + + +subscription = client.auth.on_auth_state_change(handle_auth_change) +# Later, stop receiving events. +subscription.unsubscribe() +``` + +Registration queues `INITIAL_SESSION`. It normally arrives before registration returns, but an +existing notification dispatch may deliver it afterward. Successful session creation, refresh, and +local clearing emit `SIGNED_IN`, `TOKEN_REFRESHED`, and `SIGNED_OUT`. Callbacks are delivered locally +in transition order after the state lock is released. Ordinary callback `Exception` failures are +isolated; exceptions such as `KeyboardInterrupt` propagate after the session transition has committed. Unsubscribing prevents queued and future delivery; a callback already selected for +delivery may finish after `unsubscribe()` returns. The SDK does not broadcast between processes or +persist sessions. + +## Sign out the current session + +Sign out by revoking and clearing the current session: + +```python +client.auth.sign_out() +assert client.auth.get_session() is None +``` + +Sign-out uses the refresh token directly when the SDK received both credentials together from +sign-in or a validated refresh. Supplied credentials use the access-token session when its JWT +contains a readable UUID `session_id`; on HTTP 401, the SDK can refresh once and revoke that +same session without adopting the renewed credentials. Without that identifier, sign-out uses +the supplied refresh token, or only clears local state if no refresh token is available. +Calling `sign_out()` without a session succeeds without a request. A revocation failure is raised +after the captured local session is cleared. Sign-out waits for an already-running refresh and uses its validated credentials. +Later refresh attempts raise `SessionChangedError` without a request. Concurrent sign-out calls +share one result. A separate sign-in or adoption remains current. diff --git a/docs/functions.md b/docs/functions.md new file mode 100644 index 00000000..f73dc60c --- /dev/null +++ b/docs/functions.md @@ -0,0 +1,65 @@ +--- +title: Functions +description: Invoke deployed Volcano functions from Python and inspect their response status, headers, and data. +order: 5 +--- + +Invoke a deployed function by its name. +This example assumes a public function named `hello` that accepts a JSON object. +Grant the anonymous key the explicit `functions.invoke` permission; the default +authentication-only permissions do not allow invocation. See [anonymous keys](/platform/authentication/security/anon-keys). + +```python +import os + +from volcano_sdk import VolcanoClient + +client = VolcanoClient( + anon_key=os.environ["VOLCANO_ANON_KEY"], + api_url=os.environ.get("VOLCANO_API_URL", "https://api.volcano.dev"), +) +result = client.functions.invoke("hello", {"name": "Ada"}) +print(result.status) +print(result.data) +print(result.version) +``` + +The SDK resolves the function's invocation endpoint and caches it for the lifetime provided by Volcano. +Allow outbound requests to the resolved function domain as well as the API host. +Deployments without a separate function domain use the API invocation endpoint. + +## Choose the invocation identity + +The SDK uses the current session's access token, including a supplied `access_token`, before a configured service key or anonymous key. +A rejected or unsupported session token does not fall back to a key. Use an end-user access token when the function requires a user identity. +Use the [quickstart](./README.md) to sign in before invoking a function that requires a user. +An invocation authorized only by the anonymous key has no user identity. + +Supply `service_key` to the client constructor for trusted server operations that require it. +Use a separate client for each independent user session. + +## Recover a rejected session + +Function resolution and invocation recover from a platform HTTP 401 before dispatch: +the SDK refreshes the captured session and retries the rejected request once. +Concurrent calls share successful recovery. Replacing or signing out that session +prevents replay under another identity. The call preserves its original payload values. +A function's own response, HTTP 403, or a network failure never triggers this retry. +Anonymous and service keys do not refresh. + +## Read the result + +`FunctionResponse` is immutable and exposes `status`, `headers`, `version`, and `data`. +The payload sent to `invoke()` must be a JSON object; omitting it sends an empty object. +Response data can be a JSON object, array, scalar, text, or `None` for an empty body. +JSON arrays become immutable tuples, and invalid JSON is returned as text. + +A function's own non-success response is returned as a result when Volcano confirms the function ran. +Check `result.status` to handle those application errors. +Non-success platform HTTP responses before dispatch raise typed SDK exceptions such as `NotFoundError` or `AuthenticationError`. + +If a cached function identity no longer exists, the SDK resolves the name again and retries once only when the platform confirms no function was dispatched. +A function's own HTTP 404 does not trigger another invocation. +Network failures do not establish whether a function ran; do not blindly retry operations with side effects. + +Invalid invocation arguments and malformed successful resolution responses can raise `ValueError` or `TypeError`.