架构总览
@tripo3d/auth v2 把 Ory Kratos 浏览器 flow 与 Tripo 自家的业务规约(identifier 自动分流、跨子域登录信号、独立登录页 / dialog 登录、JWT token 缓存)统一收敛成一个 TripoAuth 实例。所有业务能力按命名空间挂在实例下:业务方调用形态恒定为 auth.{ns}.{verb}(),子路径 @tripo3d/auth/ui 另外提供 Vue 组件 + 命令式 showLogin()。
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 维度创建实例
import { TripoAuth } from '@tripo3d/auth';
const auth = new TripoAuth({
basePath: 'https://auth.tripo3d.ai', // Ory public URL 或同源反代入口
onError: (err) => { /* 可选:监控 SDK 非阻塞运营错误 */ },
});要点:
basePath用于装配 OryFrontendApi,所有 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
loginflow + 发码 - 未注册 → 创建 ory
registrationflow + 发码
auth.login.submitCode({ code, email }) 复用 sendCode 缓存的 flow id + csrf token 提交。同一个 TripoAuth 实例的两次调用必须串行匹配,邮箱变更后需要重新 sendCode(详见 验证码流程)。
3. 登录信号 cookie + onLogin(替代 v1 的 BroadcastChannel)
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? }))业务侧统一接口:
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 走两条通道:
- 构造时立刻消费一次 cookie 进入 pendingSignals Map:覆盖「OIDC 回跳页 cookie 已写好,但业务还没注册 onLogin」的竞态窗口;新注册的 onLogin handler 都能补偿派发收到(
context.isRedirect=true) - UI 轮询通道:
LoginDialog/LoginPage的useConsumeSignal/useReadSignalcomposable 在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 | 推荐用法 |
|---|---|---|---|
| PC | dialog | popup | 业务嵌入 |
| PC | standalone | popup | 独立登录页 |
| Mobile | dialog | redirect | 移动端 popup 受限,整页跳 |
| Mobile | standalone | redirect | 同上 |
startOidc 不再返回 done Promise——成功语义统一通过 cookie 信号 + onLogin 接收:
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 完全解耦:
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 网关时覆盖:
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。
一句话接入
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' });