Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when a PinMe project (Worker TypeScript) needs to integrate user authentication — creating email/password users, verifying id_tokens, querying user info, or listing users via Identity Platform auth proxy APIs.
.claude/skills/glitternetwork-pinme-auth/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 135% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 115% | 0% |
Guides how to call PinMe platform's Identity Platform auth proxy APIs in a PinMe Worker (TypeScript).
typescript// backend/src/worker.ts export interface Env { DB: D1Database; API_KEY: string; // 项目 API Key — 用于所有 auth 接口认证 PROJECT_NAME: string; // 项目名 — 所有 auth 接口必须同时传递 BASE_URL?: string; // 可选,默认 https://pinme.cloud }
> API_KEY 和 PROJECT_NAME 是所有 auth 接口的必填凭证,缺一不可。
| 参数 | 传递方式 | 必填 | 说明 | |------|---------|------|------| | X-API-Key | 请求头 | 是 | 项目 API Key | | project_name | Query 参数 | 是 | 必须与 X-API-Key 对应同一个项目 |
服务端会先校验这两个字段是否匹配同一个项目,再从项目配置中取出 tenant_id,然后转调 Identity Platform。
| 场景 | HTTP | data.error | |------|------|-------------| | 缺少 X-API-Key | 401 | X-API-Key header is required | | 缺少 project_name | 400 | project_name is required | | API Key 和项目不匹配 | 401 | Invalid API key or project name | | 项目未配置认证租户 | 400 | Auth service not configured for this project |
typescripttype ApiEnvelope<T> = { code: number // 200=成功,其他=失败 msg: string // "ok" | "fail" | "invalid param" data: T } type ApiErrorData = { error?: string } type UserInfo = { uid: string email: string display_name: string photo_url?: string disabled: boolean email_verified: boolean }
Endpoint: POST {BASE_URL}/api/v1/auth/create_user?project_name={project_name}
仅用于邮箱密码注册。成功时用户已创建且验证邮件已发出;失败时自动回滚,不会留下僵尸账号。
> 创建成功后用户默认仍是"未验证"状态,需点击邮件验证链接后,verify_token 才能通过校验。
json{ "email": "alice@example.com", "password": "Test@12345678", "display_name": "Alice" }
| 字段 | 类型 | 必填 | |------|------|------| | email | string | 是 | | password | string | 是 | | display_name | string | 否 |
| 场景 | HTTP | data.error | |------|------|-------------| | 缺少 email/password | 400 | email and password are required | | 上游创建失败 | 502 | Failed to create user | | 发送验证邮件失败 | 500 | Failed to send verification email. Please try again. |
typescriptasync function createAuthUser( env: Env, payload: { email: string; password: string; display_name?: string } ): Promise<{ user?: UserInfo; error?: string }> { const baseUrl = env.BASE_URL ?? 'https://pinme.cloud'; const resp = await fetch( `${baseUrl}/api/v1/auth/create_user?project_name=${encodeURIComponent(env.PROJECT_NAME)}`, { method: 'POST', headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify(payload), } ); const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>; if (!resp.ok || result.code !== 200) { return { error: (result.data as ApiErrorData)?.error ?? result.msg }; } return { user: result.data as UserInfo }; }
Endpoint: POST {BASE_URL}/api/v1/auth/verify_token?project_name={project_name}
校验前端登录后拿到的 id_token(邮箱密码或 Google 登录均适用)。
注意: token 合法但邮箱未验证时返回 403,不是 401。
json{ "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6..." }
typescripttype VerifyTokenData = { uid: string email?: string tenant_id: string claims: Record<string, unknown> }
| 场景 | HTTP | data.error | |------|------|-------------| | 缺少 id_token | 400 | id_token is required | | token 无效或过期 | 401 | Invalid or expired token | | 邮箱未验证 | 403 | Email not verified. Please check your inbox and verify your email address. |
typescriptasync function verifyAuthToken( env: Env, idToken: string ): Promise<{ uid?: string; email?: string; error?: string; emailNotVerified?: boolean }> { const baseUrl = env.BASE_URL ?? 'https://pinme.cloud'; const resp = await fetch( `${baseUrl}/api/v1/auth/verify_token?project_name=${encodeURIComponent(env.PROJECT_NAME)}`, { method: 'POST', headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ id_token: idToken }), } ); const result = await resp.json() as ApiEnvelope<VerifyTokenData | ApiErrorData>; if (!resp.ok || result.code !== 200) { const error = (result.data as ApiErrorData)?.error ?? result.msg; return { error, emailNotVerified: resp.status === 403 }; } const data = result.data as VerifyTokenData; return { uid: data.uid, email: data.email }; }
Endpoint: GET {BASE_URL}/api/v1/auth/user?project_name={project_name}&uid={uid}
| 场景 | HTTP | data.error | |------|------|-------------| | 缺少 uid | 400 | uid is required | | 用户不存在 | 404 | User not found | | 上游查询失败 | 502 | Failed to get user |
typescriptasync function getAuthUser(env: Env, uid: string): Promise<{ user?: UserInfo; error?: string }> { const baseUrl = env.BASE_URL ?? 'https://pinme.cloud'; const resp = await fetch( `${baseUrl}/api/v1/auth/user?project_name=${encodeURIComponent(env.PROJECT_NAME)}&uid=${encodeURIComponent(uid)}`, { method: 'GET', headers: { 'X-API-Key': env.API_KEY } } ); const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>; if (!resp.ok || result.code !== 200) { return { error: (result.data as ApiErrorData)?.error ?? result.msg }; } return { user: result.data as UserInfo }; }
Endpoint: GET {BASE_URL}/api/v1/auth/list_users?project_name={project_name}
默认 max_results=100,最大 1000。通过 next_page_token 循环翻页。
| 参数 | 必填 | 说明 | |------|------|------| | project_name | 是 | 项目名 | | page_token | 否 | 分页游标 | | max_results | 否 | 每页数量,1–1000 |
typescriptasync function listAuthUsers( env: Env, options: { pageToken?: string; maxResults?: number } = {} ): Promise<{ users?: UserInfo[]; nextPageToken?: string; error?: string }> { const baseUrl = env.BASE_URL ?? 'https://pinme.cloud'; const url = new URL('/api/v1/auth/list_users', baseUrl); url.searchParams.set('project_name', env.PROJECT_NAME); if (options.pageToken) url.searchParams.set('page_token', options.pageToken); if (options.maxResults) url.searchParams.set('max_results', String(options.maxResults)); const resp = await fetch(url.toString(), { method: 'GET', headers: { 'X-API-Key': env.API_KEY } }); const result = await resp.json() as ApiEnvelope<{ users: UserInfo[]; next_page_token?: string } | ApiErrorData>; if (!resp.ok || result.code !== 200) { return { error: (result.data as ApiErrorData)?.error ?? result.msg }; } const data = result.data as { users: UserInfo[]; next_page_token?: string }; return { users: data.users, nextPageToken: data.next_page_token }; } // 批量遍历所有用户示例 async function* iterAllUsers(env: Env) { let pageToken: string | undefined; do { const { users, nextPageToken, error } = await listAuthUsers(env, { pageToken, maxResults: 1000 }); if (error) throw new Error(error); for (const user of users ?? []) yield user; pageToken = nextPageToken; } while (pageToken); }
create_worker 响应中包含 public_client_config,前端用它初始化 Firebase Auth SDK。
| 字段 | 用途 | 是否可暴露到浏览器 | |------|------|-----------------| | data.api_key | 项目 API Key,调用本文所有代理接口 | 不能,只给 Worker/服务端 | | data.public_client_config.auth_api_key | Firebase Web API Key,初始化前端登录 SDK | 可以 |
| 字段 | 前端用途 | |------|---------| | public_client_config.auth_api_key | initializeApp({ apiKey }) | | public_client_config.auth_domain | initializeApp({ authDomain }) | | public_client_config.auth_project_id | initializeApp({ projectId }) | | public_client_config.tenant_id | auth.tenantId = config.tenant_id(必须设置,否则 token 归属错误) |
typescriptimport { initializeApp } from 'firebase/app' import { type Auth, getAuth, GoogleAuthProvider, signInWithEmailAndPassword, signInWithPopup, } from 'firebase/auth' type PublicClientConfig = { tenant_id: string auth_api_key: string auth_domain: string auth_project_id: string } export function createProjectAuth(config: PublicClientConfig): Auth { const app = initializeApp({ apiKey: config.auth_api_key, authDomain: config.auth_domain, projectId: config.auth_project_id, }) const auth = getAuth(app) auth.tenantId = config.tenant_id // 必须设置,确保 token 归属正确租户 return auth } // 邮箱密码登录,返回 id_token export async function loginWithEmail(auth: Auth, email: string, password: string): Promise<string> { const credential = await signInWithEmailAndPassword(auth, email, password) return credential.user.getIdToken() } // Google 登录,返回 id_token export async function loginWithGoogle(auth: Auth): Promise<string> { const credential = await signInWithPopup(auth, new GoogleAuthProvider()) return credential.user.getIdToken() } // 用法示例 // pinme create 会自动将 public_client_config 写入 frontend/src/utils/config.ts import { public_client_config } from '../utils/config' const auth = createProjectAuth(public_client_config) const idToken = await loginWithGoogle(auth) // 然后把 idToken 发给自己的 Worker,由 Worker 调用 verify_token
> 前端只负责登录和拿 id_token,不要直接持有项目 api_key。verify_token 必须由 Worker/服务端代调。 > frontend/src/utils/config.ts 由 pinme create 自动生成,无需手动创建。
邮箱密码注册流程:
create_user → 创建用户并发出验证邮件id_tokenverify_token → 校验 token,取得 uidgetAuthUser 读取完整用户信息Google 登录流程:
id_tokenverify_token → 校验 token(无需调用 create_user)| 错误 | 正确做法 | |------|---------| | 只传 X-API-Key,忘记 project_name | 每个请求都要同时带 X-API-Key header 和 project_name query | | verify_token 返回 403 时当 token 失效处理 | 403 = 邮箱未验证,提示用户检查邮箱;401 才是 token 失效 | | create_user 成功就认为邮箱已验证 | 创建成功只代表验证邮件已发,用户必须点击后才算验证 | | list_users 只取第一页 | 有 next_page_token 时需继续请求,直到为空 | | 成功判断只看 resp.ok | 同时判断 resp.ok && result.code === 200 |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-12 | pass→pass | 12,518 | 4,502 | -64% | 1 | 1 | 0% | 2,565 | 4,418 | +72% | 0 | 0 | — |
case-05 | fail→pass | 10,932 | 5,277 | -52% | 1 | 1 | 0% | 1,952 | 4,586 | +135% | 0 | 0 | — |
case-11 | fail→pass | 9,183 | 3,431 | -63% | 1 | 1 | 0% | 1,759 | 4,358 | +148% | 0 | 0 | — |
case-01 | fail→pass | 24,321 | 14,190 | -42% | 1 | 1 | 0% | 3,172 | 7,495 | +136% | 0 | 0 | — |
case-02 | fail→pass | 15,176 | 15,121 | -0% | 1 | 1 | 0% | 3,263 | 7,537 | +131% | 0 | 0 | — |
case-03 | pass→pass | 11,275 | 7,422 | -34% | 1 | 1 | 0% | 2,591 | 5,420 | +109% | 0 | 0 | — |
case-04 | fail→pass | 12,548 | 5,341 | -57% | 1 | 1 | 0% | 2,212 | 4,753 | +115% | 0 | 0 | — |
case-06 | pass→pass | 9,057 | 3,244 | -64% | 1 | 1 | 0% | 1,755 | 4,260 | +143% | 0 | 0 | — |
case-07 | fail→pass | 11,262 | 5,318 | -53% | 1 | 1 | 0% | 2,191 | 4,692 | +114% | 0 | 0 | — |
case-08 | pass→pass | 13,734 | 5,754 | -58% | 1 | 1 | 0% | 2,608 | 4,790 | +84% | 0 | 0 | — |
case-09 | pass→pass | 11,497 | 2,517 | -78% | 1 | 1 | 0% | 2,089 | 3,963 | +90% | 0 | 0 | — |
case-10 | pass→pass | 10,681 | 6,439 | -40% | 1 | 1 | 0% | 1,956 | 4,887 | +150% | 0 | 0 | — |
case-13 | pass→pass | 15,980 | 8,923 | -44% | 1 | 1 | 0% | 3,254 | 5,429 | +67% | 0 | 0 | — |
case-14 | fail→pass | 11,435 | 1,817 | -84% | 1 | 1 | 0% | 1,968 | 3,888 | +98% | 0 | 0 | — |
case-15 | pass→pass | 9,497 | 6,971 | -27% | 1 | 1 | 0% | 1,700 | 4,892 | +188% | 0 | 0 | — |
case-16 | fail→pass | 11,493 | 3,905 | -66% | 1 | 1 | 0% | 1,930 | 4,325 | +124% | 0 | 0 | — |
case-17 | fail→pass | 8,731 | 2,982 | -66% | 1 | 1 | 0% | 1,495 | 4,101 | +174% | 0 | 0 | — |
case-18 | fail→pass | 13,836 | 8,817 | -36% | 1 | 1 | 0% | 2,575 | 5,230 | +103% | 0 | 0 | — |
case-19 | pass→pass | 9,640 | 4,379 | -55% | 1 | 1 | 0% | 1,843 | 4,486 | +143% | 0 | 0 | — |
case-20 | pass→pass | 12,248 | 9,014 | -26% | 1 | 1 | 0% | 2,416 | 5,367 | +122% | 0 | 0 | — |
case-21 | pass→fail | 9,559 | 8,348 | -13% | 1 | 1 | 0% | 1,881 | 5,281 | +181% | 0 | 0 | — |
case-22 | pass→pass | 12,087 | 8,485 | -30% | 1 | 1 | 0% | 2,633 | 5,383 | +104% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.