원클릭으로
viverse-auth
VIVERSE Login SDK integration for user authentication and SSO
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
VIVERSE Login SDK integration for user authentication and SSO
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Three.js Polygon Streaming .xrg integration and debugging playbook for @polygon-streaming/web-player-threejs, including correct wrapper events, asset publishing, model fitting, and fallback replacement policy
Bundle entry for the exported VIVERSE PlayCanvas Toolkit workflow pack. Use when you want bundled VIVERSE PlayCanvas Toolkit skills, prompts, catalogs, and helper assets in one standalone package.
Collision detection patterns for Three.js games without physics engines
Protect third-party API keys by moving secret-bearing calls into VIVERSE Play Lambda scripts and keeping only non-secret user data in VIVERSE Storage.
Build or fix CPU racing drivers for Three.js or VIVERSE racing games. Use for waypoint loops, tile-to-route reconstruction, lap completion, recovery logic, and low-overhead debug workflows.
Integrate VIVERSE Token Gateway for AI chat in published apps — auth header construction, non-streaming and SSE streaming patterns, rate-limit handling, and error recovery.
| name | viverse-auth |
| description | VIVERSE Login SDK integration for user authentication and SSO |
| prerequisites | ["VIVERSE SDK script tag","VIVERSE Studio App ID"] |
| tags | ["viverse","authentication","login","sso"] |
Add VIVERSE user authentication to any web project. Supports SSO across VIVERSE experiences.
Use this when a project needs:
index.html:<script src="https://www.viverse.com/static-assets/viverse-sdk/index.umd.cjs"></script>
VITE_VIVERSE_CLIENT_ID is set and matches target appaccount_id as display nameThese are release blockers for any auth integration task:
client.checkAuth().account.htcvive.com for new vSdk.client(...).https://sdk-api.viverse.com/.accessToken, token, and authorization when constructing new vSdk.avatar(...).accesstoken (lowercase); it is blocked in production CORS preflight.avatarClient.getProfile() -> client.getUserInfo() -> client.getUser() -> client.getProfileByToken(token) -> direct API fallback.
Continue to the next strategy whenever profile is missing or missing required identity/avatar fields.account_id (full or partial) as username in UI fallback.VERSION_NAME constant logged on startup.useRef); do not re-run full initialize()->checkAuth() due to hook dependency churn.setSdk state before calling avatar.getProfile()).vSdk.bridge.isReady === false, wait 500ms before profile fallback chain.VIVERSE Player/Player-*) must not overwrite a previously resolved specific name.ViverseService implementations in different folders.VITE_VIVERSE_CLIENT_ID when valid, and in Worlds iframe fallback to hostname-derived app id (<appId>-preview.world.viverse.app -> <appId>).const vSdk = window.viverse || window.VIVERSE_SDK || window.vSdk;
const client = new vSdk.client({
clientId: 'YOUR_APP_ID', // From VIVERSE Studio
domain: 'account.htcvive.com' // MANDATORY: Do not use viverse.com for the auth domain
});
[!IMPORTANT] The SDK may expose itself as
window.vSdk,window.viverse, orwindow.VIVERSE_SDKdepending on the version. Always check all candidates.
To avoid "openUrl" destructuring crashes during initialization, you must wait for the VIVERSE message bridge:
const detectSdk = () => {
const vSdk = window.viverse || window.VIVERSE_SDK || window.vSdk;
const bridgeReady = vSdk && (vSdk.bridge ? vSdk.bridge.isReady !== false : true);
if (vSdk?.client && bridgeReady) {
// Proceed to initialize client
} else {
requestAnimationFrame(detectSdk);
}
};
const initOnceRef = useRef(false);
useEffect(() => {
if (initOnceRef.current) return;
initOnceRef.current = true;
initialize(); // one-shot
}, []);
If this guard is missing, repeated checkAuth() cycles can race and leave profile stuck at generic fallback.
const result = await client.checkAuth();
if (result) {
// User is already logged in — but this only gives auth tokens!
console.log('Token:', result.access_token);
console.log('Account ID:', result.account_id);
console.log('Expires in:', result.expires_in, 'seconds');
} else {
// No active session
}
[!CAUTION]
checkAuth()does NOT return user profile data (display name, avatar). It only returnsaccess_token,account_id, andexpires_in. See step 2b below.
checkAuth() gives token/account identity, then profile must be recovered with a deterministic fallback chain.
const vSdk = window.viverse || window.VIVERSE_SDK || window.vSdk;
const appId = import.meta.env.VITE_VIVERSE_CLIENT_ID;
const auth = await client.checkAuth();
if (!auth?.access_token) return null;
let mergedProfile = null;
const token = auth.access_token;
const accountId = auth.account_id;
const merge = (p) => {
if (!p || typeof p !== 'object') return;
mergedProfile = mergedProfile ? { ...mergedProfile, ...p } : { ...p };
};
// 1) Avatar SDK primary path (tank-aligned)
if (vSdk?.avatar) {
try {
const avatarClient = new vSdk.avatar({
baseURL: 'https://sdk-api.viverse.com/',
accessToken: token,
token,
authorization: token,
appId,
clientId: appId,
});
merge(await avatarClient.getProfile());
} catch (_) {}
}
const hasIdentity = (p) =>
!!(p && (p.name || p.displayName || p.display_name || p.nickName || p.nickname || p.userName || p.email));
const hasAvatar = (p) =>
!!(p && (p.activeAvatar?.avatarUrl || p.avatarUrl || p.avatar_url || p.profilePicUrl));
const needsMoreProfile = (p) => !p || !hasIdentity(p) || !hasAvatar(p);
// 2) Bridge-safe fallback
if (needsMoreProfile(mergedProfile) && client?.getUserInfo) {
try { merge(await client.getUserInfo()); } catch (_) {}
}
// 3) Legacy fallback
if (needsMoreProfile(mergedProfile) && client?.getUser) {
try { merge(await client.getUser()); } catch (_) {}
}
// 4) Token-based fallback
if (needsMoreProfile(mergedProfile) && client?.getProfileByToken) {
try { merge(await client.getProfileByToken(token)); } catch (_) {}
}
// 5) Optional direct API fallback (environment-dependent, may be blocked by CORS)
if (needsMoreProfile(mergedProfile)) {
try {
const resp = await fetch('https://account-profile.htcvive.com/SS/Profiles/v3/Me', {
headers: { Authorization: `Bearer ${token}` },
});
if (resp.ok) merge(await resp.json());
} catch (_) {}
}
const displayName =
mergedProfile?.displayName ||
mergedProfile?.display_name ||
mergedProfile?.name ||
mergedProfile?.nickname ||
mergedProfile?.userName ||
mergedProfile?.email ||
'VIVERSE Player';
const avatarUrl =
mergedProfile?.activeAvatar?.headIconUrl ||
mergedProfile?.activeAvatar?.head_icon_url ||
mergedProfile?.headIconUrl ||
mergedProfile?.head_icon_url ||
mergedProfile?.avatarUrl ||
mergedProfile?.avatar_url ||
null;
setUser({
accountId,
accessToken: token,
displayName,
avatarUrl,
});
[!TIP] Use the reusable helper in patterns/robust-profile-fetch.md so every project applies the same fallback order.
client.loginWithWorlds({ state: 'optional-custom-value' });
// This redirects the page to VIVERSE login
// After login, user is redirected back with a session
await client.logout();
// Then re-check auth state in app logic.
// In many React apps, avoid forced reload and refresh state instead.
The SDK loads asynchronously. Poll for it:
async function waitForSDK(maxAttempts = 50, interval = 100) {
return new Promise((resolve) => {
let attempts = 0;
const check = () => {
attempts++;
const vSdk = window.viverse || window.VIVERSE_SDK;
if (vSdk?.client) {
resolve(vSdk);
} else if (attempts > maxAttempts) {
console.warn('VIVERSE SDK failed to load');
resolve(null);
} else {
setTimeout(check, interval);
}
};
check();
});
}
Use this structure to avoid the common auth bugs:
App (or a single AuthProvider).user/loading/error/login/logout down to UI components (AuthGate, Lobby) via props/context.ViverseService), not inside multiple UI components.avatar.getProfile -> getUserInfo -> getUser -> getProfileByToken -> API fallback).Minimal component wiring:
function App() {
const { user, loading, error, login, logout } = useViverseAuth();
return (
<AuthGate user={user} loading={loading} error={error} login={login}>
<Lobby user={user} onLogout={logout} />
</AuthGate>
);
}
[!IMPORTANT] Do not call
useViverseAuth()separately in bothAppandAuthGate/Lobby. That creates desynced states.
name, displayName, display_name, userName, email) for UI.account_id as an internal identifier only.VIVERSE Player only (never append account fragments).headIconUrl/activeAvatar.headIconUrl when available.checkAuth() returns access_token and account_idVIVERSE Player ab12cd)VIVERSE Player when identity is unavailableexpires_in is in seconds. Refresh the session before it expires for long-running experiences.client constructor — the namespace itself has methods directly. Handle both cases.checkAuth() only returns auth tokens, NOT user profile data. Use the full fallback chain, not checkAuth() fields alone.clientId causes silent checkAuth timeout: Passing an empty string to new sdk.client({ clientId: '', ... }) does NOT throw — it silently times out with "Timeout waiting for parent message". This happens whenever VIVERSE_APP_ID is unset (e.g. no ?appId= param, no localStorage entry, no hostname fallback). Always verify the App ID is resolved before constructing the client. For Worlds iframe apps the hostname regex ([a-z0-9]+)(?:-preview)?\.world\.viverse\.app reliably extracts the App ID without any query param:
const _hostnameAppId = (() => {
const m = location.hostname.match(/^([a-z0-9]+)(?:-preview)?\.world\.viverse\.app$/);
return m ? m[1] : null;
})();
const appId = new URLSearchParams(location.search).get('appId')
|| localStorage.getItem('my_app_id')
|| _hostnameAppId
|| '';
// Guard before constructing client:
if (!appId) { console.warn('[Auth] App ID not resolved — skipping checkAuth'); return null; }
_initViverseAuth() eagerly on page load, not lazily on first user action: If auth is only triggered when the user first clicks an AI/social feature, there is a noticeable cold-start delay (1200ms handshake + checkAuth + profile fetch). Call auth bootstrap proactively on DOMContentLoaded or during app init — results are cached and reused. Only skip eager init for dev environments that use a direct bearer token instead of SSO:
// main.js / app entry point
if (GATEWAY_ENV !== 'dev') {
initViverseAuth().then(auth => {
if (auth) showToast(`✓ 已登入 · ${auth.displayName}`);
}).catch(() => {});
}
checkAuth:ack): If the application hangs on VIVERSE Studio or logs unhandled methods: VIVERSE_SDK/checkAuth:ack, it is almost always caused by an App ID mismatch. The VIVERSE parent iframe security model prevents the auth handshake if clientId (from your .env file) does not exactly match the App ID the iframe was launched with. Double check copied .env files..env still has VITE_VIVERSE_CLIENT_ID=YOUR_APP_ID, auth may silently fall back to guest mode in preview even when publish uses correct --app-id. Build/runtime must resolve a real app id (env or iframe hostname fallback).getProfile() returns null (often due to App ID mismatch or invalid token) and the code tries to access profile.accountId without a check. Safety Fix: Always use optional chaining profile?.accountId or a null-guard if (profile).import.meta.env.VITE_* is compiled at build time. If App ID changes, update .env and run a fresh npm run build before publishing. Re-publishing an old dist keeps the old/invalid App ID and causes guest-mode auth.unauthorized origin console noise: Logs like Received message from unauthorized origin: https://www.viverse.com commonly indicate parent-iframe auth security rejecting the handshake (usually App ID mismatch, or redirect URI/origin not registered in Studio). Verify App ID + Studio auth settings together.checkAuth() returns null, do NOT silently fall back to guest mode without logging a clear warning about VITE_VIVERSE_CLIENT_ID mismatch.checkAuth might need a few milliseconds after SDK load to establish the iframe message bridge. If checkAuth fails immediately, try one retry after 500ms.initialize() does NOT complete auth — you must call checkAuth() explicitly: initialize() only sets up the SDK client and returns true/false (ready). Auth state remains isAuthenticated: false until checkAuth() is separately awaited. The correct sequence is always: const ready = await initialize(); if (ready) await checkAuth();. Stopping after initialize() leaves the login button showing even when the user is already logged in.checkAuth() race with initialize-auth postMessage: The VIVERSE parent frame sends an initialize-auth postMessage to the iframe after the handshake. On first call checkAuth() may return null if this message hasn't arrived yet. Retry 2–3 times with 1s delay before concluding the user is unauthenticated: for (let i = 0; i < 3; i++) { auth = await client.checkAuth(); if (auth) break; await delay(1000); }.hidden rule must exist for every toggled auth UI element: classList.add('hidden') and classList.remove('hidden') silently do nothing if no CSS rule defines display: none for that class on that element. A generic .hidden { display: none; } in base styles is safer, but if you use ID-scoped styles (e.g. #profile-chip { ... }), you must explicitly add #login-btn.hidden { display: none; } for each auth-toggled element. Missing this rule causes the login button to remain visible even after successful auth.window.viverse in a VIVERSE Worlds iframe exposes: Avatar, Client (constructor), DEFAULT_SCOPE, GameDashboard, Play, Storage, TokenType, User, avatar (constructor), client (constructor, typeof === 'function'), default, gameDashboard, getGuestLeaderboardRanking, getLeaderboardRanking, getSessionToken, getUserAchievement, play, postLeaderboardRecord, postUserAchievement, storage. Use typeof sdk.client === 'function' to confirm a constructor is available before calling new sdk.client(...).useViverseAuth() in multiple top-level components independently (for example in both App and AuthGate). Keep one source of truth in App, then pass user/loading/error/login/logout down via props/context. This prevents "logout button looks fake" and "user info not updating after account switch" behavior caused by desynced local hook state.checkAuth() again, but switching to another account may still require explicit SSO sign-out at platform level.account_id directly or partially as display name. Prefer profile name/email, then generic VIVERSE Player.