SDK reference
Build once, run against any node. You publish the app’s signed
manifest to a registry; a node’s owner enables it there. The user types
their node URL into the gear, signs in on that node’s own page and
approves the app, and the node holds their account and the app’s data.
See examples/notes in the node’s
repository for the full sample, and spec/openapi.json
(pinned from the spec in cloudoakhq/cloudoak-developer) for the wire
contract.
The SDK is not on npm yet. It is developed in this repository,
cloudoakhq/cloudoak-sdk,
and ships as a tarball with each of its releases
(cloudoak-sdk-2.0.0.tgz, with the release’s SHA256SUMS). Download
it and install the file:
npm install ./cloudoak-sdk-2.0.0.tgzIt has no dependencies. React is an optional peer, needed only for
@cloudoak/sdk/ui.
New here? From zero to an app on a node takes the starter app from an empty folder to signed, published, installed and upgraded, with every command.
Then the guide: sign-in, data and sync with
the Cloudoak class, which is what most apps use. This page is the
reference for the pieces underneath it.
Gear flow
Section titled “Gear flow”import { CloudoakClient, tables } from '@cloudoak/sdk';
const client = await CloudoakClient.connect({ nodeUrl: 'https://cloud.example.com', // normalized to https; http only for localhost appId: 'com.example.notes', appVersion: '1.0.0', // this build's version; required});// Discovery (/.well-known/cloudoak) is verified before anything else;// credentials only ever go to this URL afterwards.appVersion is required and must be semver (1.2.0, 1.2.0-rc.1).
Use the version of this build, which should match app.version in
the manifest the build was made for. The SDK sends it as
X-Cloudoak-App-Version on every request, along with its own version
in X-Cloudoak-SDK-Version, and as app_version on the browser
sign-in URL. See app versions for what the node does
with it.
Sign-in happens on the node’s own page. The app sends the user there, the node asks for the password and for approval, and the app gets a one-time code back. The app never sees the password, and the approval screen is drawn by the node, not by the app being approved.
The address the node returns to must be listed, exactly, in the manifest:
"auth": { "redirect_uris": ["https://notes.example.dev/", "com.example.notes:/callback"], "origins": ["https://notes.example.dev"]}redirect_uris are https addresses, http://localhost addresses for
development, or a custom scheme in reverse-DNS form for native apps.
origins are the web origins allowed to call the node for this app;
a browser app on any other origin is refused. Native apps need none.
Web:
const client = await CloudoakClient.connect({ nodeUrl, appId: 'com.example.notes', appVersion: '1.2.0', redirectUri: 'https://notes.example.dev/',});// On every page load: finish a sign-in the node just sent back.if (!(await client.completeSignIn())) { // From the gear: leave for the node's page. await client.signIn();}Native apps pass the system browser session and get the tokens from one call:
const client = await CloudoakClient.connect({ nodeUrl, appId: 'com.example.notes', appVersion: '1.2.0', redirectUri: 'com.example.notes:/callback', openAuthSession: async (url, redirectUri) => { const result = await WebBrowser.openAuthSessionAsync(url, redirectUri); if (result.type !== 'success') throw new Error('sign-in was cancelled'); return result.url; },});await client.signIn();client.session() says who signed in (user.username, never an
email) and which node this is.
An app the node has not seen before is installed on first sign-in only
if the node’s operator trusts its publisher; otherwise signIn fails
with pending_approval until an admin approves it.
A node operator can turn on the older password grant
(CLOUDOAK_APP_PASSWORD_LOGIN=on), where an app collects the password
itself. It is off by default and this SDK does not use it: no function
or component in the SDK takes a password, and a test fails if one ever
does.
Refresh tokens rotate on every use; reuse revokes the family
(refresh_revoked: sign in again). logout() revokes the family.
Data, files and polling sync
Section titled “Data, files and polling sync”await tables(client, 'notes').list();await documents(client, 'settings').create({ theme: 'dark' });await vectors(client, 'note_embeddings').query({ vector, k: 5 });const { putUrl } = await files(client, 'attachments').reserve('a.pdf', size, 'application/pdf');await changes(client, sinceCursor);Live updates are GET /changes polling only. Start with no cursor,
keep the next value each poll returns and send it back; it is an
opaque string, so never build or compare one. Poll on an interval that
fits the app (5 to 30 seconds while visible), back off on failures,
and keep the cursor across restarts.
The feed does not skip entries, including when two writes overlap. A
change appears once every write that started before it has finished,
so your own write can take a moment to show up in the feed. If a poll
answers resync: true the cursor could not be continued (the node was
restored from a backup): reload what you show and carry on from next.
Files: files(client, bucket).reserve(name, size, contentType) returns
an upload URL that only accepts exactly that size and content type.
Upload with uploadBytes, then call finalize(id); the file can be
downloaded only after the node has checked what arrived. Downloads are
served as attachments. An upload that is never finalized stops
counting against quota after 20 minutes and is cleared soon after.
Token storage
Section titled “Token storage”Short access tokens stay in memory; refresh tokens live in a
TokenStore — memory default for web, localStorage adapter, or a
native secure-store adapter (Keychain/Keystore). Apps may supply their
own store.
What approving an app grants
Section titled “What approving an app grants”A user who approves your app gives it all of their data in your app on that node, and nothing else. Manifest scopes are shown on the node’s sign-in page as your description of what the app uses; the node does not enforce them one by one, and the page says so. An app token never works on another app, or on the user’s account, grants or developer routes.
API tokens for scripts
Section titled “API tokens for scripts”Every account can mint API tokens (oak_pat_...) in the console
(Account > API tokens), for scripts and devices that use its own data. A token
is limited to its scopes: data:read or data:write for every app,
app:<app-id>:read or app:<app-id>:write for one app. It reaches
what the account can reach in the app and no more, and no token
manages apps: that is done in the terminal on the node. Send it as
Authorization: Bearer. It needs no app version header.
Errors
Section titled “Errors”Every failure is a typed CloudoakError (consent_required,
pending_approval, refresh_revoked, not_installed, …) with the
HTTP status and node request id. pending_approval means an admin
must approve the install first — show it, don’t retry blindly.
App versions
Section titled “App versions”A node has one version of your app’s manifest installed, and it
accepts builds from the manifest’s min_client up to that installed
version. Outside that range a call fails with one of two errors:
client_update_required(426): this build is older than the node accepts. Ask the user to update the app.app_upgrade_required(409): this build is newer than what the node has installed. The node’s admin has to upgrade the app; the user cannot fix it from the device.
Both carry clientVersion, installedVersion and minClientVersion
(unset when the manifest has no minimum). Tell them apart from other
failures with isVersionMismatch, and check error.code, not the
status: 409 is also an ordinary write conflict.
import { CloudoakClient, isVersionMismatch } from '@cloudoak/sdk';
const client = await CloudoakClient.connect({ nodeUrl, appId: 'com.example.notes', appVersion: '1.2.0', // Called for every version mismatch, whichever call hit it. onVersionMismatch: (error) => showUpdateBanner(error),});
try { await tables(client, 'notes').list();} catch (error) { if (isVersionMismatch(error)) { /* already reported to the hook */ }}The user stays signed in through a mismatch. Tokens are kept, and a refresh that is turned away does not use up the refresh token, so the app works again as soon as it is updated or the node is upgraded.
Drop-in UI
Section titled “Drop-in UI”@cloudoak/sdk/ui ships the gear button (React peer,
optional); the headless entry never loads React. There is no sign-in
form or consent sheet in it: both are on the node’s page.
CloudoakVersionNotice takes any caught error and renders an update
or ask-your-admin message when it is a version mismatch, and nothing
otherwise:
<CloudoakVersionNotice error={error} appName="Notes" />Built from spec 2.1.0, SDK 2.0.0 and the node's guide at v0.6.0-alpha.1.