Skip to content

架构总览 ​

@tripo3d/auth v2 把 Ory Kratos 浏览器 flow 与 Tripo 自家的业务规约(identifier 自动分流、跨子域登录信号、独立登录页 / dialog 登录、JWT token 缓存)统一收敛成一个 TripoAuth 实例。所有业务能力按命名空间挂在实例下:业务方调用形态恒定为 auth.{ns}.{verb}(),子路径 @tripo3d/auth/ui 另外提供 Vue 组件 + 命令式 showLogin()。

ts
class TripoAuth {
  readonly login:    LoginApi      // sendCode / submitCode / submitPassword / startOidc
  readonly signal:   SignalApi     // onLogin/onLogout / emitLogin/emitLogout / publishLogin/consumeSignal / readSignal/readSignalRecord/writeSignal/clearSignal
  readonly oidc:     OidcApi       // handleComplete / loadEdgeFlow / promptGoogleOneTap / cancelOneTap / submitIdToken
  readonly reset:    ResetApi      // 未登录恢复:sendCode / verifyCode / submitNewPassword
  readonly settings: SettingsApi   // 已登录改密码:sendVerifyCode / verifyCode / updatePassword
  readonly token:    TokenApi      // peek / set / clear / get / refresh

  logout(): Promise<void>          // 失效 ory session + clearSignal + emitLogout(内部 token.clear)
}

一图概览 ​

  • TripoAuth门面类,按 basePath 维度创建;持有 Ory FrontendApi、SignalApi、各命名空间
    • loginsendCode / submitCode / submitPassword / startOidc:业务侧主入口(ory flow 动作)
    • signalonLogin(handler) / onLogout(handler) + emit / readSignal / writeSignal / clearSignal:同根域登录待办竞争消费 + 退出订阅
    • oidchandleComplete(独立完成页 /login/oidc)/ loadEdgeFlow(中间页 continue / merge)/ promptGoogleOneTap(Google 一键登录)/ cancelOneTap(关闭常驻卡片)/ submitIdToken(id_token 直传)
    • resetsendCode / verifyCode / submitNewPassword:三段式密码重置(未登录场景,verifyCode 提权后再改密)
    • settingssendVerifyCode / verifyCode / updatePassword:已登录用户改密码(step-up via email code,session 保持)
    • tokenpeek / set / clear / get / refresh:JWT 内存缓存 + 过期前 60s 自动续
    • logout()顶层方法:调 ory logout → clearSignal → emitLogout(内部 token.clear,触发业务 onLogout)

核心设计 ​

1. 按 basePath 维度创建实例 ​

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

const auth = new TripoAuth({
  basePath: 'https://auth.tripo3d.ai',    // Ory public URL 或同源反代入口
  onError: (err) => { /* 可选:监控 SDK 非阻塞运营错误 */ },
});

要点:

  • basePath 用于装配 Ory FrontendApi,所有 ory flow 都走它
  • OIDC 完成页 URL 不再由 SDK 拼接,而是业务方在 startOidc / showLogin / 页面组件里通过 completeUrl 显式传入的绝对 URL
  • onError 是可选的非阻塞错误观察回调(详见 错误处理)
  • basePath 缺失会立刻抛 TripoAuthError(name: 'INTERNAL')
  • 一个 TripoAuth 实例对应一个 auth 域;切环境时整体重建实例,不要复用

2. 验证码自动分流(identifier-first) ​

auth.login.sendCode({ email }) 内部先调业务端 /idp-ext/auth/check-identifier,按返回的 flow_type 决定:

  • 已注册 → 创建 ory login flow + 发码
  • 未注册 → 创建 ory registration flow + 发码

auth.login.submitCode({ code, email }) 复用 sendCode 缓存的 flow id + csrf token 提交。同一个 TripoAuth 实例的两次调用必须串行匹配,邮箱变更后需要重新 sendCode(详见 验证码流程)。

v2 完全删除了 SDK 内建的 BroadcastChannel / postMessage 协议。登录成功后 SDK 写一份业务方可订阅的 cookie,跨标签共享、跨子域送达,但同根域只保证由一个实例认领,并非每个 tab 都收到:

cookie 名:    __tripo_login_signal__
Domain:       .tripo3d.ai      (所有 tripo3d 子域可见)
SameSite:     Lax              (OAuth 回跳带得上)
Path:         /
Max-Age:      60s
payload:      base64url(JSON({ v: 1, provider, ts, params? }))

业务侧统一接口:

ts
const unsubscribe = auth.signal.onLogin(async ({ signal, context }) => {
  // SDK 在派发前已 await token.refresh() 兜底;业务侧 token.get() 直接拿得到
  await initUser();
  if (context.isRedirect) {
    // 业务页冷启路径(独立页 / OIDC redirect 跳回来的)
  }
});

// 退出登录订阅:auth.logout() 内部派发,handler 跑起来时 token 已 clear
auth.signal.onLogout(() => clearLocalUserStore());

SignalApi 走两条通道:

  1. 构造时立刻消费一次 cookie 进入 pendingSignals Map:覆盖「OIDC 回跳页 cookie 已写好,但业务还没注册 onLogin」的竞态窗口;新注册的 onLogin handler 都能补偿派发收到(context.isRedirect=true)
  2. UI 轮询通道:LoginDialog / LoginPage 的 useConsumeSignal / useReadSignal composable 在 onMounted 起 setInterval 探 cookie,卸载时清 timer;polling 不在 SDK 里

业务方只关心 onLogin(handler) / onLogout(handler) 两个 API;详见 登录信号。

4. OIDC 4 模式矩阵(替代 v1 的 popup-only 三页模型) ​

auth.login.startOidc(provider, { completeUrl, locale?, returnTo?, params?, mode? }) 的 mode 由 SDK 内部按 isMobileUA 推导(mobile → redirect,pc → popup),与"PC dialog / PC standalone / Mobile dialog / Mobile standalone"4 种容器组合:

端UI 容器OIDC mode推荐用法
PCdialogpopup业务嵌入
PCstandalonepopup独立登录页
Mobiledialogredirect移动端 popup 受限,整页跳
Mobilestandaloneredirect同上

startOidc 不再返回 done Promise——成功语义统一通过 cookie 信号 + onLogin 接收:

ts
const { url, mode } = await auth.login.startOidc('google', {
  completeUrl: 'https://www.tripo3d.ai/login/oidc',  // 必填,绝对 URL
  returnTo: 'https://studio.tripo3d.ai/projects',
});
if (mode === 'popup') {
  window.open(url, '_blank', 'popup=yes,width=500,height=600');
} else {
  window.location.assign(url);
}
// 业务方在别处注册的 onLogin(handler) 会在 cookie 信号到达后被触发

完成页路由由业务方传入的 completeUrl 决定(如 https://www.tripo3d.ai/login/oidc),由 OidcCompletePage 挂载 auth.oidc.handleComplete();中间页路由(如 /login/oidc/edge?flow=...)由 ory 服务端配置决定,由 OidcEdgePage 调 auth.oidc.loadEdgeFlow(flowId, ctx)(详见 OIDC 登录)。

5. UI 子路径 /ui 与 showLogin ​

UI 组件 + 命令式入口独立挂在 @tripo3d/auth/ui 子路径下,与 core 完全解耦:

ts
import { showLogin } from '@tripo3d/auth/ui';

// showLogin 返回 void —— 登录完成统一靠 onLogin handler 收尾
showLogin(auth, {
  completeUrl: 'https://www.tripo3d.ai/login/oidc',  // 必填:OIDC 完成页绝对 URL
  locale: useI18n().locale.value,                    // 可选:宿主当前语言;缺省回落 'en'
  params: { fromBanner: 'home_v2' },
});
  • showLogin 内部依赖 @tripo3d/design 的 dialog.show,业务方需要在根上挂 <DialogOpener />
  • showLogin 不返回任何东西;登录完成的唯一消费路径是业务方注册的 auth.signal.onLogin(handler)
  • 独立登录页直接挂 <LoginPage :auth="auth" :complete-url="completeUrl" />(无需 provide/inject 概念)(详见 UI 子路径 /ui)

6. Token 默认 vs 自定义 fetchToken ​

auth.token 维护 JWT 内存缓存 + 过期检测;五个 API:

方法语义
peek()同步读,不发请求;SSR / hydrate / 已知新鲜场景用
get()过期前 60s 阈值自动续;HTTP 拦截器主入口
refresh()强制拉一次并写 cache
set(jwt)直接写入(SSR 注入 / 团队切换)
clear()清缓存(logout 自动调,业务通常不必)

不传 fetchToken 时回落默认实现:调 ory toSession({ tokenizeAs: 'default_jwt' })。业务有自家 BFF 网关时覆盖:

ts
const auth = new TripoAuth({
  basePath,
  fetchToken: async (fetcher) => {
    const res = await fetcher(`${apiUrl}/whoami?tokenizeAs=default_jwt`, { method: 'GET' });
    return res.ok ? (await res.json()).tokenized : undefined;
  },
});

错误约定(与 v1 的差异):refresh 网络抖动时保留旧 cache 再 rethrow,不像 v1 把瞬时网络错误误判成 logout(详见 Token 管理)。

通信职责拆分 ​

v2 把跨窗 / 跨标签 / 跨子域的登录待办收敛到一种机制——同 site cookie 信号:

[OIDC popup 完成页, www.tripo3d.ai]              [业务子域, studio.tripo3d.ai]
auth.oidc.handleComplete()
  → 写 __tripo_login_signal__ cookie
  → window.close()
                                                onLogin handler 触发
                                                business code 调 token.refresh / initUser

跨 origin 业务集成(例如 studio 嵌入 www 登录页)同样靠 cookie 共享 Domain .tripo3d.ai,业务方不再需要写 postMessage 协议:

  • iframe 子页面调 submitCode / submitPassword / handleComplete 时 SDK 自动写 cookie
  • iframe 父窗的 auth.signal.onLogin(handler) 在轮询到 cookie 时被触发
  • 双方都通过同一份 SDK,签名一致

cookie 是跨 tab 的尽力待办而非广播:某个实例派发或认领后,其他 tab 不保证触发 onLogin;cookie 的 read + clear 也无法给跨子域竞争提供严格原子 claim。已派发的 One Tap 使用 v2 cookie,旧 SDK 会忽略它以避免混合版本双报;其他 tab 通过后续 session/token 请求自然感知。需要所有 tab 立即刷新或严格全局一次时,应由宿主提供服务端 claim/权威 relay。

一句话接入 ​

ts
import { TripoAuth } from '@tripo3d/auth';
import { showLogin } from '@tripo3d/auth/ui';

const auth = new TripoAuth({ basePath });

auth.signal.onLogin(async () => {
  // SDK 已在派发前 await token.refresh() 兜底;这里直接拉用户即可
  await fetchCurrentUser();
});
auth.signal.onLogout(() => clearLocalUserStore());

showLogin(auth, { completeUrl: 'https://www.tripo3d.ai/login/oidc' });

下一步 ​

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