tcm-oauth
Use when integrating the @crimsoncorp/oauth-react SDK (The Crimson Market / Portal.Service OAuth) into a React or Next.js app. Covers OAuth client registration, callback pages, server-side token exchange, session adapters, logout routes, popup vs redirect interaction modes, scopes (profile, email, subscriptions, listings, roles, gaming, relics, youtube, external_club), and external-club partner provisioning + webhook enrichment.
Works with
---
name: tcm-oauth
description: Use when integrating the @crimsoncorp/oauth-react SDK (The Crimson Market / Portal.Service OAuth) into a React or Next.js app. Covers OAuth client registration, callback pages, server-side token exchange, session adapters, logout routes, popup vs redirect interaction modes, scopes (profile, email, subscriptions, listings, roles, gaming, relics, youtube, external_club), and external-club partner provisioning + webhook enrichment.
license: MIT
---
# tcm-oauth
Use this skill when adding, fixing, or migrating authentication in an
application that consumes `@crimsoncorp/oauth-react` against the Portal.Service
OAuth backend (a.k.a. TCM / The Crimson Market / Mana).
The supported topology is server-backed:
```txt
Browser -> @crimsoncorp/oauth-react -> App's server exchange route -> Portal.Service /oauth/token
```
Portal.Service still requires `client_secret` for token exchange, so a
fully browser-only integration is **not** supported. The app must own a server
route that can keep `TCM_OAUTH_CLIENT_SECRET` private.
Do not invent custom popup/redirect flows or call `/oauth/token` directly from
the browser. If the SDK does not expose a needed capability, call that out
rather than duplicating exchange or session logic in the app.
## Install
```bash
npm install @crimsoncorp/oauth-react
```
Optional bundled styles:
```ts
import "@crimsoncorp/oauth-react/styles.css";
```
For production apps, pin to an exact version.
## Prerequisites
Before writing any integration code:
1. A **callback route** on the app origin. Recommended path: `/auth/tcm/callback`.
2. A **server exchange endpoint**. Default route-backed path: `/api/auth/tcm/oauth-exchange`.
3. A client registered in the Developers portal with **exact** callback URLs
and a server-kept client secret.
4. A server-backed app runtime (Next.js route handler, Express, etc.) that can
hold `client_secret` privately.
Register clients and redirect URIs in OAuth App Setup. URI matching is exact,
so local, staging, and production callbacks must each be registered.
## Environment
OAuth endpoints are relative to the Mana API base URL.
| Environment | Web URL | API URL |
| ----------- | ---------------------------------------- | --------------------------------------------- |
| Development | `https://dev.portal.raum.au` | `https://dev.portal.raum.au/mana` |
| Production | `https://www.thecrimsonmarket.com` | `https://www.thecrimsonmarket.com/mana` |
```env
NEXT_PUBLIC_TCM_OAUTH_CLIENT_ID=tcm_xxx
NEXT_PUBLIC_TCM_OAUTH_WEB_URL=https://www.thecrimsonmarket.com
TCM_OAUTH_API_URL=https://www.thecrimsonmarket.com/mana
TCM_OAUTH_CLIENT_ID=tcm_xxx
TCM_OAUTH_CLIENT_SECRET=your-secret
TCM_OAUTH_REDIRECT_URI=https://your-app.example.com/auth/tcm/callback
```
`TCM_OAUTH_CLIENT_SECRET` is server-only. It must never appear in
`NEXT_PUBLIC_*`, client components, React props, browser bundles, logs, error
responses, screenshots, or test snapshots. If a client secret is rotated,
update the server route immediately before testing logins.
For `external_club` clients, the same secret is also used to sign optional
webhook enrichment callbacks.
## Choosing an Integration Surface
| Use case | Surface |
| ----------------------------------------------------- | ----------------------------------------------------------------------- |
| Standard server-backed React or Next.js app | `useTcmOAuth` + `TcmOAuthCallbackPage` + `createTcmOAuthExchangeRoute` |
| Preserving an older popup-only flow | `useTcmOAuthPopupRoute` (compatibility; default `/auth/tcm/popup-callback`) |
| Custom exchange handling / non-Next.js servers | `useTcmOAuthPopup`, `createTcmOAuthClient`, `createTcmOAuthRouteClient` |
The first row is the recommended path for new integrations. The lower-level
APIs exist for migration and edge cases — prefer the high-level path unless
the user has a specific reason to drop down.
## Recommended React Hook: `useTcmOAuth`
`useTcmOAuth` lets the SDK pick popup vs redirect automatically and falls back
to redirect if popup opening is blocked.
```tsx
import { useTcmOAuth } from "@crimsoncorp/oauth-react";
export function GoogleLogin() {
const oauth = useTcmOAuth<{ userId: string }>({
clientId: process.env.NEXT_PUBLIC_TCM_OAUTH_CLIENT_ID!,
tcmWebUrl: process.env.NEXT_PUBLIC_TCM_OAUTH_WEB_URL!,
exchangeEndpoint: "/api/auth/tcm/oauth-exchange",
callbackPath: "/auth/tcm/callback",
scope: "profile email",
interactionMode: "auto",
onSuccess: ({ userId }) => {
console.log("Logged in", userId);
},
onError: (error) => {
console.error(error.code, error.message);
},
});
return (
<button onClick={() => void oauth.startLogin("google")} disabled={oauth.authenticating}>
{oauth.authenticating ? "Please wait..." : "Sign in with Google"}
</button>
);
}
```
### Hook return shape
- `authenticating` — boolean; true while a flow is in progress.
- `phase` — current step of the flow.
- `error` — last error, or null.
- `resolvedInteractionMode` — `"popup" | "redirect"` once the SDK has decided.
- `startLogin(provider?)` — kicks off the flow.
- `clearError()` — resets the error state.
### Interaction modes
```ts
interactionMode: "auto" | "popup" | "redirect"
```
- `"auto"` (recommended): popup on desktop-like environments, redirect on
mobile-like environments, with automatic redirect fallback if the popup is
blocked.
- Disable popup-blocked fallback with `fallbackToRedirect: false`.
- Specify a post-login landing route for redirect flows with `returnTo: "/account"`.
### Diagnostics
Route-backed exchange requests send `x-tcm-flow-id` and `x-tcm-message-id`
correlation headers in development and staging by default. Override with:
```ts
diagnostics: "always" | "never"
```
### Callback page
Render `TcmOAuthCallbackPage` on the app origin at the registered callback
path:
```tsx
import { TcmOAuthCallbackPage } from "@crimsoncorp/oauth-react";
export default function Page() {
return <TcmOAuthCallbackPage />;
}
```
Recommended path: `/auth/tcm/callback`. The callback handler stores the
result and resumes at the initiating route via `returnTo` when redirect mode
is used.
## Next.js App Router (Recommended)
This is the production-recommended integration for `@crimsoncorp/oauth-react`.
### 1. Callback page
```tsx
// app/auth/tcm/callback/page.tsx
import { TcmOAuthCallbackPage } from "@crimsoncorp/oauth-react";
export default function Page() {
return <TcmOAuthCallbackPage />;
}
```
### 2. Exchange route + session adapter
```ts
// app/api/auth/tcm/oauth-exchange/route.ts
import {
createTcmCookieSessionAdapter,
resolveTcmAuthSession,
} from "@crimsoncorp/oauth-react/server";
import {
createTcmLogoutRoute,
createTcmOAuthExchangeRoute,
} from "@crimsoncorp/oauth-react/nextjs";
const sessionAdapter = createTcmCookieSessionAdapter({
appId: "my-app",
maxAgeSeconds: 60 * 60 * 24,
});
const route = createTcmOAuthExchangeRoute({
oauth: {
apiBaseUrl: process.env.TCM_OAUTH_API_URL!,
clientId: process.env.TCM_OAUTH_CLIENT_ID!,
clientSecret: process.env.TCM_OAUTH_CLIENT_SECRET!,
callbackPath: "/auth/tcm/callback",
redirectUri: process.env.TCM_OAUTH_REDIRECT_URI,
googleOnly: true,
},
async onResolvedUser({ userInfo, traceId }) {
if (!userInfo.googleId) {
return {
status: 400,
body: { message: "Missing googleId", traceId },
};
}
const user = await upsertUserFromTcm(userInfo);
return {
body: { userId: user.id, email: user.email },
session: { id: user.id },
};
},
applySession(response, session) {
sessionAdapter.apply(response, session.id);
},
});
export const { POST } = route;
```
`createTcmOAuthExchangeRoute` owns:
- payload validation
- redirect URI resolution and retry
- token exchange
- userinfo fetch
- correlation header forwarding
- duplicate-request single-flight handling
The app still owns:
- user creation or lookup (`onResolvedUser`)
- session payload signing/verification
- response body shape
### 3. Runtime auth resolution
Mixed-mode apps should resolve auth per request, not at build time.
```ts
function readStandaloneSession(request: Request) {
const cookieValue = sessionAdapter.read(request);
return cookieValue ? { sub: cookieValue } : null;
}
function readParentAuthToken(request: Request) {
const hasParentCookie = request.headers.get("cookie")?.includes("authToken=");
return hasParentCookie ? { sub: "host-user-id" } : null;
}
export function getSessionFromRequest(request: Request) {
return resolveTcmAuthSession(request, {
sources: [
{ name: "sdk_session", resolve: readStandaloneSession },
{ name: "parent_auth_token", resolve: readParentAuthToken },
],
precedence: ["sdk_session", "parent_auth_token"],
});
}
```
Guidance:
- **Standalone apps**: only `sdk_session`.
- **Embedded shared-domain apps**: keep host-owned `authToken`.
- **Mixed-mode apps**: resolve `sdk_session` first, fall back to `parent_auth_token`.
### 4. Logout route
```ts
const logoutRoute = createTcmLogoutRoute({
resolveSession: getSessionFromRequest,
standaloneSessionAdapter: sessionAdapter,
onSharedCookieLogout() {
return Response.json({
success: true,
authSource: "parent_auth_token",
delegated: true,
});
},
});
export const { GET, POST } = logoutRoute;
```
Behavior:
- `sdk_session` clears the SDK-managed standalone cookie.
- `parent_auth_token` delegates to host/platform logout and does **not** clear
the standalone cookie.
### 5. Browser hook
```tsx
import { useTcmOAuth } from "@crimsoncorp/oauth-react";
export function LoginButton() {
const oauth = useTcmOAuth<{ userId: string }>({
clientId: process.env.NEXT_PUBLIC_TCM_OAUTH_CLIENT_ID!,
tcmWebUrl: process.env.NEXT_PUBLIC_TCM_OAUTH_WEB_URL!,
exchangeEndpoint: "/api/auth/tcm/oauth-exchange",
callbackPath: "/auth/tcm/callback",
interactionMode: "auto",
googleOnly: true,
});
return (
<button onClick={() => void oauth.startLogin()} disabled={oauth.authenticating}>
{oauth.authenticating ? "Signing in..." : "Continue with Google"}
</button>
);
}
```
## Popup Contract (Google-only flows)
For Google-only popup flows, the SDK standardizes the authorize URL down to:
- OAuth protocol params
- `ui_mode=popup`
- `required_provider=google`
UI-specific popup params (`provider`, `auto_start_provider`, `popup_variant`)
are no longer part of the standard SDK contract. `service.core-ui` is expected
to:
- show its own minimal spinner shell for popup login
- silently preflight `/oauth/authorize`
- redirect the popup window itself to Google when the backend returns 401 or
`provider_link_required`
- show consent only when the backend returns a consent payload
## Registering OAuth Applications
Workflow when setting up a new client:
1. Create the client in **Create Application** in the developer portal.
2. Register the **exact** callback URI the app will use, including protocol,
hostname, and path. The recommended path is `/auth/tcm/callback`.
3. Keep the generated client secret on the server only.
4. Select the minimum scopes the integration needs (always include `profile`).
5. Match the configured callback route with the SDK callback component and
exchange route.
Manage existing clients from **My Applications** in the portal.
## Scopes
Request only the scopes the integration needs. `profile` is required for
every client.
| Scope | Returns |
| ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `profile` (req.) | `userName`, `_id`, `firstName`, `lastName`, `displayName`, `avatarUrl`, `googleId`, `youtubeChannelIds`, `primaryYouTubeChannelId` |
| `email` | `email` |
| `subscriptions` | `club`, `customerId` |
| `listings` | `listings` |
| `roles` | `roles` |
| `gaming` | `battlenetId`, `discordId`, `steamId`, `twitchId` |
| `relics` | `relics` |
| `youtube` | `youtubeChannelIds`, `primaryYouTubeChannelId` |
| `external_club` | Enables partner provisioning + webhook enrichment (no fixed userinfo fields by itself). |
The route-backed flow uses the explicit scope requested by the app, or the
single allowed client scope when only one scope is configured.
## External Club Integration
When a client has the `external_club` scope, partners can provision or link
users via:
```
POST /:uiName/external-club/user
```
Base URL should not include the tenant path; `uiName` is part of the route.
```env
EXTERNAL_CLUB_BASE_URL=https://www.thecrimsonmarket.com
EXTERNAL_CLUB_UI_NAME=mana
```
Resulting endpoint: `https://www.thecrimsonmarket.com/mana/external-club/user`.
### Required fields
`userName`, `email`, `clubName`, `clubUserId`, `hubClubId`, `clientId`,
`clientSecret`, `encryptedPassword`.
`encryptedPassword` **must** be a bcrypt hash. Portal.Service rejects
non-bcrypt values.
### Optional fields
`webhookEventEndpoint`.
### Example payload
```json
{
"userName": "charades_diablo4traders",
"email": "socials@diablo4traders.com",
"encryptedPassword": "$2b$10$.................................................",
"clubName": "charadescollective",
"clubUserId": "113769124091223507890",
"hubClubId": "11735",
"clientId": "tcm_xxx",
"clientSecret": "your-oauth-client-secret",
"webhookEventEndpoint": "https://pokecharades.example.com/api/v1/public/external-club/user-context"
}
```
### Validation and behavior
- OAuth client credentials are validated before user provisioning starts.
- The client must include `external_club` in `allowedScopes`.
- Existing users are matched by partner mapping (`clubName` + `clubUserId`)
or by normalized email.
- Portal.Service persists external-club metadata, the OAuth client id, and
the stored password hash on the linked user.
- The response includes the resolved `hubUserId`, `hubClubId`, `oauthClientId`,
and the stored password hash.
### Webhook enrichment on `/oauth/userinfo`
When a linked external-club user later authenticates with OAuth, Portal.Service
can append partner-owned data under `externalClubContext` in
`/oauth/userinfo`. The enrichment call is optional — if no
`webhookEventEndpoint` is stored or the callback fails, normal OAuth login
still succeeds.
The enrichment callback is signed with the OAuth client secret stored for the
linked user:
```
POST https://pokecharades.example.com/api/v1/public/external-club/user-context
x-external-club-client-id: tcm_xxx
x-external-club-timestamp: 1741702982
x-external-club-signature: <hmac_sha256_hex>
{
"clubName": "charadescollective",
"externalUserId": "113769124091223507890"
}
```
Resulting `/oauth/userinfo` shape:
```json
{
"sub": "67d0...",
"tcmid": "11735",
"email": "socials@diablo4traders.com",
"externalClubContext": {
"clubName": "charadescollective",
"externalUserId": "113769124091223507890",
"externalClubUser": {
"isSubscribed": true,
"tierName": "Gold",
"billingCycle": "monthly"
}
}
}
```
The webhook receives only partner lookup context and must return only
partner-owned data. The `external_club` scope does not add fixed userinfo
fields by itself; it enables the provisioning + enrichment flow.
## Low-level / Advanced APIs
Drop down to these only when the recommended path doesn't fit:
- `useTcmOAuthPopup` — manual popup management; supply `exchangeCode` yourself.
- `useTcmOAuthPopupRoute` — compatibility route-backed popup-only path; default
callback `/auth/tcm/popup-callback`.
- `createTcmOAuthClient`, `createTcmOAuthRouteClient` — client factories for
custom flows.
- Server helpers under `@crimsoncorp/oauth-react/server` — `createTcmCookieSessionAdapter`,
`resolveTcmAuthSession`.
- Next.js helpers under `@crimsoncorp/oauth-react/nextjs` —
`createTcmOAuthExchangeRoute`, `createTcmLogoutRoute`.
## Operational Notes
- Redirect URI matching is exact. Register local, staging, and production
callbacks separately.
- Keep the callback page same-origin with the opener (popup flows require this).
- Never expose `TCM_OAUTH_CLIENT_SECRET` to the browser.
- Use `createTcmCookieSessionAdapter` so the SDK owns standalone session
cookie naming and clearing.
- Embedded shared-domain `authToken` remains host-owned and is **not** replaced
by the SDK cookie adapter.
- Popup-specific exports still exist for compatibility, but the neutral
`useTcmOAuth` + `TcmOAuthCallbackPage` path is the recommended production
integration.
- If a client uses `external_club`, the same client secret signs both
provisioning requests and webhook enrichment callbacks. Rotate carefully.
## Implementation Checklist
1. Confirm the app framework, router, and package manager.
2. Register the OAuth client in the portal with exact callback URLs and the
minimum scopes (always include `profile`).
3. Set server env vars (`TCM_OAUTH_API_URL`, `TCM_OAUTH_CLIENT_ID`,
`TCM_OAUTH_CLIENT_SECRET`, `TCM_OAUTH_REDIRECT_URI`) and public browser
config (`NEXT_PUBLIC_TCM_OAUTH_CLIENT_ID`, `NEXT_PUBLIC_TCM_OAUTH_WEB_URL`)
without exposing the client secret.
4. Mount `TcmOAuthCallbackPage` at the registered callback path.
5. Add the server exchange route via `createTcmOAuthExchangeRoute` and own
user upsert in `onResolvedUser`.
6. Wire `createTcmCookieSessionAdapter` and `resolveTcmAuthSession` for
session reads; add `createTcmLogoutRoute` for logout.
7. Use `useTcmOAuth` with `interactionMode: "auto"` in browser components.
8. For `external_club` clients, document the bcrypt password requirement,
verify the webhook signature on the partner side, and never log the
raw client secret.
9. Add narrow tests for the protected exchange route, callback page, and
logout behavior.
## Verification
Run the target app's normal checks after integration, e.g.:
```bash
npm run lint
npm run typecheck
npm run test
```
Manually verify:
- Login on desktop opens a popup; on mobile (or with popups blocked) it
redirects.
- Callback at the registered path completes and lands at `returnTo` when set.
- Logout clears the SDK session cookie for `sdk_session` flows and delegates
for `parent_auth_token` flows.
- `/oauth/userinfo` (server-side, after exchange) returns the expected
scope-bound fields.More API Design skills
lark-event
larksuite/cli
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses.
lark-contact
larksuite/cli
飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名,以及按关键词搜索当前用户可见的机器人 / 智能体(agent)。当用户提到一个名字要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。
lark-openapi-explorer
larksuite/cli
飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。

