Aster UI / Guides
Recover an expired session
A session can expire while a user sends a message, saves an edit or waits for a background refresh. Put recovery around the transport once so every connected control can wait for sign-in without losing its draft.
Try the mock mail workspace. Start a message, open Demo controls, choose Expire session, then send it. The dialog offers simulated passkey, email and SSO flows adapted from Aster's account-access preview. Nothing is sent outside the local mock service. Cancelling sign-in leaves the draft available for retry.
Connect your authentication mechanism
import { Authentication, DataContext, fetchTransport } from '@getaster/data';
const authentication = new Authentication({
// Include the tenant as well as the account. Use your SDK's verified identity.
identity: () => session.accountKey,
credentials: async () => ({ Authorization: `Bearer ${await session.accessToken()}` }),
recover: async ({ signal }) => {
// Your implementation can renew silently, then offer interactive sign-in.
// Resolve only when fresh credentials are ready; honour cancellation.
await session.restore({ signal });
},
timeoutMs: 120_000,
});
const ctx = await DataContext.connect(
authentication.wrap(fetchTransport('/odata')),
);
session above is your application or identity-provider integration; Aster does
not supply an identity provider. For cookie sessions, omit credentials and
configure fetchTransport('/odata', { credentials: 'include' }) where appropriate.
Use an SDK or an unwrapped transport for sign-in itself so it cannot wait on its
own recovery. Only wrap services trusted to receive that context's credentials.
One coordinator represents one authentication context. Concurrent challenged requests share recovery, and requests arriving during recovery wait before sending. Late failures from the old credentials do not open another dialog after successful recovery. Each challenged operation can be replayed once.
Return useful API errors
The default trigger is 401. Use 403 for access denied; it does not open
login. Return a WWW-Authenticate challenge with 401, rather than redirecting API
calls to an HTML login page. Network failures and CORS failures do not trigger
re-authentication. If you inspect challenge headers across origins, the server
must expose those headers through its CORS configuration.
For a legacy server, supply a narrow classifier:
challenge: ({ response }) => response.status === 401 || (
response.status === 403 && response.headers['x-session-state'] === 'expired'
),
Treat ordinary permission denial separately. A login endpoint rejecting entered credentials should display that failure in the login UI, without starting another recovery cycle.
Choose which operations may replay
Only GET is replayable by default. A rejected write throws
AuthenticationError with reason replay-blocked before opening login. Allow
writes only when the server guarantees the rejected operation was not applied,
or when a stable idempotency key and server deduplication make replay safe.
The mail mock rejects expired sessions before touching data, so its explicit policy is:
replay: ({ response }) => response.status === 401,
This is a guarantee of that mock server, not a policy to copy blindly for another API. Aster retains the original body, ETag and idempotency key across replay and rebuilds credential headers for every send. It does not retry an ambiguous network failure or assume aborting a dispatched write undoes it.
Cancellation, account changes and feedback
A low-level ODataRequest accepts an AbortSignal. Cancelling one request leaves
other recovery waiters alive; cancelling all waiters aborts recovery. Call
authentication.dispose() when the owning context ends. Recovery has a five-minute
default deadline, configurable with timeoutMs, even if an integration ignores
abort. Your handler must honour its signal to close its own UI and release SDK work.
When identity() initially returns a stable key, switching accounts or tenants
fails waiting operations with identity-changed. Rebuild your data context,
record store and permissions for the new identity instead of replaying the old
account's operations. Returning undefined permits an initial login; provide the
stable key once signed in.
authentication.recovering is an immediate readonly signal. Bind it to
ProgressStatus for delayed silent-renewal feedback. An
interactive sign-in dialog should appear when user action is required. Keep
errors immediate and preserve the form or draft beneath it.
Failures expose a typed reason: cancelled, timeout, failed,
identity-changed, retry-limit, replay-blocked or disposed. Individual
caller cancellation preserves its AbortSignal reason. Successful recovery does
not imply the retried operation succeeds: validation, conflicts and permission
errors still flow through the normal data APIs.
Put failures beside the affected task
Keep transport error details in the command state and translate them into useful
application text. The mail example shows a failed send above the same draft's
fields, with Try sending again and Dismiss. Refresh failures use one
workspace notification. Single-click previews a message; double-click or Enter
opens a keyed tab through Grid's cancellable row-open event.
CommandBar supports formatError(error, action) for application wording, or
showErrors: false when another surface owns feedback. The mail YAML selects the
latter so the same exception is not repeated in the toolbar, draft and footer.
Cancellation retains the draft; it does not display a successful-send message.
Read the working implementation
The workspace YAML declares the Sidebar, shared Grid, CommandBar and retained Tabs. Application code contains the full authentication handler, mock server boundary and composition registry. The mail model owns keyed open messages, drafts, commands and selection. Presentation supplies the responsive layout. These are the actual files executed by the demo.
Open-tab keys and drafts in this example belong to its application model. Persistent sessions, generalized keyed composition children and resumable redirect/reload recovery remain separate work. The 30-second refresh option is explicitly controlled by the page; no hidden background subscription is implied.
See Authentication and transport for the public APIs.