Skip to content

Token 管理 ​

auth.token 命名空间负责 JWT 内存缓存与自动续期。v2 与 v1 最大的差异:

  • 不再自动 refresh:v1 在 submitCode / submitPassword / OIDC 成功路径都会内部调一次 token.refresh;v2 完全交给业务方在 onLogin handler 里主动调
  • 网络抖动不丢 cache:v1 refresh 失败会清掉旧 token;v2 保留旧 cache 后再 rethrow,让业务侧决定降级
  • 默认 fetchToken 走 ory toSession:业务方可自定义覆盖到自家 BFF 网关

5 个 API ​

方法签名说明
auth.token.peek() => string | undefined同步读,不发请求;SSR / hydrate / 已知新鲜场景用;缓存空返回 undefined
auth.token.get() => Promise<string | undefined>HTTP 拦截器主入口;过期前 60s 阈值时自动 refresh;新鲜则直接返回 cache
auth.token.refresh() => Promise<string | undefined>强制拉一次并写 cache;fetchToken 返回 undefined → 清 cache;拿回非法 JWT → 保留旧 cache 返 undefined;网络抛错 → 保留旧 cache 再 rethrow
auth.token.set(jwt: string) => void直接把外部拿到的 token 写入 cache;解 exp;非法 JWT 静默忽略保留旧 cache(不抛错)
auth.token.clear() => void清缓存;logout 自动调,业务通常不必

续期策略 ​

cache 维护两个值:cached: string | undefined 与 exp: number | undefined(JWT exp 字段,秒级 unix timestamp)。

get() 判断是否新鲜:

ts
function isFresh(): boolean {
  if (!cached || exp === undefined) return false;
  // 过期前 60s 视作需续,避免请求发出去时 token 刚好过期
  return Date.now() < (exp - REFRESH_LEEWAY_SECONDS) * 1000;
}

REFRESH_LEEWAY_SECONDS = 60。新鲜则直接返回 cache;不新鲜则调 refresh()。

refresh() 的三种结果:

fetchToken 返回refresh 行为
合法 JWT 字符串jwtDecode 解 exp → 写 cache → 返回 token
undefined(未登录语义)清空 cache 与 exp → 返回 undefined
非法 JWT(无 exp 字段 / decode 抛错)保留旧 cache → 返回 undefined
网络抛错保留旧 cache → rethrow

set(jwt) 的两种结果:

输入set 行为
合法 JWT写 cache + 解 exp
非法 JWT保留旧 cache,不抛错(业务侧塞错 token 时保留可用旧 cache 比清空更安全)

默认 fetchToken ​

业务方不传 fetchToken 时 SDK 走默认实现(createDefaultFetchToken):调 ory toSession({ tokenizeAs: 'default_jwt' }),从 session.tokenized 拿 JWT 字符串。

ts
// 等价的伪代码
async function defaultFetchToken(): Promise<string | undefined> {
  try {
    const session = await api.toSession({ tokenizeAs: 'default_jwt' });
    return session.tokenized;
  } catch (err) {
    // session_inactive / 401 → 视作未登录返回 undefined(不抛错)
    if (err.code === 'session_inactive' || /unauthorized|no active session/i.test(err.message)) {
      return undefined;
    }
    throw err;
  }
}

适用:业务直接吃 ory cookie 的场景(典型:登录后所有 API 都通过 ory cookie 认证 + BFF 透传 JWT)。

自定义 fetchToken ​

业务有自家 BFF 网关(比如要注入团队 ID / 调试 header / 走非 ory 的鉴权链)时传 fetchToken 覆盖:

ts
const auth = new TripoAuth({
  basePath,
  fetchToken: async (fetcher) => {
    // fetcher 是 SDK 自带 credentials/json header 的 FetchAPI
    const res = await fetcher(`${apiUrl}/whoami?tokenizeAs=default_jwt`, {
      method: 'GET',
      headers: { 'x-team-id': currentTeamId.value },
    });
    if (!res.ok) {
      if (res.status === 401) return undefined;   // 未登录
      throw new Error(`whoami failed: ${res.status}`);
    }
    const body = await res.json();
    return body.tokenized;
  },
});

要点:

  • 返回 undefined → SDK 清 cache,视作未登录
  • 抛错 → SDK 保留旧 cache,rethrow 给上层
  • 必须返回合法 JWT(有 exp 字段);不合法时 SDK 保留旧 cache 返回 undefined
  • fetcher 参数自带 credentials: 'include' + JSON header;想要更轻量也可以无视 fetcher 直接用全局 fetch

HTTP 拦截器主入口(get()) ​

每次发请求前调一次 auth.token.get():

ts
import ofetch from 'ofetch';

export const apiClient = ofetch.create({
  baseURL: 'https://api.tripo3d.ai',
  async onRequest({ options }) {
    const token = await auth.token.get();
    if (token) {
      options.headers = new Headers(options.headers);
      options.headers.set('Authorization', `Bearer ${token}`);
    }
  },
});

行为:

  • token 新鲜 → 同步 resolve(实际上是 microtask,因为 get 签名是 Promise)
  • token 临期 → 一次 refresh + 写 cache + resolve
  • 未登录 → resolve 出 undefined;业务侧应当跳登录页

并发请求场景:N 个请求同时 get(),SDK 内置 in-flight 合并——同一时刻只发起 1 次 fetchToken,所有并发调用共享同一份 Promise 结果。业务侧无需在拦截器外再手工去重。

成功 / 抛错都在 finally 清掉 in-flight 标记;下一波并发 get() 仍可正常发起新的 refresh。

同步判断「是否已登录」(peek()) ​

peek() 不发请求,直接读 cache:

ts
const isLoggedIn = !!auth.token.peek();

if (!isLoggedIn) router.push('/login');

适用:路由 guard / SSR hydrate / 同步 UI 状态切换。

注意:peek() 只反映「cache 里有没有 token」。如果业务想要「确认 ory session 仍然活跃」,必须调 refresh()——peek 不知道 cookie 是否已过期、是否被业务清掉。

团队切换示例 ​

业务方切换团队时通常想拿一份「新 team 视角」的 JWT;可以走 refresh()(如果 fetchToken 已经感知 team)或 set(newJwt)(如果业务侧自己已经拿到新 token):

ts
// 模式 A:fetchToken 自动感知 team(如 header x-team-id)
async function switchTeam(teamId: string) {
  currentTeamId.value = teamId;
  await auth.token.refresh();   // 触发 fetchToken;新 team 的 JWT 写入 cache
}

// 模式 B:业务侧拿到新 JWT 后直接注入
async function switchTeam(teamId: string) {
  const res = await api.post('/teams/switch', { teamId });
  const newToken: string = res.tokenized;
  auth.token.set(newToken);    // 同步写 cache,无需再 refresh
}

SSR 场景 ​

服务端实例化 TripoAuth 不会有 token:

  • peek() 返回 undefined
  • get() 会触发 fetchToken 调 ory;服务端通常没有 cookie 上下文,会拿到 undefined
  • 即便要做 SSR 透传,建议走 set(jwt) 注入:
ts
// Nuxt server middleware:从请求 cookie 拿 ory session → 调 ory toSession 拿 JWT
export default defineEventHandler(async (event) => {
  const cookie = getRequestHeader(event, 'cookie');
  if (!cookie) return;
  // 走 ory 拿 JWT(独立于 SDK,避免共享 fetcher 上的状态)
  const jwt = await fetchSessionToken({ basePath, fetchApi: makeForwardingFetch(cookie) });
  if (jwt) event.context.tripoToken = jwt;
});

// 客户端 hydrate
const ssrToken = window.__NUXT__.tripoToken;
if (ssrToken) auth.token.set(ssrToken);

fetchSessionToken 是 core 顶层导出的工具(不是 auth.token.*):

ts
import { fetchSessionToken } from '@tripo3d/auth';

const jwt = await fetchSessionToken({ basePath, fetchApi: customFetch });
// jwt: string | undefined(未登录时 undefined,不抛错)

它内部构造一个临时 FrontendApi 调 toSession({ tokenizeAs: 'default_jwt' });与业务的 TripoAuth 实例完全独立,不共享状态——适合 SSR 一次性取 JWT 的场景。

与 v1 的差异速查 ​

行为v1v2
登录成功后自动续 token✓ SDK 内部自动✗ 业务在 onLogin handler 里主动调
refresh 网络抖动清旧 cache(业务侧 token 抖没)保留旧 cache 后 rethrow
set 非法 JWT抛错静默忽略,保留旧 cache
构造参数 token: string支持 SSR 直接注入已删;统一走 token.set(jwt) 注入
fetchSessionToken不存在顶层工具,独立于 TripoAuth 实例

错误处理 ​

auth.token.* 各方法的失败行为:

  • peek() — 不抛错;缓存空返回 undefined
  • get() — 新鲜路径不抛错;refresh 路径透传 fetchToken 抛的错(含 TripoAuthError(name: 'FLOW' | 'NETWORK'));保留旧 cache
  • refresh() — 同 get() 的 refresh 路径;保留旧 cache
  • set(jwt) — 不抛错;非法 JWT 静默忽略
  • clear() — 不抛错

业务通常在拦截器层兜:

ts
try {
  const token = await auth.token.get();
  // ...
} catch (err) {
  // 网络异常路径:保留旧 cache,业务可继续用 peek() 拿到的旧 token 跑请求
  const fallback = auth.token.peek();
  if (fallback) return fallback;
  // 真的没 token 了:跳登录
  router.push('/login');
}

下一步 ​

基于 MIT 协议发布(内部使用)