Skip to content

5 分钟接入 ​

本页给出 @tripo3d/auth v2 的最小可跑接入路径:从安装到「业务请求拦截器读 token」5 步走,整张表单 + OIDC 全走 showLogin 一键唤起。读完即可上手;细节继续看 架构总览 与各分章。

0. 前置依赖 ​

1. 安装 ​

bash
pnpm add @tripo3d/auth

1.5 宿主构建集成(必做) ​

@tripo3d/auth/ui 组件不自带 CSS,所有样式都是 UnoCSS 原子类,靠宿主 Vite + UnoCSS pipeline 在 dev/build 时扫描 node_modules/@tripo3d/auth/dist/**/*.uno.js 生成。

而 Vite 默认会用 esbuild 预打包 node_modules 依赖(optimizeDeps),auth 一旦被预打包就绕过 UnoCSS transform → 原子类生成不出来 → dev 模式登录 UI 全无样式。

Nuxt 使用 @tripo3d/auth/nuxt 模块:它会自动挂载 tripoAuth();Google GSI 默认由 SDK 在实际有 googleClientId 的登录容器按需加载,避免每个 SSR 页面都请求第三方脚本。只有首屏确定需要 One Tap/GSI 的站点才应显式开启 SSR 预热。纯 Vite 使用 @tripo3d/auth/vite 插件。两者都会把 auth 排除出预打包(走 transform → 被 UnoCSS 扫到),同时把 yup CJS 依赖链强制预打包。

ts
export default defineNuxtConfig({
  modules: ['@tripo3d/auth/nuxt'],
  // GSI 默认在 SSR head 预热;确定用不到 Google 登录时置 false 关闭。
  // Vite 集成不受此开关影响。
  // tripoAuth: { preloadGoogleGsi: false },
});
ts
import { defineConfig } from 'vite';
import { tripoAuth } from '@tripo3d/auth/vite';

export default defineConfig({
  plugins: [tripoAuth()],
});

不挂这个插件的两个典型症状:

  1. dev 模式登录页 / 弹窗无样式(核心症状,根因:auth 被预打包后原子类没扫到)
  2. 若自己手写 optimizeDeps.exclude: ['@tripo3d/auth'],会报 tiny-case does not provide an export named 'camelCase' —— 这是 yup CJS 依赖被一起带出预打包的副作用,正是 tripoAuth() 内部处理的事情

宿主不要再自己手写 optimizeDeps.exclude / include,全部交给插件。

2. 实例化 TripoAuth ​

业务侧把 TripoAuth 当成单例 —— 在 plugin / composable 里建一次,整个 SPA 复用。showLogin 第一参直接传实例;独立登录页用 :auth prop 显式传。无需 provide/inject。

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

// 整个 SPA 生命周期复用同一个 auth 实例
export const auth = new TripoAuth({
  basePath: 'https://auth.tripo3d.ai',
  // 可选:设备指纹,构造时传一次即自动随所有登录/注册/重置/改密请求上报(详见 §2.5)
  deviceId: getDeviceId(),
});
ts
import { TripoAuth } from '@tripo3d/auth';

export default defineNuxtPlugin(() => {
  // 服务端跳过实例化,避免 SSR 期间触发浏览器 API
  if (import.meta.server) return;

  const auth = new TripoAuth({
    basePath: 'https://auth.tripo3d.ai',
    deviceId: getDeviceId(), // 可选:设备指纹,见 §2.5
  });

  return { provide: { auth } };
});

2.5 透传设备指纹 / 业务临时数据(transient_payload) ​

需要把 设备指纹 device_id 或风控等业务临时数据带给 Ory 后端 / webhook(用于设备绑定、风控判定)时,SDK 把它们统一注入 Ory 标准字段 transient_payload——仅本次提交对后端 / webhook 可见、不落库到 identity traits,正适合这类临时数据。

两个来源、两个时机:

来源时机形态适用
deviceId构造期(new TripoAuth)string设备级静态值,传一次后全局自动带上
options.transientPayload调用期(每个动作方法)Record<string, unknown>某次调用按需附加的额外字段

二者在每次请求前合并为 transient_payload = { ...transientPayload, device_id: deviceId }(deviceId 优先),两者都不传则请求体不含该字段(向后兼容)。

覆盖范围:登录 / 注册 / 重置 / 改密链路的全部底层提交——login.sendCode / submitCode / submitPassword / startOidc、reset.sendCode / verifyCode / submitNewPassword、settings.sendVerifyCode / verifyCode / updatePassword。

ts
// 构造期:device_id 全局自动带
const auth = new TripoAuth({ basePath, deviceId: getDeviceId() });

// 调用期:某次提交再附加业务字段(与 device_id 合并上报)
await auth.login.submitPassword(
  { email, password },
  { transientPayload: { invite_code: 'X8K2', scene: 'home_banner' } },
);
// 实际提交的 transient_payload = { invite_code, scene, device_id }

验证:浏览器 DevTools Network 里看 updateLoginFlow 等请求体的 transient_payload 字段;或在 Playground 顶部改 deviceId、在「验证码登录」卡里填 transientPayload JSON,对照请求体观察合并结果。

3. 注册 onLogin handler ​

登录成功后 SDK 写一份 __tripo_login_signal__ cookie,业务侧用 onLogin(handler) 订阅;handler 拿到 { signal, context } payload 后通常做:拉新用户信息(SDK 在派发前已 await token.refresh() 兜底)、按 context.isRedirect 区分冷启路径:

ts
import type { LoginPayload } from '@tripo3d/auth';

const unsubscribe = auth.signal.onLogin(async ({ signal, context }: LoginPayload) => {
  // SDK 已在派发前确保 token 就绪;保留 refresh 作为容错(内部并发去重,不会重复打后端)
  await auth.token.refresh();
  await fetchCurrentUser();
  console.log('logged in via', signal.provider, 'isRedirect=', context.isRedirect, signal.params);
});

// 组件卸载时调 unsubscribe();app 级订阅可以不卸

handler 不需要担心被同一个信号触发多次——SDK 在 cookie 命中后立即调用 clearSignal,保证「一次信号一次处理」。

4. showLogin 唤起登录 ​

业务任意位置一键弹出登录对话框,邮箱/密码 + OIDC 全在同一个 dialog 里完成:

vue
<script setup lang="ts">
import { showLogin } from '@tripo3d/auth/ui';
import { auth } from '~/plugins/auth';

function onClickLogin() {
  // showLogin 返回 void —— 登录完成统一靠 onLogin handler 收尾,dialog 内部错误自己 toast
  showLogin(auth, {
    completeUrl: 'https://www.tripo3d.ai/login/oidc',  // 必填:OIDC 完成页绝对 URL
    locale: useI18n().locale.value,                    // 可选:宿主当前语言;缺省回落 'en'
    params: { fromBanner: 'home_v2' },
  });
}
</script>

<template>
  <button @click="onClickLogin">Sign in</button>
</template>

showLogin 不返回任何东西——无论 dialog 自己关闭、用户取消,还是 redirect 整页跳走,登录完成的唯一消费路径都是业务方注册的 auth.signal.onLogin(handler)。务必先注册 handler 再唤起登录。

5. 业务请求拦截器读 token ​

每次发请求前调 auth.token.get(),过期前 60s SDK 自动续期,业务无感知:

ts
import ofetch from 'ofetch';
import { auth } from '~/plugins/auth';

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

同步判断「是否已登录」(不发请求)用 auth.token.peek():

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

端到端最小示例 ​

把上面 5 步串起来,得到一个端到端最小可跑示例:

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

const auth = new TripoAuth({
  basePath: 'https://auth.tripo3d.ai',
});

// 1. 全局 onLogin handler,处理所有登录路径的收尾
// SDK 已在派发前 await token.refresh() 兜底,handler 跑起来 token 直接可用
auth.signal.onLogin(async ({ signal, context }) => {
  await fetchCurrentUser();
});
// 配套:订阅退出事件(由 auth.logout() 派发,token 已 clear)
auth.signal.onLogout(() => clearLocalUserStore());

// 2. 唤起登录(showLogin 返回 void,业务收尾全部交给上面的 onLogin handler)
function login() {
  showLogin(auth, { completeUrl: 'https://www.tripo3d.ai/login/oidc' });
}

// 3. 退出登录
async function logout() {
  await auth.logout();   // 内部清 signal cookie + token cache
  router.push('/');
}

// 4. 全局请求 token
async function authHeader() {
  const token = await auth.token.get();
  return token ? { Authorization: `Bearer ${token}` } : {};
}

下一步 ​

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