@privageapp/privage3-addon-sdk
v1.0.0
Published
Shared client SDK for Privage add-ons — session bootstrap, persistence, and OAuth callback coordination.
Readme
@privageapp/privage3-addon-sdk
Client SDK for building Privage add-ons — iframe apps embedded in the Privage CRM platform.
It handles the three things every add-on needs:
- 🔑 Session bootstrap — parses
exchange_token,business_code, andreturn_urlfrom the URLmanagement-siteinjects on mount, then exchanges them for an access / refresh token pair. - 💾 Persistence — caches the session (
localStorage,cookie, ormemory) so your add-on survives iframe reloads and the single-use exchange token. - 🔁 OAuth callback coordination — one code path works for both wrapped (
{return_url}/callback) and direct ({addon_origin}/callback) redirect-URI shapes, with cross-tab andpostMessagedelivery handled for you.
Zero runtime dependencies. Framework-agnostic — works with Next.js, Vite, or plain JS.
Install
npm install @privageapp/privage3-addon-sdk
# or
yarn add @privageapp/privage3-addon-sdk
# or
pnpm add @privageapp/privage3-addon-sdkRequires a browser environment (uses window, localStorage, document.cookie, postMessage).
Public API
import {
init,
refresh,
clear,
is_initialized,
get_access_token,
get_business_code,
get_refresh_token,
get_callback_url,
set_callback_data,
get_callback_data,
on_callback_data,
complete_callback,
MissingCredentialsError,
NotInitializedError,
} from '@privageapp/privage3-addon-sdk'Usage
1. Bootstrap the session
Call init() once when the iframe mounts (typically in a layout or top-level provider):
'use client'
import { useEffect, useState } from 'react'
import { init } from '@privageapp/privage3-addon-sdk'
export default function AddonProvider({ children }) {
const [ready, setReady] = useState(false)
useEffect(() => {
init({ apiUrl: process.env.NEXT_PUBLIC_PLATFORM_API_URL! })
.then(() => setReady(true))
.catch(console.error)
}, [])
return ready ? children : <Loader />
}2. Call your backend with the session
import { get_access_token, get_business_code, refresh } from '@privageapp/privage3-addon-sdk'
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const go = () => fetch(path, {
...init,
headers: {
...(init?.headers ?? {}),
Authorization: `Bearer ${get_access_token()}`,
'X-Business-Code': get_business_code() ?? '',
},
})
let res = await go()
if (res.status === 401) {
await refresh()
res = await go()
}
if (!res.ok) throw new Error(await res.text())
return res.json()
}3. Start an OAuth flow
get_callback_url() returns the URL to hand the provider as redirect_uri.
import { get_callback_url, on_callback_data } from '@privageapp/privage3-addon-sdk'
export function ConnectTikTokButton() {
useEffect(() => on_callback_data((data) => {
if (data.provider === 'tiktok') mutate('/api/tiktok/shops')
}), [])
const start = async () => {
const redirectUri = get_callback_url() // wrapped by default
// const redirectUri = get_callback_url({ mode: 'direct' }) // for fixed-URI providers
const { data } = await axios.post('/api/tiktok/authorize', { redirect_uri: redirectUri })
window.open(data.url, '_blank')
}
return <Button onClick={start}>Connect TikTok</Button>
}4. Handle the OAuth callback
The callback page is the same code whether the provider redirected to the wrapped or the direct URL — the SDK figures out the right delivery channel:
'use client'
import { useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
import { init, complete_callback } from '@privageapp/privage3-addon-sdk'
export default function CallbackPage() {
const params = useSearchParams()
useEffect(() => {
init({ apiUrl: process.env.NEXT_PUBLIC_PLATFORM_API_URL! }).then(async () => {
const res = await axios.post('/api/tiktok/callback', {
code: params.get('code'),
state: params.get('state'),
})
complete_callback({ data: { provider: 'tiktok', ...res.data } })
})
}, [])
return <Loader />
}complete_callback does the right thing for each shape:
| Context | Behaviour |
|---------|-----------|
| Inside the main iframe (wrapped) | set_callback_data + window.parent.postMessage({ type: 'privage_addon_callback', id, data }) |
| Standalone tab (direct) | set_callback_data (localStorage cross-tab event delivers to the main iframe) + window.close() |
The main iframe's on_callback_data listener receives each callback exactly once, dedup'd by envelope id.
init() decision tree
- Parse
exchange_token,business_code,return_urlfrom the URL. - Read any cached session from storage.
- Resolve:
- Cache hit, same
business_code→ reuse, no network call.return_urlis refreshed from the URL if present. - Cache hit, different
business_code→ clear storage, exchange fresh. - No URL params but cached session exists → reuse cache (supports in-app reloads).
- URL params present, no usable cache →
POST /api/v1/auth-connect/token, persist, return. - Neither → throw
MissingCredentialsError.
- Cache hit, same
This is what lets the add-on survive even when the parent's exchange token has already been consumed (single-use, 60 s TTL).
Storage modes
init({ apiUrl, storage: 'localStorage' }) // default
init({ apiUrl, storage: 'cookie', cookie: { sameSite: 'None', secure: true } })
init({ apiUrl, storage: 'memory' }) // not persisted; equivalent to the pre-SDK behaviourTwo keys are used: privage_addon_session and privage_addon_callback. Override via sessionKey / callbackKey in InitOptions.
Security notes
- Persisting access tokens in
localStorage/ cookies trades a larger XSS blast radius for surviving iframe remounts and the single-use exchange token (60 s TTL). Keep your add-on's UI sanitisation tight. localStorageis per-origin, so one add-on's tokens are not visible tomanagement-siteor to other add-ons served from different origins.- If you cannot accept persisted tokens, use
storage: 'memory'— session lives only for the iframe's lifetime, no persistence across remounts. - The SDK uses
window.postMessage(..., '*')to notify the parent iframe of OAuth callbacks. Parent origins vary per business, so a specifictargetOriginisn't assumed. The main iframe receives callbacks viaon_callback_dataand can dedupe or validate the payload shape before acting.
Configuration reference
type InitOptions = {
apiUrl: string // e.g. 'https://api-dev.privagecdn.com'
storage?: 'localStorage' | 'cookie' | 'memory' // default: 'localStorage'
sessionKey?: string // default: 'privage_addon_session'
callbackKey?: string // default: 'privage_addon_callback'
cookie?: {
domain?: string
sameSite?: 'Lax' | 'Strict' | 'None' // default: 'None'
secure?: boolean // default: true when sameSite is 'None'
maxAgeSec?: number // default: 7_776_000 (90 days — matches refresh TTL)
}
searchParams?: URLSearchParams // override the default (new URLSearchParams(location.search))
}Backend contract (for reference)
The SDK calls the Privage core backend directly (CORS is open — no proxy route required):
| Method | Path | Body | Response |
|--------|------|------|----------|
| POST | {apiUrl}/api/v1/auth-connect/token | { exchange_token, business_code } | { access_token, refresh_token } |
| POST | {apiUrl}/api/v1/auth-connect/refresh | { refresh_token } | { access_token, refresh_token } |
Full Open API reference: api-docs.privageapp.com
License
See LICENSE or the license field in package.json. Published by Privage.
