diff --git a/.github/scripts/quickstart.rb b/.github/scripts/quickstart.rb index 290ec013..43b32dd3 100644 --- a/.github/scripts/quickstart.rb +++ b/.github/scripts/quickstart.rb @@ -4,8 +4,31 @@ require 'timeout' require_relative '../../spec/support/recording_server' -# Executes the unchanged public example with the isolated gem selected by smoke-gem.sh. +# Executes the public quickstart with the isolated gem selected by smoke-gem.sh. class PackageQuickstart + # The public quickstart, as published in Kong/volcano-docs + # content/sdk/ruby/index.md ("Sign in and read a profile"). Change both together. + EXAMPLE = <<~'RUBY' + require "volcano" + + client = Volcano::Client.new( + anon_key: ENV.fetch("VOLCANO_ANON_KEY"), + api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev") + ) + session = client.auth.sign_in( + email: ENV.fetch("VOLCANO_USER_EMAIL"), + password: ENV.fetch("VOLCANO_USER_PASSWORD") + ) + begin + user = client.auth.user + raise "Unexpected user" unless user.id == session.user_id + + puts "Signed in as #{user.email}" + ensure + client.auth.sign_out + end + RUBY + USER = { id: '22222222-2222-4222-8222-222222222222', project_id: '11111111-1111-4111-8111-111111111111', @@ -45,18 +68,9 @@ def configure(url) ) end - def example - document = File.read(File.expand_path('../../docs/README.md', __dir__)) - section = document.split("## Sign in and read a profile\n").fetch(1).split("\n## ").first - examples = section.scan(/```ruby\n(.*?)\n```/m).flatten - raise 'Expected one complete documented quickstart' unless examples.length == 1 - - examples.first - end - def execute previous_stdout = $stdout - File.write('quickstart.rb', example) + File.write('quickstart.rb', EXAMPLE) output = StringIO.new $stdout = output Timeout.timeout(30) { load File.expand_path('quickstart.rb') } diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml deleted file mode 100644 index 460a7a77..00000000 --- a/.github/workflows/lint-docs.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Lint docs - -on: - pull_request: - paths: - - 'docs/**' - - '.github/workflows/lint-docs.yml' - -permissions: - contents: read - -jobs: - docs: - uses: Kong/volcano-docs/.github/workflows/lint-docs.yml@2eed9e93cae0b070f57788e7c270ccfbb4cd4579 - with: - path: docs diff --git a/AGENTS.md b/AGENTS.md index 14dc6724..0d455d44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,4 +9,6 @@ automation. - Keep native unit, type, build, and package checks here. Hosting owns behavioral acceptance scenarios and bindings; coordinate changes with `Kong/volcano-hosting`. -- Keep maintainer guidance under `maintainers/`; `docs/` is published. +- Keep maintainer guidance under `maintainers/`. +- Public SDK docs live in `Kong/volcano-docs` under `content/sdk/ruby`. When + user-facing behavior changes, open a companion PR there and link the two PRs. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bba444c..b854fdee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,8 +82,9 @@ actual live results separately from discovery or package checks. ### Documentation and release boundaries -`docs/` is published to the developer documentation site. Put user-facing -examples there and maintainer instructions in this file or beside the code. +Public guides live in `Kong/volcano-docs` under `content/sdk/ruby` and publish +to https://docs.volcano.dev/sdk/ruby. Put user-facing examples there in a +companion PR, and maintainer instructions in this file or beside the code. Keep equivalent language examples current in the same coordinated change. Package checks validate artifacts; they do not authorize publication. Treat registry-installed quickstarts and release approval as separate release work. diff --git a/Gemfile.lock b/Gemfile.lock index ddb40308..d5ad0cdd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,7 +97,7 @@ GEM protocol-websocket (0.21.2) protocol-http (~> 0.2) racc (1.8.1) - rack (3.2.7) + rack (3.2.8) rainbow (3.1.1) rake (13.4.2) rb-fsevent (0.11.2) diff --git a/README.md b/README.md index b62c3e8e..7f78644b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Official Ruby SDK for Volcano. Requires Ruby 3.3 or later. -Start with the [Ruby quickstart](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/README.md). +Start with the [Ruby quickstart](https://docs.volcano.dev/sdk/ruby). -See [Authentication](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/authentication.md) for account, session, email, and OAuth workflows. +See [Authentication](https://docs.volcano.dev/sdk/ruby/authentication) for account, session, email, and OAuth workflows. ## Install @@ -393,7 +393,7 @@ without changing credentials. Without a refresh token, `refresh_session` raises `Volcano::Error::AuthenticationError`. `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-ruby/blob/main/docs/README.md#use-a-supplied-access-token). +See the [token bootstrap example](https://docs.volcano.dev/sdk/ruby#use-a-supplied-access-token). ### Adopt an existing session @@ -493,7 +493,7 @@ prevents replay under another identity. The call preserves its original payload A function's own response, HTTP 403, or a network failure never triggers this retry. Anonymous and service keys do not refresh. -See the [functions guide](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/functions.md). +See the [functions guide](https://docs.volcano.dev/sdk/ruby/functions). ### Start and follow a durable execution @@ -680,7 +680,7 @@ they started; the anon key, or another user, passes the execution's `client_token:`, which only the creating start returns. Pass `project_id:` to `send_message`, `list`, or `subscribe` to use the owner's route with a platform user token or a project access token; `subscribe` reads back over it. See the -[functions guide](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/functions.md#message-a-durable-execution). +[functions guide](https://docs.volcano.dev/sdk/ruby/functions#message-a-durable-execution). ### Read project logs @@ -708,7 +708,7 @@ events. Pass `next_cursor` back as `cursor` to continue a search. `activity` returns immutable time buckets using the same resource selector and query syntax. Both methods require a platform user token or a project access token. A `read_only` project token is sufficient; end-user sessions cannot read project logs. -See the [logs guide](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/logs.md). +See the [logs guide](https://docs.volcano.dev/sdk/ruby/logs). ### Query a database @@ -890,7 +890,7 @@ Acquisition accepts caller-owned UUID `token` and `request_id` values and retrie an ambiguous transport failure or HTTP 503 once with the same request and credential. Retain those IDs to recover an uncertain acquisition. Other lock methods accept `request_id`; block-scoped helpers forward initial IDs only to acquisition. -See the [lock guide](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/locks.md) +See the [lock guide](https://docs.volcano.dev/sdk/ruby/locks) for examples and fencing requirements. `locks.get` returns immutable lock availability, expiry, and fencing-token diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 6af196bd..00000000 --- a/docs/README.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: Ruby SDK -description: Install the Volcano Ruby SDK and authenticate a user from a Ruby application. -order: 1 ---- - -Use `volcano-sdk` for authentication, database queries, storage, functions, logs, locks, and realtime events. -REST calls are synchronous and raise typed exceptions on failure. -Ruby 3.3 or later is required; CI tests Ruby 3.3 and 3.4. - -## Install the published gem - -Add the [RubyGems package](https://rubygems.org/gems/volcano-sdk) to your application: - -```bash -bundle add volcano-sdk -``` - -For a new directory without a Gemfile, run `bundle init` first. -Require the package with `require "volcano"`. - -## Sign in and read a profile - -Create a project, enable [email and password authentication](/platform/authentication/configuring-auth-methods), and create a user with a confirmed email when your project requires confirmation. -Use that project's [anonymous key](/platform/authentication/security/anon-keys). -Set `VOLCANO_ANON_KEY`, `VOLCANO_USER_EMAIL`, and `VOLCANO_USER_PASSWORD` in your environment. -Set `VOLCANO_API_URL` only when using a different API endpoint, such as local mode. - -Save this as `quickstart.rb`: - -```ruby -require "volcano" - -client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev") -) -session = client.auth.sign_in( - email: ENV.fetch("VOLCANO_USER_EMAIL"), - password: ENV.fetch("VOLCANO_USER_PASSWORD") -) -begin - user = client.auth.user - raise "Unexpected user" unless user.id == session.user_id - - puts "Signed in as #{user.email}" -ensure - client.auth.sign_out -end -``` - -Run it with `bundle exec ruby quickstart.rb`. -It signs in, fetches the server-validated profile, prints the user's email, revokes its server session, and clears the local session. -An invalid email or password raises `Volcano::Error::AuthenticationError`; failed network requests raise `Volcano::Error::TransportError`. -Both inherit from `Volcano::Error::VolcanoError`. - -## Keep authentication scoped to one user - -A client holds its current session in memory. -Use a separate client for each independent user session; do not share one mutable client across users in a web server. -`client.current_session` and `client.auth.current_session` read the local immutable snapshot without a request. -Use `client.auth.user` when you need a server-validated profile; `get_user` is an alias. -Successful profile operations update the cached user while preserving the current credentials. -Authenticated profile, session-list/deletion, email-change request/cancellation, linked-provider, provider-token, and provider-API operations refresh a usable session once after HTTP 401 and replay the original request values. -Deleting the current server session also clears its refreshed local credentials; a separately adopted session remains current. -These operations do not retry other HTTP failures or ambiguous network failures, and they never retry under a replacement session. - -`sign_up` returns an acknowledgement without signing in by default. -Pass `sign_in_when_allowed: true` to sign in only when the project does not require email confirmation. -To adopt an existing session, assign a `Volcano::Session` containing its access token, refresh token, and user ID to `client.auth.current_session`. -The SDK does not persist tokens for you. - -For operations that require a [service key](/platform/authentication/security/service-keys), pass `service_key` to the constructor in trusted server code. -Keep service keys and user credentials out of source control and client applications. - -## Use a supplied access token - -For a server request that already carries a user's access token, create a client for that request: - -```ruby -require "volcano" - -def load_request_user(access_token) - client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - access_token: access_token - ) - client.auth.user -end -``` - -Call this helper from your request handler with the bearer token from that request. -For a Volcano function, use the access token in `event.fetch("__volcano_auth")` supplied for that invocation. -The helper validates the token with Volcano before returning the user. - -Once a user identity has been validated, a refresh response for another user is rejected and leaves the current credentials unchanged. -Construction makes no request and does not persist credentials. -The initial snapshot has `nil` refresh credentials, user ID, and cached user. -A successful profile read fills in the validated identity and cached user while retaining the supplied access token. -Without a refresh token, an HTTP 401 remains an authentication error, `refresh_session` raises `Volcano::Error::AuthenticationError`, and `sign_out` revokes the server session identified by the access token before clearing local state. -Refresh must preserve the server session identified by the access JWT, even before a profile is loaded. A different session is rejected, including another session for the same user. Supplied credentials need a readable session identifier to refresh, even when you provide a user profile or load it from the server. Profile data does not prove that access and refresh tokens belong together. -For supplied credentials, sign-out revokes the access-token session. On HTTP 401, it can refresh once and revoke that same session without adopting the renewed credentials locally. -When the SDK received both credentials together from sign-in or a validated refresh, it uses the refresh token directly, even if access has expired. -Sign-out joins an existing refresh and prevents later refresh attempts for that session. Concurrent sign-outs share one result; a separate sign-in or adoption remains current. -Pass `refresh_token` alongside `access_token` when the client should refresh that session. -A revocation failure is reported after local clearing; it does not prove that copied tokens are invalid. -Assigning `auth.current_session` still requires complete credentials and identity. - -## Use the rest of the API - -The SDK repository contains [examples for every public facade](https://github.com/Kong/volcano-sdk-ruby#create-a-client), including database filters and mutations, resumable uploads, function invocation, log queries, lock guards, and realtime presence and database changes. -Follow the realtime examples to connect and disconnect channels within their supported async task lifecycle. - -See [release notes](https://github.com/Kong/volcano-sdk-ruby/releases) for version changes and [GitHub issues](https://github.com/Kong/volcano-sdk-ruby/issues) to report a problem. -Include the gem version, Ruby version, and a minimal reproduction without credentials. - -See [Distributed locks](./locks.md) for acquisition recovery, renewal, and fencing. - -Read the [database guide](./database.md) for projection, filters, ordered pagination, and mutations. - -For uploads, visibility, and resumable sessions, see [Storage](./storage.md). - -See [Logs](./logs.md) for project-token authentication, search, pagination, and activity. - -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 standard invocation, durable execution clients, and error handling. - -See [Versions and compatibility](./versions.md) for runtime support, upgrades, and restoring a tested dependency set. - -- [Sandboxes](sandboxes.md): isolated commands, sessions, files, and HTTP access. diff --git a/docs/authentication.md b/docs/authentication.md deleted file mode 100644 index b20acb74..00000000 --- a/docs/authentication.md +++ /dev/null @@ -1,454 +0,0 @@ ---- -title: Authentication -description: Manage Volcano user accounts, sessions, email flows, and OAuth from Ruby. -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). - -```ruby -require "volcano" - -client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev") -) -session = client.auth.sign_in( - email: ENV.fetch("VOLCANO_USER_EMAIL"), - password: ENV.fetch("VOLCANO_USER_PASSWORD") -) -user = client.auth.user -raise "wrong user" unless user.id == session.user_id -puts 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. - -## Sign up - -```ruby -result = client.auth.sign_up( - email: "new-user@example.com", - password: "correct-horse-battery-staple", - metadata: { display_name: "New User" } -) -puts result.message if result.confirmation_required -``` - -`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: - -```ruby -result = client.auth.sign_up(email: "new-user@example.com", password: "correct-horse-battery-staple", sign_in_when_allowed: true) -session = result.session # nil 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. - -## Sign in - -```ruby -session = client.auth.sign_in(email: "user@example.com", password: "correct-horse-battery-staple") -current_session = client.auth.current_session -raise "session changed" unless current_session == session -``` - -`current_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.user` to fetch -the server-validated profile. Existing three-field `Session` construction still -works, with `user: nil`. An adopted snapshot must have the same user ID. -Snapshots contain deeply frozen JSON data. Plain `Time` timestamps become UTC -ISO8601 strings with nanosecond precision; symbols become strings. Custom objects, -container/string/time subclasses, non-finite numbers, duplicate JSON keys, and -structures deeper than 100 levels raise `TypeError`. Use the typed `auth.user` -profile when you need Ruby `Time` values. Successful `user`, `update_user`, -`convert_anonymous`, and `confirm_email_change` calls update this snapshot -without changing credentials or emitting an authentication-state event unless an HTTP 401 -requires automatic refresh. Successful recovery rotates credentials and emits `:token_refreshed`. -Previously returned sessions remain unchanged. - -## Get the current user - -```ruby -user = client.auth.user -raise "wrong user" unless user.id == session.user_id -``` - -`user` sends the active access token to Volcano and returns an immutable, -server-validated `Volcano::User`. The SDK recursively freezes its strings and -metadata and updates `current_session.user`. A session change while the request -is in flight raises -`Volcano::Error::SessionChangedError` instead of returning a stale profile. -`get_user` is available as a cross-SDK alias. - -## Update the current user - -```ruby -user = client.auth.update_user( - password: "new-correct-horse-battery-staple", - metadata: { display_name: "Grace", avatar: nil } -) -raise "wrong user" unless user.id == session.user_id -``` - -`update_user` changes the current user's password, metadata, or both. Metadata -is a shallow patch: omitted keys remain unchanged, and a `nil` value removes -that key. The method returns an immutable `Volcano::User` and updates the local -user snapshot. It rejects a response if another authentication operation -replaces the session while the request is in flight. - -## Request a password reset email - -```ruby -client.auth.reset_password_for_email(email: "user@example.com") -``` - -When transactional email is configured, Volcano sends the reset link. Success -returns `nil`, and the acknowledgement is intentionally identical whether or -not the email belongs to an account. Failures raise the same typed Volcano -errors as other authentication operations, and the current session remains -unchanged. - -## Confirm an email address - -```ruby -client.auth.confirm_email(token: "confirmation-token") -``` - -Success returns `nil`. Confirmation does not sign in the confirmed account or -change an unrelated local session. - -## Resend a confirmation email - -```ruby -client.auth.resend_confirmation(email: "user@example.com") -``` - -Success returns `nil` 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 -`Volcano::Error::RateLimitedError` with `retry_after` when supplied. - -## Request an email change - -```ruby -result = client.auth.request_email_change(new_email: "new@example.com") -puts result.new_email -``` - -The immutable result contains the server acknowledgement. Its `message` and -`new_email` fields may be `nil`. The request requires an active session and -rejects a response if that session changes in flight. - -## Cancel an email change - -```ruby -client.auth.cancel_email_change -``` - -Success returns `nil` and preserves the active session. A successful stale -response is rejected if another authentication operation replaces the session -while cancellation is in flight. - -## Confirm an email change - -```ruby -user = client.auth.confirm_email_change(token: "email-change-token") -puts user.email -``` - -The method returns the immutable updated user and updates the local user -snapshot. A successful stale response is rejected if another authentication -operation replaces that session in flight. - -## List sessions - -```ruby -page = client.auth.list_sessions(page: 1, limit: 20) -page.sessions.each do |session| - puts [session.id, session.user_agent, session.is_current].join(' ') -end -``` - -The method returns immutable `Volcano::SessionPage` and `Volcano::AuthSession` -values in activity order. It raises `Volcano::Error::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. - -## Build a hosted-auth URL - -```ruby -require 'securerandom' - -hosted_state = SecureRandom.urlsafe_base64(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 `Volcano::Session`, validate and adopt it: - -```ruby -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 -does not parse browser URLs, navigate, or persist state. - -The `action` deep link applies to Volcano's built-in page. A customized login -page receives the request but must implement its own signup or forgot-password -flow because Hosting ignores `action` for custom pages. - -## Sign in with OAuth - -```ruby -require 'securerandom' - -oauth_state = SecureRandom.urlsafe_base64(32) -authorization_url = client.auth.sign_in_with_oauth( - '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: - -```ruby -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 linked OAuth providers - -```ruby -client.auth.list_linked_oauth_providers.each do |provider| - puts [provider.provider, provider.linked_at].join(' ') -end -``` - -The method returns a frozen array of immutable `Volcano::LinkedOAuthProvider` -values. It raises `Volcano::Error::SessionChangedError` if the active session -changes while the request is in flight. - -## Link an OAuth provider - -```ruby -authorization_url = client.auth.link_oauth_provider('github') -``` - -Redirect the user to the returned URL to complete the provider flow. The method -accepts `apple`, `github`, `google`, or `microsoft` and raises -`Volcano::Error::SessionChangedError` if the active session changes while the -request is in flight. - -## Unlink an OAuth provider - -```ruby -client.auth.unlink_oauth_provider('github') -``` - -The server rejects removal of the account's only authentication method. A -successful stale response raises `Volcano::Error::SessionChangedError` instead -of acknowledging work authorized by a replaced session. - -## Check provider token status - -```ruby -status = client.auth.get_oauth_provider_token('github') -puts [status.provider, status.expires_in].join(' ') -``` - -The immutable `Volcano::OAuthProviderTokenStatus` contains provider and expiry -metadata, not the credential. Volcano refreshes an expired token on the server. -A stale result raises `Volcano::Error::SessionChangedError`. - -Refresh a provider token explicitly: - -```ruby -status = client.auth.refresh_oauth_provider_token('github') -puts [status.provider, status.expires_in].join(' ') -``` - -The refresh credential and new access token remain on the server. A stale result -raises `Volcano::Error::SessionChangedError`. - -Call a provider API through Volcano's fixed-host server proxy: - -```ruby -repos = client.auth.call_oauth_api('github', endpoint: '/user/repos') -puts repos.first.fetch('name') -``` - -The method returns an immutable copy of the provider's JSON value. Volcano owns -token refresh and host validation. A stale result raises -`Volcano::Error::SessionChangedError`. - -## Sign out all other devices - -```ruby -client.auth.delete_all_other_sessions -``` - -Success returns `nil` and keeps the authorizing session active. 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 -`Volcano::Error::SessionChangedError` instead of acknowledging a stale result. - -## Revoke one session - -```ruby -client.auth.delete_session('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 -`Volcano::Error::SessionChangedError` instead of clearing the replacement or -acknowledging a stale result. - -## Sign in anonymously - -```ruby -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: - -```ruby -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. - -## Reset the password - -```ruby -client.auth.reset_password(token: "recovery-token", new_password: "new-correct-horse-battery-staple") -``` - -Success returns `nil`. 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. - -## Start with a supplied access token - -Pass `access_token` to `Volcano::Client.new` to start without a refresh token or -known user identity. Construction makes no request and leaves `refresh_token`, -`user_id`, and `user` as `nil`. `auth.user` validates and caches the profile -without changing credentials. Without a refresh token, `refresh_session` raises -`Volcano::Error::AuthenticationError`. `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 - -```ruby -session = source.auth.current_session -fresh.auth.current_session = session if session -``` - -The writer copies and freezes a complete native session in memory only. It does -not make a request, persist credentials, or notify auth-state subscribers. It -raises `ArgumentError` for an incomplete value. - -Password sign-in raises `Volcano::Error::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 - -```ruby -refreshed = client.auth.refresh_session -raise "refresh failed" unless client.auth.current_session.equal?(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. - -Concurrent reads share refresh work. If that refresh is rejected, each read -keeps its original authentication error, including reads that finish while the -SDK clears the rejected session. - -## Observe auth-state changes - -```ruby -subscription = client.auth.on_auth_state_change do |event, session| - puts "#{event}: #{session ? 'authenticated' : 'anonymous'}" -end - -# Later, stop receiving events. -subscription.unsubscribe -``` - -Registration immediately yields `:initial_session`. Successful session -creation, refresh, and local clearing yield `:signed_in`, `:token_refreshed`, -and `:signed_out`. Callbacks are delivered locally in transition order after the -state lock is released. Callback `StandardError` failures are isolated; exceptions such as -`Interrupt` propagate after the session transition has committed. -The SDK does not broadcast between processes or persist sessions. - -## Sign out the current session - -```ruby -client.auth.sign_out -raise "still signed in" if client.auth.current_session -``` - -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. -It revokes the captured session and clears the captured in-memory -session. It succeeds without a request when no session exists. If revocation -fails, the SDK still clears that session and raises the typed error. Sign-out waits for an already-running refresh and uses its validated credentials. -Later refresh attempts raise `Volcano::Error::SessionChangedError` without a request. -Concurrent sign-out calls share one result. A separate sign-in or adoption remains current. diff --git a/docs/database.md b/docs/database.md deleted file mode 100644 index dc78bec0..00000000 --- a/docs/database.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: Database queries -description: Read and change Postgres rows through the Ruby SDK with filters, ordering, and pagination. -order: 3 ---- - -Query an existing database and table using the signed-in user's permissions. -This example expects a `main` database with an `items` table containing `id`, `name`, and `status` columns, plus Row-Level Security policies that permit the user to read those rows. -Set the credentials described in the [quickstart](./README.md). - -```ruby -require "volcano" - -client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev") -) -client.auth.sign_in( - email: ENV.fetch("VOLCANO_USER_EMAIL"), - password: ENV.fetch("VOLCANO_USER_PASSWORD") -) -items = client.database("main").from("items") -rows = items.select("id", "name").eq("status", "published").execute -rows.each { |row| puts [row["id"], row["name"]] } -``` - -`execute` returns an array of row hashes, including an empty array when no rows match. -Builders are immutable; each chained operation returns a new builder, so a base query can be reused. -The following examples reuse an authenticated `client` and the same table. - -## Filter, sort, and page through rows - -```ruby -base = client.database("main").from("items").select("id", "name") -rows = base.ilike("name", "%volcano%") - .in("status", %w[draft published]) - .order("name") - .order("id") - .limit(20) - .offset(40) - .execute -``` - -| Methods | Filter | -| --- | --- | -| `eq`, `neq` | Equal or not equal to a value | -| `gt`, `gte`, `lt`, `lte` | Compare values | -| `like`, `ilike` | Case-sensitive or case-insensitive SQL pattern; `%` matches any sequence | -| `is("deleted_at", nil)` | Match a SQL null | -| `is("enabled", true)` | Match a SQL boolean | -| `in("status", %w[draft published])` | Match a member of a list | - -Use `select("*")` to return every column. -Pass `ascending: false` to `order` for descending order. -Include a unique final sort column such as `id` when paging with `limit` and `offset`. - -## Insert, update, and delete - -```ruby -items = client.database("main").from("items") -created = items.insert(name: "Volcano", status: "draft").execute -item_id = created.first.fetch("id") -updated = items.update(status: "published").eq("id", item_id).execute -deleted = items.delete.eq("id", item_id).execute -``` - -Mutations return an array of the affected rows. -Updates and deletes with no matches return an empty array. -They require at least one filter; Volcano rejects filterless updates and deletes. - -## Handle authentication failures - -Database reads and mutations refresh a rejected access token once when the captured session has a usable refresh token, then retry the same request. -HTTP 403 responses and network failures do not trigger this retry. -Mutations are never retried after an ambiguous transport failure. -Replacing or signing out the session during recovery raises `Volcano::Error::SessionChangedError` instead of replaying under another user. - -For direct Postgres connections inside a Volcano function, `Volcano.database_connection_string` can derive a connection string from `DATABASE_URL` and a server-validated `user_id:` while preserving its database target. -Passing a user ID applies that user's Row-Level Security context; omitting it requests service access. -Keep the resulting connection string private. diff --git a/docs/functions.md b/docs/functions.md deleted file mode 100644 index c07a5c2e..00000000 --- a/docs/functions.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: Functions -description: Invoke deployed Volcano functions from Ruby 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). - -```ruby -require "volcano" - -client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev") -) -result = client.functions.invoke("hello", name: "Ada") -puts result.status -puts result.data -puts result.version -``` - -The SDK resolves the function's invocation endpoint and caches it for the lifetime provided by Volcano. Repeated resolution of a missing name raises a new `Volcano::Error::NotFoundError` with the original message, status, and error code while the cached miss remains valid. -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 - -`Volcano::FunctionResponse` is immutable and exposes `status`, `headers`, `version`, and `data`. -The payload sent to `invoke` must be a hash representing a JSON object; omitting it sends an empty object. -Response data can be a JSON object, array, scalar, text, or `nil` for an empty body. -JSON objects and arrays are immutable, 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 `Volcano::Error::NotFoundError` or `Volcano::Error::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 `ArgumentError` or `TypeError`. - -## Start and follow a durable execution - -A durable execution can run for up to 366 days. Starting one returns a handle instead of waiting for its result: - -```ruby -handle = client.durable.start( - "charge-order", - { order_id: "order-9" }, - execution_name: "order-9" -) -token = handle.client_token - -execution = client.durable.get_from_application("charge-order", handle.id, client_token: token) -client.durable.stop_from_application("charge-order", handle.id, client_token: token) -``` - -`start`, `get_from_application`, and `stop_from_application` use the same credential as `functions.invoke`: the active session, then a service key, then the anonymous key. An execution name makes a start idempotent. The start that creates an execution returns its `client_token`; a repeated start returns nil there. Inspecting the execution masks the token. - -A service key holding `*`, the default, reads and stops any execution of the project. A signed-in user reaches the executions they started without the client token. An anonymous key, a user who did not start the execution, or a service key scoped to a list without `*` passes `client_token:`. An anonymous key or a service key scoped without `*` also needs `functions.invoke`; without it the call raises `Volcano::Error::PermissionDeniedError`. The function's visibility must also admit the credential: an anonymous key on an `authenticated` function raises `Volcano::Error::PermissionDeniedError`, even with the token, and a `private` function raises `Volcano::Error::NotFoundError` for every credential but a service key. An execution the credential cannot reach raises `Volcano::Error::NotFoundError`, as a missing one does, and so does one still `pending` unless the credential is a service key holding `*`. Reads never return `client_token`, so keep the one the start returned. - -`stop_from_application` and `stop` return after the stop request is accepted, often while the execution still reports `running`; poll the matching read until `terminal?` is true. Stopping an execution that already finished returns the state it settled in. A `stop_from_application` answered with a `503` `Volcano::Error::ServerError` may already have been sent, so read the execution before stopping it again. - -Tooling that manages a project uses the owner-scoped `get`, `list`, and `stop`, which reach any execution without a client token: - -```ruby -owner_client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev"), - access_token: ENV.fetch("VOLCANO_PLATFORM_TOKEN") -) -execution = owner_client.durable.get(project_id, "charge-order", handle.id) -page = owner_client.durable.list(project_id, "charge-order", status: "running") -owner_client.durable.stop(project_id, "charge-order", handle.id) -``` - -Call them from a trusted backend with the project owner's platform user token or a project access token; a `read_only` token can read and list but not stop. Auth-user sessions, anonymous keys, and service keys are not accepted. - -`list` returns executions and pagination metadata. A successful response must include `data`, `page`, `limit`, `total`, and `has_more` with the API's declared types. Missing or malformed fields raise `TypeError`; a complete response with `data: []` returns an empty execution list. - -Ruby can start and manage durable functions written in JavaScript or Python. The Ruby SDK has no durable authoring API. - -## Decide durable approvals - -A durable function written in JavaScript or Python can wait for a person to approve or deny a request. Ruby reads and decides those approvals: - -```ruby -approvals = owner_client.durable.approvals -page = approvals.list(project_id, status: "pending", function: "refunds") -approvals.approve(project_id, page.approvals.first.id, comment: "Matches the invoice") -approvals.deny(project_id, approval_id) -stats = approvals.stats(project_id, from: Time.now - (7 * 86_400)) -``` - -`list` and `get` return `DurableApproval` records; `stats` returns counts by status, the approval rate, and decision times for a window that defaults to the last 30 days. `daily` entries carry `YYYY-MM-DD` UTC dates. - -An approval's `decision` is nil unless it was approved or denied. `decision.decided_by` is nil once the deciding account is deleted, so read it with `decided_by&.email`. - -Reads accept the project owner's platform user token or a project access token. Decisions require a person, so a project access token raises `Volcano::Error::PermissionDeniedError`, a subclass of `Volcano::Error::AuthenticationError`. A conflicting decision, or deciding an expired or cancelled approval, raises `Volcano::Error::ConflictError` with `code` set to `approval_decided`, `approval_expired`, or `approval_cancelled`; repeating the same decision returns the approval unchanged. An unknown approval raises `Volcano::Error::NotFoundError`. Malformed responses raise `TypeError`. - -## Message a durable execution - -A durable function written in JavaScript or Python can wait for a named message and publish messages of its own. Ruby sends, lists, and follows them. Here a signed-in user starts the execution, so it reaches it without the client token: - -```ruby -require "async" - -client.auth.sign_in( - email: ENV.fetch("VOLCANO_USER_EMAIL"), - password: ENV.fetch("VOLCANO_USER_PASSWORD") -) -handle = client.durable.start("onboarding", { account_id: "account-7" }) -messages = client.durable.messages - -messages.send_message("onboarding", handle.id, "documents-uploaded", { count: 3 }, idempotency_key: "upload-42") -page = messages.list("onboarding", handle.id, direction: "out", after: 0, limit: 100) - -Async do - subscription = messages.subscribe("onboarding", handle.id) do |message| - puts [message.seq, message.name, message.data] - end - # ... - subscription.unsubscribe -ensure - client.realtime.disconnect -end.wait -``` - -`send_message` queues the message until a wait for its name takes it, and returns the stored `DurableMessage`: its `status` is `delivered` when a wait took it at once, `delivering` while the handover to that wait is still being confirmed, and `queued` otherwise. A name is 1 to 237 printable ASCII characters; `data` is any JSON value up to 64 KiB. Sending again with an `idempotency_key` the same sender has used on the execution returns the stored message in its current status and sends nothing; another sender's keys never collide with it. Sending to an execution that has ended raises `Volcano::Error::ConflictError` with `code` `execution_not_running`, and sending to one already holding 100 messages no wait has taken raises it with `too_many_queued_messages`. - -`list` returns both directions, oldest first, filtered by `direction` (`in` or `out`), `name`, `after` (a `seq`), and `limit` (1 to 500, default 100). `seq` orders an execution's messages but can skip numbers, so read the next page with `after` set to the last message's `seq` while `has_more` is true. - -`subscribe` delivers what the execution publishes, once each, in `seq` order, and one block call at a time: first the messages it published before the call, then live ones. After a reconnect, or a live message that shows an earlier one was lost, it reads back the missing ones before live delivery resumes. A read-back that fails is reported to `realtime.on_error` and retried, backing off to every 30 seconds, while the live messages after the gap wait. `realtime.remove_all_channels` and `realtime.disconnect` end the subscription, and a read-back still in flight delivers nothing; during the first read-back, `subscribe` then raises `Volcano::Realtime::ClosedError`. The subscription belongs to the session that opened it: once the client signs out, signs in again, or sets another session, it reads and delivers nothing more, the next read-back, delivery, or reconnect leaves the execution's channel, and raises `Volcano::Error::SessionChangedError`, reported to `realtime.on_error`, or from `subscribe` during its first read-back. Subscribing on a realtime connection an earlier session opened raises it too, and closes that connection so the next call reconnects as the current session. A token refresh keeps it. A reconnect the server refuses to restore the subscription on, as when the execution is no longer visible to the session, ends it too: the refusal is reported to `realtime.on_error` once, as a `Volcano::Realtime::ServerError` with the server's `code`, and the execution can be subscribed to again. A refusal the server marks `temporary?` is retried instead. It runs on the client's realtime connection, so call it inside an Async reactor with a signed-in session. A block that raises during the first read-back ends the subscription before it is called again, and raises from `subscribe`; one that raises later, on a live message or during a read-back, is reported to `realtime.on_error`, and delivery goes on with the next message. A second subscription to the same execution raises `Volcano::Realtime::DuplicateSubscriptionError` until the first unsubscribes. - -These calls use the application route with the same credential `start` uses. The service key reaches every execution. A signed-in user reaches executions they started, and the anon key, or another user, reaches one by passing its `client_token:`. The function must be visible to the credential: public for the anon key, public or authenticated for a user. The anon key on an authenticated function raises `Volcano::Error::PermissionDeniedError`; any other execution the credential cannot reach raises `Volcano::Error::NotFoundError`. Only the start that creates an execution returns the token, so a lost start response loses it. - -Pass `project_id:` to `send_message`, `list`, or `subscribe` to use the owner's route with the project owner's platform user token or a project access token instead; `subscribe` reads back over it, and a read-only token can list but not send. diff --git a/docs/locks.md b/docs/locks.md deleted file mode 100644 index 588f303d..00000000 --- a/docs/locks.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Distributed locks -description: Coordinate Ruby workers with project-scoped lock leases, renewal, and fencing tokens. -order: 7 ---- - -Acquire a project-scoped lease from trusted server code using a service key. -Set `VOLCANO_ANON_KEY` and `VOLCANO_SERVICE_KEY` for the same project. - -```ruby -require "volcano" - -client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - service_key: ENV.fetch("VOLCANO_SERVICE_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev") -) -lease = client.locks.acquire("daily-report", ttl: 30) -begin - puts "Lease acquired #{lease.fencing_token}" -ensure - client.locks.release("daily-report", lease) -end -``` - -`acquire` returns an immutable `Volcano::LockLease` containing the key, ownership token, expiration, and fencing token. -Release and renew using that lease; the ownership token identifies the holder. -A competing owner can cause `Volcano::Error::ConflictError`. - -## Renew or inspect a lease - -```ruby -state = client.locks.get("daily-report") -puts [state.held, state.expires_at, state.fencing_token] - -lease = client.locks.acquire("daily-report", ttl: 30) -begin - lease = client.locks.renew("daily-report", lease, ttl: 60) -ensure - client.locks.release("daily-report", lease) -end -``` - -`get` reports availability without acquiring the lock. -`renew` returns a new immutable lease, so retain its return value. - -## Renew for the duration of a block - -```ruby -client.locks.with_lock("daily-report", ttl: 30) do |guard| - puts "Current fence #{guard.lease.fencing_token}" - raise "Lock ownership was lost" if guard.lost? -end -``` - -The block helper renews in the background and attempts to release its latest lease on exit. -`guard.lease` reads the latest lease; `guard.lost?` checks ownership loss, and `guard.wait_lost(timeout: 1.0)` waits up to one second for it. -Stop protected work when ownership is lost. -Use fencing tokens in the protected data store so an expired worker cannot overwrite work performed by a newer holder. -Lease renewal alone cannot stop application code that is already running. - -## Recover an abandoned lock - -`client.locks.force_release("daily-report")` removes the current lease without its ownership token. -Use this only for administrative recovery with fencing enforced by the protected resource. - -## Recover an uncertain acquisition - -Acquisition retries a transport failure or HTTP 503 once with the same ownership -token, request ID, key, TTL, and credential. Other HTTP errors are not retried. -For recovery after that retry also fails, generate and retain identifiers before -acquiring, then reuse them for the same acquisition attempt: - -```ruby -require "securerandom" - -owner_token = SecureRandom.uuid -request_id = SecureRandom.uuid -lease = client.locks.acquire( - "daily-report", ttl: 30, token: owner_token, request_id: request_id -) -``` - -Caller-supplied UUID strings retain their exact spelling across retries and lease operations. -Every lock method accepts `request_id:`. `with_lock` also accepts `token:` and -`request_id:` for acquisition; its renewal and release calls use new request IDs. - -Use a new ownership token for a new lease after release or expiry. Keep ownership -tokens private. A failed response does not prove the server failed to acquire; -reuse the original token to recover the outcome rather than starting a new owner. diff --git a/docs/logs.md b/docs/logs.md deleted file mode 100644 index 2b0c7ea1..00000000 --- a/docs/logs.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Logs -description: Search retained project logs, paginate events, and read activity counts. -order: 6 ---- - -Search logs and inspect activity for resources in your project. - -Project logs require a platform user token or a [project access token](/platform/api-reference/using-the-api). For server-side log readers, create a `read_only` project token and set `VOLCANO_PROJECT_ACCESS_TOKEN`, `VOLCANO_PROJECT_ID`, and `VOLCANO_ANON_KEY`. Keep this credential on your server. The constructor requires an anon key, but the supplied project access token authorizes these requests. Project end-user sign-in, anon keys, and service keys do not grant project log access. - -```ruby -require "volcano" - -project_id = ENV.fetch("VOLCANO_PROJECT_ID") -client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev"), - access_token: ENV.fetch("VOLCANO_PROJECT_ACCESS_TOKEN") -) -request = { resource: { type: "function" }, q: "checkout_failed", limit: 100 } -page = client.logs.search(project_id, request) -page.data.each { |event| puts [event["timestamp"], event["body"]] } -``` - -`search` returns an immutable `Volcano::LogSearchResponse` with `data`, `limit`, `has_more`, and `next_cursor`. - -## Continue a search - -```ruby -if page.has_more && page.next_cursor - next_page = client.logs.search(project_id, request.merge(cursor: page.next_cursor)) -end -``` - -## Read activity buckets - -```ruby -activity = client.logs.activity( - project_id, - { resource: request[:resource], q: request[:q], bucket_count: 24 } -) -puts activity.total -activity.data.each { |bucket| puts [bucket["start_time"], bucket["counts"], bucket["total"]] } -``` - -Keep the same resource selector, query, and time bounds on every pagination request. Results are newest first. `body` preserves JSON values, including objects and arrays; it is not always a string. Events include a stable `id`, `timestamp`, and owning `resource`. - -Activity returns time buckets and a total count. Each bucket contains `start_time`, `end_time`, `total`, and `counts` grouped by `levels`, `regions`, and `resource_ids`. - -Both methods accept `resource.ids` to restrict results to specific resources, `q` for text or field queries, and RFC3339 `start_time` and `end_time` bounds. Logs arrive asynchronously; use bounded polling when waiting for a new event. These methods read retained logs rather than open a live stream. - -A project token has no refresh token. The SDK sends it as supplied and surfaces authentication failures; rotate or replace expired credentials through project token management. Do not use auth sign-out to revoke a project token. diff --git a/docs/realtime.md b/docs/realtime.md deleted file mode 100644 index 27a8c4ad..00000000 --- a/docs/realtime.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: Realtime -description: Send broadcasts, observe presence, and receive database changes with Ruby async channels. -order: 8 ---- - -Subscribe to a broadcast channel and send a JSON publication inside an Async reactor. -Sign in with the [quickstart credentials](./README.md) and enable the project's realtime capabilities and access policies. - -```ruby -require "async" -require "volcano" - -client = Volcano::Client.new( - anon_key: ENV.fetch("VOLCANO_ANON_KEY"), - api_url: ENV.fetch("VOLCANO_API_URL", "https://api.volcano.dev") -) -client.auth.sign_in( - email: ENV.fetch("VOLCANO_USER_EMAIL"), - password: ENV.fetch("VOLCANO_USER_PASSWORD") -) -Async do |task| - channel = client.realtime.channel("updates") - channel.on("message") { |message| puts message } - begin - channel.subscribe - channel.send(event: "message", value: "hello") - task.sleep(2) - ensure - client.realtime.disconnect - end -end.wait -``` - -`subscribe` waits for the server acknowledgement. -Channel names receive their type prefix, so `updates` becomes `broadcast:updates`. -Run realtime operations inside the same Async reactor for that client. -The SDK uses HTTP/1.1 WebSocket upgrades; network proxies must allow those upgrades. -The following channel examples belong inside an `Async` block with an authenticated `client`. - -## Pause or remove channels - -```ruby -channel = client.realtime.channel("updates") -channel.on("message") { |message| puts message } -channel.subscribe -channel.unsubscribe -channel.subscribe -client.realtime.remove_channel("updates") -``` - -Unsubscribe pauses delivery while retaining handlers and the in-memory broadcast recovery position. -Subscribe resumes and requests missed broadcasts when retained history is available. -Removal forgets the channel and its recovery position. -`remove_all_channels` removes all channels while leaving the shared connection available; `disconnect` closes it. -Application code owns any work its callbacks start and should await that work during shutdown. - -## Observe presence - -```ruby -lobby = client.realtime.channel("lobby", type: :presence) -lobby.on_presence_sync do |state| - state.each { |connection_id, info| puts "#{connection_id}: #{info.user}" } -end -lobby.on("join") { |info| puts "Joined #{info.user}" } -lobby.on("leave") { |info| puts "Left #{info.user}" } -lobby.subscribe -lobby.track("status" => "online") -puts lobby.tracked_state -puts lobby.presence_state -client.realtime.remove_channel("lobby", type: :presence) -``` - -Presence identity and metadata come from the authenticated user. -`track` stores optional local application state; it does not replace server-managed presence metadata. -The roster maps connection IDs to immutable client identity and user metadata snapshots. -Each entry's `client` matches its connection ID; `user` identifies the authenticated user. -One user can have several connections. The original sync callback observes connections joining and leaving. -`get_presence_state` is an alias for `presence_state`. -Presence rebuilds its current roster after reconnection. - -## Subscribe to database changes - -Use an existing `app` database and `public.messages` table with realtime and suitable Row-Level Security policies configured: - -```ruby -client.realtime.database_name = "app" -changes = client.realtime.channel("public:messages", type: :postgres) -changes.on_postgres_changes("INSERT", schema: "public", table: "messages") do |change| - puts change.record -end -changes.subscribe -Async::Task.current.sleep(30) -client.realtime.remove_channel("public:messages", type: :postgres) -``` - -Insert a row from another client during the listening period. -Automatic row lookup requires a primary key named `id`. It reads the current row after the notification; rapid updates may have already changed that row. -Matching insert and update notifications can fetch full rows using the subscription's user token. -Compatible row lookups are batched while publication order is preserved. -Defaults are a 20 millisecond window and 50 rows; set `fetch_batch_window_ms:` and `fetch_max_batch_size:` on the channel to change them. -Both values must be positive integers, and the maximum batch size is 128. -Set `auto_fetch: false` when first creating the channel to retain lightweight notifications without row lookups. -A failed lookup reports an error and delivers the original lightweight change. -Deletes use `old_record` or the row ID and do not query the database. - -## Handle connection changes - -```ruby -stop_errors = client.realtime.on_error { |context| warn context.message } -stop_connected = client.realtime.on_connect { |context| puts context.client } -stop_disconnected = client.realtime.on_disconnect { |context| puts context.reason } -``` - -Each registration returns an idempotent unsubscribe callable. -Callbacks receive immutable contexts and run outside connection processing. -Unexpected transport loss reconnects with bounded backoff and restores active subscriptions. -Broadcast recovery stays within one client lifetime and one authenticated session lineage; it is not persisted across processes. -Postgres subscriptions do not recover missed publications. -`disconnect` permanently closes that realtime facade. To change users, disconnect the old client and create a fresh client before authenticating and subscribing as the replacement user. -Do not use realtime delivery as a durable record of every database change. diff --git a/docs/sandboxes.md b/docs/sandboxes.md deleted file mode 100644 index c79b1907..00000000 --- a/docs/sandboxes.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: Sandboxes -description: Run isolated commands and manage sessions, files, and HTTP services from Ruby. ---- - -Run a command from trusted backend code in an environment with Sandbox access enabled: - -```ruby -require 'volcano' -require 'securerandom' - -client = Volcano::Client.new( - anon_key: ENV.fetch('VOLCANO_ANON_KEY'), - service_key: ENV.fetch('VOLCANO_SERVICE_KEY'), - api_url: ENV.fetch('VOLCANO_API_URL', 'https://api.volcano.dev') -) -project_id = ENV.fetch('VOLCANO_PROJECT_ID') -request_id = SecureRandom.uuid -result = client.sandboxes.exec( - project_id, 'python -c "print(42)"', - region: 'us-east-1', preset: 'python3.12', request_id: request_id -) -puts result.stdout -``` - -Keep the same `request_id` when retrying an uncertain create or execution. A new -ID represents a new operation. The SDK does not replay commands after transport failures. A rejected user token -is refreshed once; the retry preserves the request ID. -Nonzero exits and session command timeouts are result fields. A one-shot execution -timeout raises an API error (HTTP 504); retrying the same request ID returns an -unknown-outcome conflict (HTTP 409), without running the command again. API failures -raise typed `Volcano::Error` exceptions with `status`, `code`, and `retry_after` -when supplied by the server. - -## Keep a session - -```ruby -client.sandboxes.create( - project_id, region: 'us-east-1', preset: 'python3.12', max_duration_seconds: 300 -).use do |session| - 60.times do - break if session.refresh.state == 'running' - sleep 1 - end - raise 'Sandbox did not become ready' unless session.state == 'running' - - bytes = (0..255).to_a.pack('C*') - session.files.write('/workspace/input.bin', bytes) - raise 'File changed' unless session.files.read('/workspace/input.bin') == bytes - puts session.exec('wc -c /workspace/input.bin').stdout -end -``` - -Creation, suspension, resumption, and termination are asynchronous. Use `refresh` -to observe state; `state` is an immutable snapshot. `use` requests termination -when its block exits, including on exceptions. If cleanup also fails, the original -block exception is preserved. -Cleanup treats an already terminating, terminated, or missing session as complete. -It does not wait for termination to complete. Use `get(session_id)` -to reconnect, then `suspend`, `resume`, or `terminate` as needed. Files preserve -binary `String` content. Writes accept at most 8 MiB. - -## Access a background HTTP service - -Inside a running session, detach the service and redirect its streams: - -```ruby -session.exec('nohup python -m http.server 8080 --bind 0.0.0.0 >/tmp/http.log 2>&1 (sent, total) { puts "#{sent}/#{total}" } - ) - puts uploaded.name -end -``` - -The helper creates an upload session, uses the server-selected part size, and completes the object. -It streams seekable IO and spools non-seekable inputs to a temporary file with bounded reads. -Progress runs after each successful part with cumulative uploaded bytes and total size. -If a part or progress callback fails, the helper attempts to abort and raises the original error. - -## Resume or cancel an upload session - -For application-managed recovery, retain the session ID and upload progress yourself: - -```ruby -payload = "example file".b -session = bucket.create_upload_session( - "examples/resumable.txt", - total_size: payload.bytesize, - content_type: "text/plain" -) -offset = 0 -while offset < payload.bytesize - bucket.upload_part( - "examples/resumable.txt", - session_id: session.session_id, - part_number: offset / session.part_size + 1, - data: payload.byteslice(offset, session.part_size) - ) - offset += session.part_size -end -status = bucket.get_upload_session("examples/resumable.txt", session_id: session.session_id) -puts [status.parts_uploaded, status.bytes_uploaded] -completed = bucket.complete_upload_session("examples/resumable.txt", session_id: session.session_id) -``` - -The server returns the part size, total part count, and session expiration. -`get_upload_session` exposes uploaded-part metadata to help an application resume. -Uploading the same part number again replaces that part. -To abandon an unfinished session, call `bucket.abort_upload_session(path, session_id: session_id)`; this discards its uploaded parts without publishing an object. Further session-status requests report not found. - -Authenticated storage requests refresh and retry once after an explicit HTTP 401 when a refresh token is available. -They preserve the original request and session ownership, and do not retry ambiguous network failures or HTTP 403 responses. diff --git a/docs/versions.md b/docs/versions.md deleted file mode 100644 index 3621c3c5..00000000 --- a/docs/versions.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Ruby SDK versions -description: Pin a tested SDK version, check runtime compatibility, upgrade safely, and restore a previous application dependency set. ---- - -Pin the SDK version your application has tested. For example: - -```ruby -gem "volcano-sdk", "= 0.10.1" -``` - -Add this line to your Gemfile, then run `bundle install`. - -These are example versions, not a moving latest-version reference. -Commit `Gemfile` and `Gemfile.lock`. Install in Bundler frozen mode in CI and deployment so an unexpected dependency change fails. - -## Runtime and compatibility - -Use Ruby 3.3 or later. Native CI tests Ruby 3.3 and 3.4; it does not test every intervening interpreter release. REST methods are synchronous, while realtime uses an asynchronous task lifecycle. - -The gem is currently on the 0.x release line. Treat a minor-version update as potentially incompatible and read its release notes before upgrading. -JavaScript, Python and Ruby releases have independent version numbers; matching numbers are not a compatibility requirement. -Ruby raises typed exceptions; JavaScript keeps its result-envelope API. -Follow your language's public facade and examples rather than importing generated transport classes. - -The docs describe the current SDK source. Confirm a method is included in your installed version by checking its [release notes](https://github.com/Kong/volcano-sdk-ruby/releases) and [changelog](https://github.com/Kong/volcano-sdk-ruby/blob/main/CHANGELOG.md). -Server-dependent features also need the corresponding Volcano API behavior; installing a newer SDK does not deploy that behavior. - -## Upgrade an application - -1. Read the release notes between your installed version and the intended version, including breaking changes and runtime requirements. -2. Update the dependency in a branch and review the resolved lockfile changes. -3. Run the [documented quickstart](./README.md) with a disposable test project, then run the application tests for the features you use. -4. Deploy the tested application and dependency lock together. Retain the previous tested application revision and lock. - -A successful package import proves installation, not compatibility with every deployed API feature. - -## Restore a previous version - -Restore the previously tested application revision and its dependency lock together, then install from that lock in a clean environment. -Run the same quickstart and application tests before redeploying it. -Do not select an arbitrary older SDK version or downgrade only the top-level dependency while retaining a different transitive dependency tree. - -Restoring application packages does not revert server configuration, schema changes, stored data or completed operations. -Check those dependencies before rolling back an application that changed them. - -## Report a compatibility problem - -Open an [SDK issue](https://github.com/Kong/volcano-sdk-ruby/issues) with the installed SDK version, runtime version, affected method, expected result and a minimal reproduction. -Include a sanitized error category and status when available. -Remove keys, tokens, passwords and private response bodies. - -For suspected vulnerabilities, including authentication or authorization regressions, follow [Kong’s vulnerability reporting process](https://konghq.com/compliance/vuln-disclosure) and email vulnerability@konghq.com. Do not post security reproductions in public issues, pull requests or discussions. diff --git a/maintainers/releasing.md b/maintainers/releasing.md index 1032e6e1..86a03002 100644 --- a/maintainers/releasing.md +++ b/maintainers/releasing.md @@ -56,7 +56,7 @@ This does not replace registry installation or approved live platform acceptance ## Recover from a bad release -For an application regression, first restore its previously tested application revision and dependency lock using [the public guide](../docs/versions.md). +For an application regression, first restore its previously tested application revision and dependency lock using [the public guide](https://docs.volcano.dev/sdk/ruby/versions). Confirm compatibility with current server configuration and data; an SDK downgrade does not roll either back. Record the affected versions, symptom, safe previous version, artifact digests and any required data/server remediation in the incident or release issue.