蜂桥AI蜂桥AI
API 参考
管理接口管理 PAT 接入与旧版迁移
⚠️合规提示:本项目仅用于合法授权的 API 网关、内部管理和私有化部署场景。请遵守上游服务条款、平台规则、监管要求和内容安全要求。

管理 PAT 接入与旧版迁移

完整演示 rc.44 的登录、权限选择、密码操作证明及一次性 PAT 签发

接入前提

本指南面向管理接口集成,使用 https://api.beeaicloud.com。先阅读管理 API 鉴权。创建与管理 PAT 必须持有登录会话,不接受已有 PAT 代替。所有示例均为占位或临时变量,未在生产代建账号或 Token。

验证上下文必须绑定最终授权

顶层 scope="access_token.generate" 表示“创建 PAT”操作;context.scopes 是拟授予 PAT 的资源权限,两者不同。context 必须恰好包含 scopes 与 expires_at;缺失、空对象、错误类型、空权限或额外字段会被拒绝。验证与创建须复用同一组权限和同一个 Unix 秒过期时间,不能在第二步重新计算。建议设置合理到期时间;正值至少在创建时晚于当前时间一小时。创建请求另有 name(去空白后 1–64 个字符),它不属于验证 context。

{"method":"password","scope":"access_token.generate","context":{"scopes":["profile:read"],"expires_at":1794679200},"password_encrypted":"<encrypted-password>","encryption_key_id":"<current-kid>"}

密码方式完整示例

下面是 Node.js 示例,覆盖当前公钥、v2 密码加密、账号身份核对和相同 grant 绑定。仅适用于验证方式查询明确允许 password 的账号;遇到登录二次验证、验证码或其他方法时应完成相应交互,不绕过。BB_PAT_SCOPES 默认只有 profile:read,请依据 catalog 配置实际业务所需的最小权限。示例不打印 PAT;在进程结束前将 pat 接入你的秘密存储。

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 });

读取、改权与撤销

列表 GET /api/user/access_tokens 不返回明文。创建响应的 data.token 只返回一次,应保密存储,不能从 token_ref 或 token_hint 恢复。PATCH /api/user/access_tokens/{id} 的 name 必填;只改名称不需证明,提交 scopes 则先为 access_token.update 验证 {"token_id":123,"scopes":["profile:read"]}。撤销使用 DELETE /api/user/access_tokens/{id},先为 access_token.revoke 验证 {"token_id":123}。撤销旧 PAT 使用 DELETE /api/user/access_tokens/legacy,context 为 {"legacy":true}。每项写操作各用一次新证明。

常见错误定位

下面的错误码来自 rc.44 实现。不要只用 HTTP 状态判断成功,也不要把 password_proof 的调用阶段名直接解释成密码错误。

错误码含义与检查项
SECURITY_CONTEXT_INVALIDHTTP 400;context 缺失或不符合目标操作的严格结构。
SECURITY_VERIFICATION_FAILED密码、密文或验证代码校验失败;可能是 HTTP 200 + success=false。
SECURITY_VERIFICATION_FLOW_REQUIREDHTTP 400;所选方法必须使用专用交互验证流程。
AUTH_SESSION_REQUIREDHTTP 403;PAT 管理要求登录会话,不能使用 PAT 代替。
SECURITY_PROOF_CONTEXT_MISMATCH证明与创建/改权/撤销请求的具体参数不一致。
SECURITY_PROOF_EXPIRED / SECURITY_PROOF_CONSUMED证明已过期或已被消费;核对操作结果后重新验证。
ACCESS_TOKEN_SCOPE_INVALID / ACCESS_TOKEN_SCOPE_FORBIDDEN权限名无效或当前账号不可授予该权限。
ACCESS_TOKEN_EXPIRY_INVALID / ACCESS_TOKEN_LIMIT到期时间不足一小时,或已达到当前用户的 PAT 数量上限。

注册成功不等于初始化完成

企业开户通常分为“创建账号 → 登录 → 获取操作证明 → 创建 PAT → 保存并验证集成配置”。账号存在或已能登录,只说明前两步完成。已有旧 PAT 的企业可继续访问,不代表新企业能用旧请求完成签发。恢复失败初始化时保留已有账号,先核对是否已存在 PAT,再从已确认未完成的步骤续接;未知签发结果必须先对账。

这篇文档对您有帮助吗?

管理 PAT 接入与旧版迁移 | 蜂桥AI