Step 2 — Set up encryption
Every request payload is encrypted using JWE (JSON Web Encryption) with AES-256-GCM. Your CLIENT_SECRET never travels in requests — it is used locally to derive the encryption key.
Only the server, which holds the same salt, can decrypt the payload.
Install
npm install joseEncryption helper
// src/lib/apiClient.js
import { EncryptJWT } from 'jose'
import { createHmac } from 'crypto'
const CLIENT_ID = process.env.CLIENT_ID
const CLIENT_SECRET = process.env.CLIENT_SECRET
const CLIENT_SRVC = process.env.CLIENT_SRVC
const BASE_URL = process.env.API_BASE_URL // https://your-api.com
const SALT = process.env.KEY_DERIVATION_SALT
function deriveKey(secret) {
return createHmac('sha256', SALT)
.update(secret)
.digest() // returns 32-byte Buffer
}
export async function apiCall(endpoint, data) {
const key = deriveKey(CLIENT_SECRET)
const token = await new EncryptJWT({ data, srvc: CLIENT_SRVC })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuedAt()
.setExpirationTime('5m')
.setJti(crypto.randomUUID())
.encrypt(key)
const res = await fetch(`${BASE_URL}/api/v01/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-client-id': CLIENT_ID,
},
body: JSON.stringify({
data: token,
srvc: CLIENT_SRVC,
}),
})
const body = await res.json()
if (!body.ok) {
const err = new Error(body.memo ?? 'API error')
err.status = res.status
err.txId = res.headers.get('X-Transaction-Id')
throw err
}
return body.data
}Token expiry
Tokens expire after 5 minutes. Generate a new token for each request — do not reuse tokens. Each token includes a unique JTI (jti) to prevent replay attacks.