Management PAT integration and migration
The complete rc.44 sign-in, grant selection, password proof and one-time PAT issuance flow
Prerequisites
This guide covers management integrations at https://api.beeaicloud.com. Read management authentication first. Creating and managing PATs requires a signed-in session, not an existing PAT. Examples use placeholders or temporary variables and do not create production accounts or tokens.
Bind the exact grant
The top-level scope="access_token.generate" names the create-PAT operation; context.scopes lists resource permissions granted to the PAT. Context must contain exactly scopes and expires_at. Missing context, an empty object, wrong types, empty scopes or extra fields are rejected. Reuse the exact permission set and Unix-second expiry in verification and creation; do not calculate expiry again. Set an appropriate lifetime; a positive expiry must be at least an hour in the future when creating. The create request also has name (1–64 trimmed characters), which does not belong in verification context.
{"method":"password","scope":"access_token.generate","context":{"scopes":["profile:read"],"expires_at":1794679200},"password_encrypted":"<encrypted-password>","encryption_key_id":"<current-kid>"}Complete password example
The Node.js example handles the current public key, v2 password encryption, account identity checks and exact grant binding. It applies only when the methods endpoint allows password. Complete sign-in MFA, CAPTCHA or other required interactions instead of bypassing them. BB_PAT_SCOPES defaults to profile:read only; choose least-privilege scopes from the catalog. The example does not print the PAT: connect the pat variable to your secret store before the process exits.
import { constants, createCipheriv, publicEncrypt, randomBytes } from 'node:crypto';
const base = 'https://api.beeaicloud.com';
const username = process.env.BB_USERNAME;
const password = process.env.BB_PASSWORD;
const expectedUserID = process.env.BB_EXPECTED_USER_ID;
if (!username || !password || !expectedUserID) throw new Error('Missing account configuration');
async function call(path, { method = 'GET', access, proof, body } = {}) {
const response = await fetch(base + path, {
method,
headers: {
...(body ? { 'Content-Type': 'application/json' } : {}),
...(access ? { Authorization: `Bearer ${access}` } : {}),
...(proof ? { 'X-Security-Proof': proof } : {}),
},
...(body ? { body: JSON.stringify(body) } : {}),
redirect: 'error',
});
const result = await response.json();
if (!response.ok || result.success !== true) {
const code = result.code ?? result.data?.code ?? 'REQUEST_REJECTED';
throw new Error(`${method} ${path}: HTTP ${response.status} ${code}`);
}
return result.data;
}
async function passwordFields() {
const key = await call('/api/user/login/encryption-key');
if (!key.enabled) return { password };
const aesKey = randomBytes(32);
const nonce = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', aesKey, nonce);
cipher.setAAD(Buffer.from(`password-v2:${key.kid}`));
const ciphertext = Buffer.concat([
cipher.update(password, 'utf8'), cipher.final(), cipher.getAuthTag(),
]);
const wrappedKey = publicEncrypt({
key: key.public_key, padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256', oaepLabel: Buffer.from('password-v2'),
}, aesKey);
return {
password_encrypted: ['v2', wrappedKey.toString('base64'),
nonce.toString('base64'), ciphertext.toString('base64')].join('.'),
encryption_key_id: key.kid,
};
}
const login = await call('/api/user/login', {
method: 'POST', body: { username, ...await passwordFields() },
});
if (login.require_verification || !login.access_token) {
throw new Error('Complete the required sign-in verification flow first');
}
const access = login.access_token;
const profile = await call('/api/user/self', { access });
if (String(profile.id) !== expectedUserID || profile.username !== username) {
throw new Error('Account identity mismatch');
}
const catalog = await call('/api/user/access_tokens/catalog', { access });
const grantable = new Set(catalog.groups.flatMap(group => group.resources.flatMap(
resource => resource.actions.map(action => `${resource.resource}:${action.action}`),
)));
const grant = {
scopes: JSON.parse(process.env.BB_PAT_SCOPES ?? '["profile:read"]'),
expires_at: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
};
if (!Array.isArray(grant.scopes) || grant.scopes.length === 0 ||
grant.scopes.some(scope => !grantable.has(scope))) {
throw new Error('Select grantable permissions from the catalog');
}
const methods = await call('/api/verify/methods?scope=access_token.generate', { access });
if (!methods.methods.some(method => method.method === 'password' && method.available)) {
throw new Error('Use an available verification method for this account');
}
const verification = await call('/api/verify', {
method: 'POST', access,
body: { method: 'password', scope: 'access_token.generate',
context: grant, ...await passwordFields() },
});
const created = await call('/api/user/access_tokens', {
method: 'POST', access, proof: verification.proof_token,
body: { name: 'business-integration', ...grant },
});
const pat = created.token;
// Store pat in your secret manager here. Never log it or blindly retry creation.
console.log({ id: created.item.id, expires_at: created.item.expires_at });List, update and revoke
GET /api/user/access_tokens never returns plaintext. The create response returns data.token once; store it securely. token_ref and token_hint cannot recover it. PATCH /api/user/access_tokens/{id} requires name; renaming alone needs no proof. With scopes, first verify access_token.update using {"token_id":123,"scopes":["profile:read"]}. DELETE /api/user/access_tokens/{id} requires access_token.revoke with {"token_id":123}. To revoke the legacy PAT, use DELETE /api/user/access_tokens/legacy with {"legacy":true}. Each write consumes its own fresh proof.
Diagnose errors
The following codes come from the rc.44 implementation. HTTP status alone does not establish success, and a caller stage named password_proof does not establish a wrong password.
| Code | Meaning and checks |
|---|---|
SECURITY_CONTEXT_INVALID | HTTP 400: context is missing or violates the strict operation schema. |
SECURITY_VERIFICATION_FAILED | Password, ciphertext or code verification failed; HTTP 200 with success=false is possible. |
SECURITY_VERIFICATION_FLOW_REQUIRED | HTTP 400: the selected method needs its dedicated interactive flow. |
AUTH_SESSION_REQUIRED | HTTP 403: PAT management requires a sign-in session, not a PAT. |
SECURITY_PROOF_CONTEXT_MISMATCH | Proof parameters differ from the create, update or revoke operation. |
SECURITY_PROOF_EXPIRED / SECURITY_PROOF_CONSUMED | The proof expired or was consumed; reconcile the operation before verifying again. |
ACCESS_TOKEN_SCOPE_INVALID / ACCESS_TOKEN_SCOPE_FORBIDDEN | Invalid scope or a permission this account cannot grant. |
ACCESS_TOKEN_EXPIRY_INVALID / ACCESS_TOKEN_LIMIT | Expiry is less than an hour ahead or the account reached its PAT limit. |
Registration versus completed initialization
Tenant initialization commonly follows account creation → sign-in → operation proof → PAT creation → secure storage and integration validation. An existing account or successful sign-in proves only the first stages. Existing legacy PATs may keep working even when new issuance fails. Preserve the account when resuming initialization, check for existing PATs, and resume only a confirmed incomplete step. Reconcile an unknown issuance result before retrying.
How is this guide?