fleetlessfleetlessdocs
Reference/SDK/Auth & MCP consent

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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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()

ts
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

ts
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.