OAuth Apps SDK

Integrate OAuth Apps from Node.js or TypeScript with the typed OAuthAppClient.

PrerequisitesOAuth connect flow

For Node.js and TypeScript projects, the cliodot npm package (v1.2.6+) provides OAuthAppClient — a typed wrapper around the public and server-to-server HTTP endpoints documented above.

OAuthAppClient is for developer integration only. It does not manage OAuth Apps, provider templates, portal connections, or webhooks. Configure those in the Cliodot portal.

HTTP integration remains fully supported and is the canonical API contract.

Install#

npm install cliodot

Client setup#

import { OAuthAppClient } from "cliodot";
const client = new OAuthAppClient({
  baseUrl: "https://api.cliodot.com",
  appId: "oauth_app_507f1f77bcf86cd799439011",
  appApiKey: "YOUR_APP_API_KEY",
  // appSecret: "a1b2c3d4...",  // alternative to appApiKey
  // debug: false,
});
Config field Required Description
baseUrl yes Public API origin without trailing slash. SDK calls {baseUrl}/oauth/...
appId yes OAuth app _id from the portal
appApiKey no* Sends Authorization: Bearer oak_... on S2S calls
appSecret no* Sends X-Cliodot-App-Id + X-Cliodot-App-Secret on S2S calls
debug no Reserved for diagnostic logging (default false)

One of appApiKey or appSecret is required for exchange and connections.*. Connect methods (connect.start, connect.poll, connect.buildUrl) do not send credentials.

Method overview#

Method Auth HTTP equivalent
client.connect.start(input) None POST /oauth/apps/:appId/connect
client.connect.poll(input) None POST /oauth/apps/:appId/connect/poll
client.connect.buildUrl(input) None GET /oauth/apps/:appId/connect (URL builder, no request)
client.connect.isSamlPostBinding(response) Local helper
client.connect.buildSamlPostForm(response) Local helper
client.exchange(input) S2S POST /oauth/token/exchange
client.connections.get(connectionId) S2S GET /oauth/connections/:connectionId
client.connections.getToken(connectionId) S2S GET /oauth/connections/:connectionId/token
client.connections.sync(connectionId) S2S Alias of getToken
client.connections.refresh(connectionId) S2S POST /oauth/connections/:connectionId/refresh
client.connections.revoke(connectionId, input?) S2S POST /oauth/connections/:connectionId/revoke
client.connections.delete(connectionId) S2S DELETE /oauth/connections/:connectionId

client.connect.start(input)#

Starts an OAuth connect flow programmatically. Maps to POST /oauth/apps/:appId/connect. No authentication headers are sent.

Input (OAuthConnectStartInput)

Field Required Type Description
provider yes string Provider alias attached to your OAuth App (e.g. google)
redirect_uri yes string Customer callback URL; must be in the app's allowlist
scope no string Space-delimited scopes (alternative to scopes)
scopes no string[] Scope list; sent as scopes in POST body and joined as scope in GET URL
state no string CSRF token echoed on customer callback
external_user_id no string Your user key stored on the connection
connection_storage no "persistent" "ephemeral"
appId no string Override the client's default appId for this call
assertion no string JWT or SAML2 assertion for bearer grant types
subject_token no string Subject token for RFC 8693 token exchange
subject_token_type no string Token type URI for subject_token
requested_token_type no string Requested issued token type
actor_token no string Actor token for delegation
actor_token_type no string Token type URI for actor_token
audience no string Target audience for token exchange

Returns (OAuthConnectStartResponse)

Field When present
protocol Always on success (oauth2, oidc, saml)
grant_type OAuth2/OIDC flows
authorization_url Authorization code / OIDC redirect flows
sso_binding, saml_request, relay_state SAML flows
device_code, user_code, verification_uri, verification_uri_complete, poll_interval, expires_in, poll_state Device authorization grant
exchange_code, connection_id, redirect_url Immediate grants (client_credentials, assertion grants, token exchange)

Example — authorization code

const start = await client.connect.start({
  provider: "google",
  redirect_uri: "https://friendsconnect.com/auth/callback",
  scopes: ["openid", "email", "profile"],
  state: "csrf-token-from-your-app",
  external_user_id: "user_12345",
});
window.location.href = start.authorization_url!;

Example — client credentials (immediate)

import { isImmediateConnectResponse } from "cliodot";
const immediate = await client.connect.start({
  provider: "m2m",
  redirect_uri: "https://friendsconnect.com/auth/callback",
});
if (isImmediateConnectResponse(immediate)) {
  const exchanged = await client.exchange({
    code: immediate.exchange_code!,
    connection_id: immediate.connection_id!,
  });
}

Example — JWT bearer

await client.connect.start({
  provider: "m2m-api",
  redirect_uri: "https://friendsconnect.com/auth/callback",
  assertion: "eyJhbGciOiJSUzI1NiIs...",
});

client.connect.poll(input)#

Polls device authorization until complete. Maps to POST /oauth/apps/:appId/connect/poll. Retries automatically on 428 / authorization_pending using the interval below.

Input (OAuthConnectPollInput)

Field Required Type Default Description
provider yes string Same provider alias used in connect.start
poll_state yes string poll_state from the device connect response
appId no string client appId Override app ID for this poll
pollIntervalMs no number 5000 Milliseconds to wait between 428 retries
maxAttempts no number 120 Maximum poll attempts before timeout

Returns (OAuthConnectPollResponse)

Field Description
exchange_code Short-lived code to redeem via client.exchange
connection_id Connection ID paired with the exchange code
redirect_url Optional pre-built customer callback URL

Example

import { isDeviceConnectResponse } from "cliodot";
const device = await client.connect.start({
  provider: "cli-tv",
  redirect_uri: "https://friendsconnect.com/auth/callback",
});
if (isDeviceConnectResponse(device)) {
  const done = await client.connect.poll({
    provider: "cli-tv",
    poll_state: device.poll_state!,
    pollIntervalMs: (device.poll_interval ?? 5) * 1000,
  });
  await client.exchange({
    code: done.exchange_code!,
    connection_id: done.connection_id!,
  });
}

client.connect.buildUrl(input)#

Builds a browser redirect URL for GET /oauth/apps/:appId/connect. Accepts the same fields as connect.start except grant-specific body fields (assertion, subject_token, etc.) — those require connect.start (POST).

Field Required Notes
provider yes
redirect_uri yes
scope / scopes no Joined into a single scope query param
state no
external_user_id no
connection_storage no
appId no
const url = client.connect.buildUrl({
  provider: "google",
  redirect_uri: "https://friendsconnect.com/auth/callback",
  scopes: ["openid", "email", "profile"],
  state: "csrf-token",
});
window.location.href = url;

client.connect.isSamlPostBinding(response) / client.connect.buildSamlPostForm(response)#

Helpers for SAML POST binding after connect.start.

Method Returns
isSamlPostBinding(response) true when sso_binding === "post" and SAML fields are present
buildSamlPostForm(response) { action, samlRequest, relayState } or null
const saml = await client.connect.start({
  provider: "google-sso",
  redirect_uri: "https://friendsconnect.com/auth/callback",
});
if (client.connect.isSamlPostBinding(saml)) {
  const form = client.connect.buildSamlPostForm(saml)!;
}

client.exchange(input)#

Redeems an exchange code server-side. Maps to POST /oauth/token/exchange. Requires S2S credentials.

Input (OAuthExchangeInput)

Field Required Type Description
code yes string Exchange code (ex_...) from connect callback or immediate grant
connection_id yes string Connection ID paired with the code

Returns (OAuthExchangeResponse)

Field When included
connection Always — sanitized connection metadata
protocol OAuth2, OIDC, or SAML
oauth Provider token/userinfo/revocation URLs when available
access_token, refresh_token, token_type, expires_at, scope, id_token Per app token_exposure_policy and connection storage
name_id, saml_attributes, assertion_expires_at SAML connections
provider_token_response Raw provider token payload when policy allows
const result = await client.exchange({
  code: "ex_a1b2c3d4e5f6789012345678901234567890ab",
  connection_id: "conn_507f1f77bcf86cd799439014",
});

client.connections.get(connectionId)#

Returns sanitized connection metadata. Maps to GET /oauth/connections/:connectionId.

Parameter Required Type
connectionId yes string

Returns: { ok: true, connection: OAuthSanitizedConnection }

const { connection } = await client.connections.get("conn_507f1f77bcf86cd799439014");

client.connections.getToken(connectionId) / client.connections.sync(connectionId)#

Fetches access token (and related fields per policy). Maps to GET /oauth/connections/:connectionId/token. sync is an alias for getToken.

Parameter Required Type
connectionId yes string

Returns (OAuthTokenResponse)

Field When included
connection Optional sanitized connection
access_token, refresh_token, token_type, expires_at, scope, id_token Per token_exposure_policy
name_id, saml_attributes, assertion_expires_at SAML connections
oauth Provider URLs when available
const token = await client.connections.getToken("conn_507f1f77bcf86cd799439014");
const synced = await client.connections.sync("conn_507f1f77bcf86cd799439014");

client.connections.refresh(connectionId)#

Refreshes tokens using the stored refresh token. Maps to POST /oauth/connections/:connectionId/refresh.

Parameter Required Type
connectionId yes string

Returns (OAuthConnectionWithTokenResponse): connection plus refreshed token fields.

const refreshed = await client.connections.refresh("conn_507f1f77bcf86cd799439014");

client.connections.revoke(connectionId, input?)#

Revokes a connection at the provider when supported. Maps to POST /oauth/connections/:connectionId/revoke.

Parameter Required Type Description
connectionId yes string
input.reason no string Optional audit reason

Returns: { ok: true, revoked: true }

await client.connections.revoke("conn_507f1f77bcf86cd799439014", {
  reason: "user_logged_out",
});

client.connections.delete(connectionId)#

Deletes the connection record. Maps to DELETE /oauth/connections/:connectionId.

Parameter Required Type
connectionId yes string

Returns: { ok: true, deleted: true }

await client.connections.delete("conn_507f1f77bcf86cd799439014");

Response type guards and utilities#

Import from cliodot alongside OAuthAppClient:

Export Purpose
isBrowserAuthorizationResponse(response) true when authorization_url is set (redirect to IdP)
isDeviceConnectResponse(response) true for device-code grant with poll_state and device_code
isImmediateConnectResponse(response) true when exchange_code and connection_id are returned immediately
isDeviceAuthorizationPendingError(err) true for 428 / authorization_pending poll errors
isSamlProtocol(protocol) true when protocol is saml
isOidcProtocol(protocol) true when protocol is oidc
normalizeOAuthGrantType(grantType) Normalizes grant type strings to canonical values
pollDeviceConnectWithBackoff(client, input) Standalone poll helper with the same retry semantics as connect.poll
import {
  OAuthAppClient,
  isImmediateConnectResponse,
  isDeviceConnectResponse,
  pollDeviceConnectWithBackoff,
} from "cliodot";

Error handling#

SDK methods throw CliodotApiError on failure with message, status, and code (when the API returns one).

import { CliodotApiError } from "cliodot";
try {
  await client.exchange({ code: "ex_bad", connection_id: "conn_1" });
} catch (err) {
  if (err instanceof CliodotApiError) {
    console.error(err.code);
  }
}

Common codes: OAUTH_EXCHANGE_CODE_INVALID, OAUTH_UNAUTHORIZED_APP, OAUTH_CONNECTION_REVOKED, OAUTH_CONNECTION_EXPIRED, OAUTH_PROVIDER_ERROR.