Skip to content

Authentication

Copy page

Each package authenticates a little differently because each runs in a different trust context. This page covers the client and server SDKs; the MCP SDK uses an Auth0 PKCE flow documented separately in MCP Authentication.

The server SDK runs on your trusted backend and authenticates with a secret API key (or an OAuth access token). The apiKey is required unless you pass accessToken.

import { DivinciServer } from "@divinci-ai/server";
const divinci = new DivinciServer({
apiKey: process.env.DIVINCI_API_KEY, // never hardcode
});
// Verify the key is valid and active
const ok = await divinci.validateApiKey();

Client SDK — three ways to identify the caller

Section titled “Client SDK — three ways to identify the caller”

The client SDK always needs a releaseId. Beyond that, choose one identity mechanism depending on how much you trust the runtime.

Pass a short-lived JWT your backend issues for the signed-in user. Internally the client wraps it in a SimpleTokenStorage.

import { DivinciClient } from "@divinci-ai/client";
const client = new DivinciClient({
releaseId: "rel_abc123",
userToken: jwtFromYourBackend, // obtained from your own auth flow
});

You can also set or replace the token after construction — useful when the token is fetched asynchronously or rotated:

await client.setToken(newJwt, expiresAtUnixMs);
await client.setRefreshToken(refreshJwt); // optional
await client.logout(); // clears stored tokens

Headless external-user login (no embed script)

Section titled “Headless external-user login (no embed script)”

If you’re driving @divinci-ai/client directly (no embed-script.js) and want to identify an external user against a workspace API key, exchange credentials in two hops — this is the same flow the embed script performs internally, documented here for headless/backend-proxied integrations:

1. Backend calls POST /embed/login with the workspace API key and the caller’s identity. This step must run server-side — it takes your secret API key.

// Request
{
"apikey": "divinci_key_...",
"userId": "user_123",
"membershipTier": "free",
"username": "Ada Lovelace"
}
// Response
{
"refreshToken": "eyJhbGciOi...", // long-lived (7 days) — NOT a valid Authorization value
"refreshExpiresAt": "2026-07-08T00:00:00.000Z",
"tierAssigned": "free" // resolved server-side; ignore any tier the caller requested
}

2. Backend (or a browser-direct client) calls POST /embed/validate-login with the refresh token plus the calling origin and release, and gets back a short-lived access token:

// Request
{
"refreshToken": "eyJhbGciOi...",
"origin": "https://your-app.example.com",
"releaseId": "rel_abc123"
}
// Response
{
"valid": true,
"accessToken": "eyJhbGciOi...", // 30-minute lifetime — this is the userToken
"userInfo": { "user_id": "...", "nickname": "Ada Lovelace", "picture": null },
"tierConfig": { "tier": "free", "limits": { /* ... */ }, "usage": { /* ... */ }, "remaining": { /* ... */ } }
}

3. Pass the accessToken as userToken to the client SDK:

const client = new DivinciClient({
releaseId: "rel_abc123",
userToken: accessTokenFromValidateLogin,
});

/embed/validate-login deliberately returns a uniform 200 {"valid": false} for every failure (no oracle for attackers), so check these in order — they are the real causes, most common first:

  1. Release ↔ API key whitelabel mismatch. The releaseId must belong to the same whitelabel as the API key that minted the refresh token. A stale release id from an old workspace fails this way even though the token itself is perfectly valid. Verify with GET /white-label-release/{releaseId} — its target names the whitelabel the release belongs to.
  2. Origin not allowlisted. allowedOrigins is a property of the API key (not the release — you won’t find it on the release object), set at key creation/update in the workspace admin. The origin you send must match an entry exactly (scheme included, case-sensitive). Local dev origins like http://localhost:5173 must be added explicitly.
  3. Missing fields. refreshToken, origin, and releaseId are all required — omitting any of them fails.
  4. Token/key state. The refresh token is expired (7 days), the API key was deactivated, or its sessions were revoked.

Also note: the membershipTier passed to /embed/login must match one of the whitelabel’s configured embed-tier slugs (“Invalid tier” 400s list the availableSlugs). Tiers are defined per whitelabel by the workspace admin — if your membership levels aren’t in the list, ask the workspace owner to add matching tier slugs.

2. External user (API key + caller identity)

Section titled “2. External user (API key + caller identity)”

For embedded or partner integrations, authenticate with an API key and attach the end user’s identity so usage and quotas attribute to the right person.

const client = new DivinciClient({
releaseId: "rel_abc123",
apiKey: "divinci_key_...",
externalUser: {
userId: "user_123", // required: your system's user id
email: "user@example.com",
name: "Ada Lovelace",
membershipTier: "premium",
metadata: { company: "Acme Inc" },
},
});
// Or update the identity later
client.setExternalUser({ userId: "user_456" });

For full control over where tokens live and how they refresh, implement the TokenStorage interface and pass it as tokenStorage:

import { DivinciClient, type TokenStorage } from "@divinci-ai/client";
const storage: TokenStorage = {
async getToken() {
return localStorage.getItem("divinci_token");
},
async setToken(token, expiresAt) {
localStorage.setItem("divinci_token", token);
},
async clearToken() {
localStorage.removeItem("divinci_token");
},
async getRefreshToken() {
return localStorage.getItem("divinci_refresh");
},
async setRefreshToken(token) {
localStorage.setItem("divinci_refresh", token);
},
};
const client = new DivinciClient({ releaseId: "rel_abc123", tokenStorage: storage });
const signedIn = await client.isAuthenticated();
const keyValid = await client.validateApiKey();
  1. Never commit secrets — load API keys from environment variables.
  2. Keep server keys server-side — browsers get user tokens, not API keys.
  3. Use separate keys per environment — dev, staging, and production.
  4. Scope keys — issue workspace-scoped keys with divinci.apiKeys.create() (see API Keys).
  5. Rotate regularlydivinci.apiKeys.rotate() swaps a key without downtime.
  6. Prefer short-lived user tokens — they limit blast radius if leaked.