SDK · Auth & MCP consent
The client auth API an app user’s own UI calls: registration and email verification, login and logout, password recovery, invitation acceptance, federated sign-in through one of the app’s identity providers, and the MCP consent screen the app draws for its own users. Fleetless renders none of these pages.
Generated from the SDK’s own source. The SDK Reference is the narrative; this page is every member, in the order the SDK declares them.
AuthApi
Who the caller is, reachable as client.auth — the whole client
authentication API, as JSON.
Fleetless serves an app user no page. The developer’s own UI owns every
screen: login, registration, verification, invitation acceptance, password
reset, the provider buttons and the MCP consent. These methods are what those
screens call. The hosted, app-branded login and consent pages this SDK used
to drive are gone, along with beginHostedLogin/completeHostedLogin.
The enumeration discipline is the design’s, and it shapes this surface.
register, resendVerification and requestPasswordReset resolve for every
policy-allowed request whether or not the address exists, and login answers
the identical invalid_credentials for a wrong password, a blocked account
and an unverified one. So: resolving does not mean an account exists, and
the only honest refusals are the ones about policy rather than about a
person — registration_closed, domain_not_allowed, quota_exceeded.
A client built with a serverKey refuses everything that needs an app
user’s own session with invalid_option, before any request. me(),
listProviders(), mcpInteraction() and oidcErrorFromCallback() still
work on one: the first is what a server key is for, the next two are public
reads the cloud answers without any credential at all, and the last touches
no network.
register()
register(input: RegisterOptions): Promise<void>
Self-registration. Writes the account as pending_verification and mails
the app’s verification link; the account cannot log in until that link is
spent (verifyEmail).
Resolves on the route’s 202 — which the cloud answers for every
policy-allowed request, whether the address was new or already known. It is
not a claim that an account was created, and an app that renders it as one
(“welcome, Ada!”) is showing a stranger the enumeration oracle this whole
family is built to avoid. Render “check your mail” instead.
Throws a FleetlessError carrying the cloud’s own code for a refusal, and
that is the distinction this method exists to preserve: registration_closed
(the app has self-registration off), domain_not_allowed (the address is
outside the app’s allowed domains), quota_exceeded (the org has as many
app users as its quota allows) and not_found (no app carries this
client’s appIdentifier) are all things the app can say out loud, because
none of them is about whether a person exists.
| Parameter | Type | Required | Description |
|---|---|---|---|
input |
RegisterOptions |
yes |
Returns Promise<void>.
References: RegisterOptions
verifyEmail()
verifyEmail(token: string): Promise<void>
Spends a verification token and stores the session it answers with, so the person is not asked to log in immediately after proving they can read the mail.
token_spent covers unknown, expired and already-used alike — one code,
because the remedy is one thing: ask for a fresh link with
resendVerification. An app rendering this refusal should offer that.
| Parameter | Type | Required | Description |
|---|---|---|---|
token |
string |
yes |
Returns Promise<void>.
resendVerification()
resendVerification(email: string): Promise<void>
Asks for the verification mail again. Resolves on 202 for every policy-allowed request, existing address or not — same reason as register.
| Parameter | Type | Required | Description |
|---|---|---|---|
email |
string |
yes |
Returns Promise<void>.
login()
login(email: string, password: string): Promise<void>
Exchanges email + password, and the client’s configured app identifier, for a session.
invalid_credentials is answered identically for a wrong password, a
blocked account and one still waiting to verify. Do not try to tell them
apart — there is nothing in the answer that does, deliberately.
| Parameter | Type | Required | Description |
|---|---|---|---|
email |
string |
yes |
|
password |
string |
yes |
Returns Promise<void>.
logout()
logout(): Promise<void>
Ends the session: revokes the whole refresh-token family server-side (a stolen refresh token stops working immediately), closes this client’s live realtime connection if it has one, and clears the local store.
Never rejects, and always clears the store, even if the server call fails: a user who presses “log out” must end up logged out locally regardless of the network.
What this does not do: invalidate the access token already issued. Access-token checks are stateless (a signed JWT, verified without a lookup), so logout has nothing to flip on that token — only on the refresh family behind it. A token stolen before logout keeps working on REST, and can still open a new realtime connection, until it expires on its own, at most 15 minutes. That is a deliberate boundary of the stateless-JWT design, not a bug, but a kiosk or a shared workstation needs to know the number.
Nor does it end a session at the identity provider. It used to report what was left of one; that apparatus belonged to the hosted login, where Fleetless owned the browser. The app owns it now, and an app that wants to end a provider session redirects there itself — knowing its own provider, which Fleetless never did better than it.
Returns Promise<void>.
me()
me(): Promise<{ kind: "developer" | "server_key" | "app_user"; developer_id: string | null; app_user_id: string | null; server_key_id: string | null; app_id: string | null; role_id: string | null; email: string | null }>
Who the caller turned out to be, without decoding a token client-side — which is how apps end up trusting claims nobody verified.
Returns Promise<{ kind: "developer" | "server_key" | "app_user"; developer_id: string | null; app_user_id: string | null; server_key_id: string | null; app_id: string | null; role_id: string | null; email: string | null }>.
changePassword()
changePassword(currentPassword: string, newPassword: string): Promise<void>
Changes the current app user’s password.
currentPassword is required even though the session already proves
identity — it is what stops a stolen session from becoming a stolen
account.
Every other session of this identity is revoked, and this call’s own
session is re-issued rather than spared. The request carries nothing
identifying the caller’s own refresh family, so the server revokes all of
them and hands back a fresh pair, which this method stores exactly like
login. A user with other tabs or devices signed in will see those signed
out the moment this resolves; if your app does not make that consequence
visible before they confirm, they will find out from a support ticket.
| Parameter | Type | Required | Description |
|---|---|---|---|
currentPassword |
string |
yes |
|
newPassword |
string |
yes |
Returns Promise<void>.
requestPasswordReset()
requestPasswordReset(email: string): Promise<void>
Asks for a reset link. Resolves on 202 for a known and an unknown address alike — the answer says nothing about which it was.
| Parameter | Type | Required | Description |
|---|---|---|---|
email |
string |
yes |
Returns Promise<void>.
confirmPasswordReset()
confirmPasswordReset(token: string, newPassword: string): Promise<void>
Spends a reset token, sets the new password and stores the session it answers with. Every refresh family of that user is revoked first — a forgotten password is one of the two states where somebody else may be holding a session.
| Parameter | Type | Required | Description |
|---|---|---|---|
token |
string |
yes |
|
newPassword |
string |
yes |
Returns Promise<void>.
acceptInvitation()
acceptInvitation(input: AcceptInvitationOptions): Promise<void>
Accepts an app invitation: creates the account (or activates one invited before it existed) with the role the invitation fixed, and stores the session.
An invitation always bypasses the app’s domain whitelist — a developer inviting somebody by hand has already made the decision the whitelist automates.
| Parameter | Type | Required | Description |
|---|---|---|---|
input |
AcceptInvitationOptions |
yes |
Returns Promise<void>.
References: AcceptInvitationOptions
listProviders()
listProviders(): Promise<ProviderButton[]>
The app’s enabled sign-in providers, for drawing the buttons on your own login screen. A disabled provider is not a button that refuses; it is a button that is not there.
Public and unauthenticated, and it carries nothing but slug and name on
purpose: the issuer, the client id, the scopes and the linking policy are
management-side facts that would tell a stranger how the app’s federation
is configured.
Returns Promise<ProviderButton[]>.
References: ProviderButton
beginOidcLogin()
beginOidcLogin(input: BeginOidcLoginOptions): Promise<OidcLoginRequest>
Builds the URL that starts a federated sign-in, with a fresh state and a
fresh PKCE verifier. Makes no network call and does not navigate —
persist state and codeVerifier, then send the browser to url.
Nothing about the app, the provider or the redirect URI is validated here; it is all checked when the browser actually reaches the route, in that order, with the redirect target checked before the provider so that a caller who got the target wrong learns nothing about which providers the app has.
Async only because the S256 code_challenge needs crypto.subtle.digest,
which the Web Crypto API only ever offers as a promise.
| Parameter | Type | Required | Description |
|---|---|---|---|
input |
BeginOidcLoginOptions |
yes |
Returns Promise<OidcLoginRequest>.
References: BeginOidcLoginOptions, OidcLoginRequest
completeOidcLogin()
completeOidcLogin(input: CompleteOidcLoginOptions): Promise<void>
Completes a federated sign-in: checks state against expectedState,
trades the one-time code for a session, and stores it.
The state check runs before any request is sent. RFC 6749 §10.12’s
whole point is that a client must not complete an authorization response it
did not itself request — a check made after the exchange would already have
spent a code for a flow this client never started. Both an outright
mismatch and an empty expectedState throw state_mismatch; the message
says which, because the remedies differ (“check how your app persisted the
value” versus “this response belongs to a sign-in you did not start”) even
though the next step is the same either way — start the sign-in again.
The code lives 60 seconds and is single-use. Unknown, expired, replayed and
“the account was blocked in between” all arrive as one token_spent,
because the app has nothing different to do about any of them.
| Parameter | Type | Required | Description |
|---|---|---|---|
input |
CompleteOidcLoginOptions |
yes |
Returns Promise<void>.
References: CompleteOidcLoginOptions
oidcErrorFromCallback()
oidcErrorFromCallback(params: URLSearchParams): FleetlessError | null
Reads a failed federated sign-in off the redirect back, as a
FleetlessError you can branch on, or null when the callback carries no
error at all.
Fleetless renders no page for these: the reason is carried to your own
redirectUri as ?error=<code>, and this turns that string into the same
error type every other method throws. A code the contracts define (see
ClientOidcErrorCode) becomes that code verbatim; anything else becomes
unexpected_response with the raw value in the message, rather than being
passed through as a code neither side defines.
Purely local — it parses a query string and asks nothing.
| Parameter | Type | Required | Description |
|---|---|---|---|
params |
URLSearchParams |
yes |
Returns FleetlessError | null.
References: FleetlessError
mcpInteraction()
mcpInteraction(id: string): Promise<{ id: string; app_id: string; client_name: string | null; client_name_verified: false; scopes: string[]; already_granted: boolean; expires_at: string }>
Reads a pending MCP authorization by the interaction id the browser arrived with, so the app can render its own consent screen.
client_name is a string the client typed about itself during an
unauthenticated dynamic registration — nobody checked it, which is why
client_name_verified is the literal false rather than a boolean with a
true branch that could never happen. Do not render it as an identity.
interaction_expired means exactly that: ten minutes ran out, or the id
was never real. Both answer the same way, so the screen to show is “that
took too long, start again” rather than an error.
Call this with the app user already signed in. The route needs no
credential, but it reads one if present, and already_granted is false
for an anonymous read whatever the truth is — so a consent screen rendered
from an unauthenticated call asks a person to agree to something they
agreed to already. An expired token counts as anonymous to this route,
which answers 200 rather than refusing, so this method probes the
session’s liveness first and refreshes if it can; a session that cannot be
refreshed is not an error here, it is genuinely anonymous.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string |
yes |
Returns Promise<{ id: string; app_id: string; client_name: string | null; client_name_verified: false; scopes: string[]; already_granted: boolean; expires_at: string }>.
approveMcpInteraction()
approveMcpInteraction(id: string): Promise<McpInteractionDecision>
Approves a pending MCP authorization on behalf of the signed-in app user, and returns where to send the browser.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string |
yes |
Returns Promise<McpInteractionDecision>.
References: McpInteractionDecision
denyMcpInteraction()
denyMcpInteraction(id: string): Promise<McpInteractionDecision>
Denies one. Also returns a redirect — with error=access_denied on it, so the client learns from its own callback.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string |
yes |
Returns Promise<McpInteractionDecision>.
References: McpInteractionDecision
listMcpGrants()
listMcpGrants(): Promise<({ client_id: string; client_name: string | null; client_name_verified: false; granted_at: string })[]>
Every MCP client this app user has standing consent for — the “connected apps” list, and the door out of a decision a person could otherwise make once and never unmake. A withdrawn grant is never listed.
client_name_verified is false here for the reason it is on
mcpInteraction, and it matters more rather than less: a list like this is
read long after the moment of approval, when nobody remembers what they
clicked.
Returns Promise<({ client_id: string; client_name: string | null; client_name_verified: false; granted_at: string })[]>.
revokeMcpGrant()
revokeMcpGrant(clientId: string): Promise<void>
Withdraws one standing consent by the client’s id.
Resolves whether or not there was anything to withdraw — a client id this account never approved and one it withdrew a minute ago both land on the end state the caller asked for. A refusal there would tell a caller which clients an account has connected, and would turn a double-clicked button into a failure.
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string |
yes |
Returns Promise<void>.
RegisterOptions
register()'s input. The app identifier is not here: the client already
holds one (createClient({ appIdentifier })) and sends it itself, so there
is no way for a caller to register somebody into a different app than the
one this client speaks for.
| Property | Type | Required | Description |
|---|---|---|---|
email |
string |
yes |
The address the verification mail goes to. Nothing works until that link is spent. |
password |
string |
yes |
At least 12 characters — clientRegisterRequest refuses less with a validation_error. |
displayName |
string |
no |
What the app should call this person. Optional, and omitted from the request entirely when you do not pass it — clientRegisterRequest is a strict schema, so a key carrying undefined would be a 422 rather than a default. |
AcceptInvitationOptions
acceptInvitation()'s input — the token out of the mailed link, plus the password the account gets.
| Property | Type | Required | Description |
|---|---|---|---|
token |
string |
yes |
The token from the invitation link the developer’s app was linked to. |
password |
string |
yes |
At least 12 characters. The invitation fixes the role; this call fixes the credential. |
displayName |
string |
no |
Optional, and omitted from the request entirely when absent — same strict-schema reason as RegisterOptions.displayName. |
ProviderButton
One sign-in button on the app’s own login screen, as listProviders() lists it.
| Property | Type | Required | Description |
|---|---|---|---|
slug |
string |
yes |
What beginOidcLogin addresses this provider by. |
name |
string |
yes |
The label the developer configured, to be rendered on the button. |
BeginOidcLoginOptions
beginOidcLogin()'s input: which provider, and where Fleetless should send the browser back to.
| Property | Type | Required | Description |
|---|---|---|---|
slug |
string |
yes |
A slug from listProviders(). An unknown one is a 404 when the browser reaches the start route, not here. |
redirectUri |
string |
yes |
Where the browser comes back to with ?code=…&state=… (or ?error=…). Checked against the app’s allowed origins server-side, by origin — so the path is yours to choose and the origin is not. Two shapes are refused outright, before the origin is compared, and neither refusal mentions them: a URL carrying a fragment (https://app.example.com/#/auth/callback) and one carrying userinfo (https://someone@app.example.com/cb). Both come back as a flat 400 invalid_redirect_uri reading “The redirect_uri is not an origin this app answers for”, which sends people to re-check an allow-list that was never the problem. The fragment case is the one that costs time, because hash routing is the default for a static-hosted SPA with no server rewrite. Give the callback a real path (/auth/callback) and let your router pick the hash route up from there; a fragment is a browser-side construct the redirect could not carry a code in anyway. |
OidcLoginRequest
What beginOidcLogin() returns. Nothing here has touched the network.
| Property | Type | Required | Description |
|---|---|---|---|
url |
string |
yes |
Send the end user’s browser here. This SDK does not navigate — it has no opinion about whether that is a full page load, a popup or a native web view, the same boundary cameras.live draws by handing back a URL and a token and stopping there. |
state |
string |
yes |
Persist this next to codeVerifier before navigating away, and pass both back into completeOidcLogin. The SDK does not persist them for you: the redirect back is a fresh page load for a browser app, and nothing kept in this SDK’s memory survives it. sessionStorage, a signed cookie or a plain variable (a popup flow that never truly navigates) are all valid — that choice is the caller’s, and an in-memory default here would look like it worked right up until the first real redirect. |
codeVerifier |
string |
yes |
The PKCE code verifier for this attempt — persist it exactly as state. The app runs its own PKCE against Fleetless, a second exchange independent of the one Fleetless runs against the identity provider, which is what makes the one-time code in the redirect worth nothing to whoever else reads that URL. |
CompleteOidcLoginOptions
completeOidcLogin()'s input — the redirect back, plus what beginOidcLogin returned for this same attempt.
| Property | Type | Required | Description |
|---|---|---|---|
code |
string |
yes |
The code query parameter from the redirect back to redirectUri. It lives 60 seconds. |
state |
string |
yes |
The state query parameter from that same redirect. |
expectedState |
string |
yes |
The state this attempt’s beginOidcLogin returned. Compared before any network call. |
codeVerifier |
string |
yes |
The codeVerifier this attempt’s beginOidcLogin returned. |
ClientOidcErrorCode
type ClientOidcErrorCode = z.infer<typeof clientOidcErrorCode>
McpInteractionDecision
What approveMcpInteraction/denyMcpInteraction resolve with: where to
send the browser, and nothing else.
A denial carries a redirect too, with error=access_denied on it — a client
that is refused has to learn so from its own callback rather than from a page
nobody sent it, so both outcomes end the same way for the app: navigate here.
| Property | Type | Required | Description |
|---|---|---|---|
redirectTo |
string |
yes |
The absolute URL to navigate to. Wire field redirect_to. |
ClientMcpInteraction
The client-mcp-interaction wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.
McpConsentGrant
The mcp-consent-grant wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.
ClientIdentity
The client-identity wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.