Tokens
Last updated 29 October 2026
How the requests to a stream get their bearer token: the token option, a token provider, and the token file.
On Tonbo, the token is an access token exchanged for an API key (see The token exchange). Through the /tonbo entry the extension makes that exchange for you and keeps the token fresh; the root entry takes a token you supply.
Where the token comes from
- The
tokenoption of the last open() that names one: a string, or a provider. - Through
@tonbo/sqlite-ursula/tonbowithout atoken, unlessURSULA_VFS_TOKEN_FILEis set: the API key, which the extension exchanges for Streams tokens and refreshes on a thread of its own, at 70 to 80% of each token’s lifetime and at once after a401. A failed refresh backs off from 1 second up to 30 while the last token serves. An open by name, or resolveDatabase(), makes the key the source again after an open with atokenreplaced it. This is so since 0.11.2; 0.11.1 ran a token provider for the key. URSULA_VFS_TOKEN_FILE, which the extension reads only while no open of the process has set a token (see The token file).
No token is ever logged, and no error message carries one.
TokenProvider
Exported by @tonbo/sqlite-ursula and /tonbo, since 0.11.1. Before it, token took a string only.
type TokenProvider = (options: { signal: AbortSignal }) => Promise<BearerToken>;
interface BearerToken {
token: string;
expiresAt: Date | number; // milliseconds since the epoch
}import { open, UrsulaUnavailableError } from "@tonbo/sqlite-ursula";
const token = async ({ signal }) => {
const r = await fetch(exchangeUrl, {
method: "POST",
headers: { authorization: `Bearer ${apiKey}` },
signal,
});
if (r.status === 401) {
// Fails for good: the package stops calling the provider.
throw new UrsulaUnavailableError("unauthorized: the exchange refused the key", "unauthorized");
}
if (!r.ok) throw new Error(`the exchange answered ${r.status}`); // retried
const { access_token, expires_in } = await r.json();
return { token: access_token, expiresAt: Date.now() + expires_in * 1000 };
};
const db = await open(url, { token });- The package calls the provider before an open’s first request when it holds no valid token, again after 70 to 80% of each token’s lifetime, and again at once when a request was refused with
401. Concurrent calls share one. signalfires when the package stops waiting for the answer: after 30 seconds, or once an open with a string token stopped the provider or no database uses it.- To fail for good, throw an UrsulaError whose
actionisfix: an open waiting for its token rejects with it, and the package stops calling the provider until the nextopen()with a provider. Every other error is retried with backoff, from 1 second up to 30, while the token held keeps serving. - An open gets a valid token the package holds at once, so a connection on a database already bound needs no network. An expired token serves only while the provider is failing; otherwise the open waits for a new one.
- When an open’s signal fires while it waits for the provider, it rejects with
UrsulaUnavailableErrorunavailable, whosecauseis the provider’s last error, or the signal’s reason when it did not answer.
BearerToken
What a provider hands out, since 0.11.1.
| Field | Type | What it holds |
|---|---|---|
token | string | The token: printable ASCII without spaces. |
expiresAt | Date or number | When it expires: a Date, or milliseconds since the epoch. A number below 1012 reads as seconds, such as a JWT’s exp, and is refused: pass exp * 1000. |
A token that is not printable ASCII, or an expiresAt that is not a time, fails the open with UrsulaSetupError invalid_argument. A token that expires within 1 second leaves no time to refresh it, and is retried as a failure.
One token per process
- The process carries one token, whichever function minted it, so give it one account’s token: every database it opens is reached as that account. The last open that names a token sets it.
- One API key serves a process’s Streams tokens (since 0.11.2): once the first key held a token, an open by name without a
tokenand with a second key fails withUrsulaSetupErrorinvalid_argumentwhile any database of the process may send requests, and for 60 seconds after an open was handed the first key’s token. A second key with atoken, or withURSULA_VFS_TOKEN_FILE, only looks the name up and is never refused. - An open with another provider, an inline arrow included, shares the token held and the call in flight, and names the function the next call runs. An open with a string stops the provider on its thread, and the opens waiting for it go on under the string.
- A provider lives on one JavaScript thread, so each worker thread runs its own: give the threads of a process the same kind of token.
- The provider’s timers keep the process alive only while an open waits for a token, and it stops once no database its thread opened is bound.
When the server refuses a token
A request of a bound database refused twice with 401 blocks replication, and replication.condition is unauthorized. The provider is called at once, or the API key’s token is refreshed at once, and the blocked request goes again as soon as the new token is set. Commits continue meanwhile, until the backlog fills. A provider failure after the open leaves the token held in place.
The token file
URSULA_VFS_TOKEN_FILE names a file that holds the token, for a process that keeps its token in a file a separate refresher replaces. How to run it is on Connect from Node.js and The refresher. importSqlite() takes a string token of its own, for the import alone.