Guides how to call PinMe platform's Identity Platform auth proxy APIs in a PinMe Worker (TypeScript).
Environment Variables
// 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_KEYandPROJECT_NAMEare required credentials for all auth interfaces; neither can be missing.
Authentication method (common to all interfaces)
| Parameter | Passing method | Required | Description |
|---|---|---|---|
X-API-Key |
Request header | Yes | Project API Key |
project_name |
Query parameter | Yes | Must match X-API-Key for the same project |
The server first verifies whether these two fields match the same project, then retrieves tenant_idfrom the project configuration, and then forwards the request to Identity Platform.
Common errors
| Scenario | HTTP | data.error |
|---|---|---|
Missing X-API-Key |
401 | X-API-Key header is required |
Missing project_name |
400 | project_name is required |
| API Key and project do not match | 401 | Invalid API key or project name |
| Project has no authentication tenant configured | 400 | Auth service not configured for this project |
Common TypeScript types
type 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
}
API 1: Create user
Endpoint: POST {BASE_URL}/api/v1/auth/create_user?project_name={project_name}
Only for email/password registration. On success, the user has been created and the verification email has been sent; on failure, it automatically rolls back and will not leave a zombie account.
After successful creation, the user is still in the "unverified" state by default and must click the email verification link before
verify_tokencan pass validation.
Request body
{ "email": "alice@example.com", "password": "Test@12345678", "display_name": "Alice" }
| Field | Type | Required |
|---|---|---|
email |
string | Yes |
password |
string | Yes |
display_name |
string | No |
Error
| Scenario | HTTP | data.error |
|---|---|---|
| Missing email/password | 400 | email and password are required |
| Upstream creation failed | 502 | Failed to create user |
| Failed to send verification email | 500 | Failed to send verification email. Please try again. |
TypeScript example
async 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 };
}
API 2: Verify id_token
Endpoint: POST {BASE_URL}/api/v1/auth/verify_token?project_name={project_name}
Verify the id_tokenobtained after frontend login (applicable to both email/password and Google login).
Note: When the token is valid but the email is not verified, returns 403, not 401。
Request body
{ "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6..." }
Success response data
type VerifyTokenData = {
uid: string
email?: string
tenant_id: string
claims: Record<string, unknown>
}
Error
| Scenario | HTTP | data.error |
|---|---|---|
Missing id_token |
400 | id_token is required |
| Token invalid or expired | 401 | Invalid or expired token |
| Email not verified | 403 | Email not verified. Please check your inbox and verify your email address. |
TypeScript example
async 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 };
}
API 3: Query a single user
Endpoint: GET {BASE_URL}/api/v1/auth/user?project_name={project_name}&uid={uid}
Error
| Scenario | HTTP | data.error |
|---|---|---|
Missing uid |
400 | uid is required |
| User does not exist | 404 | User not found |
| Upstream query failed | 502 | Failed to get user |
TypeScript example
async 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 };
}
API 4: List users (paginated)
Endpoint: GET {BASE_URL}/api/v1/auth/list_users?project_name={project_name}
Default max_results=100, maximum 1000. Paginate via a next_page_token loop.
Query parameters
| Parameter | Required | Description |
|---|---|---|
project_name |
Yes | Project name |
page_token |
No | Pagination cursor |
max_results |
No | Number per page, 1–1000 |
TypeScript example
async 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);
}
Frontend integration (Firebase Auth)
create_worker The response contains public_client_config, and the frontend uses it to initialize the Firebase Auth SDK.
Distinguishing the two api_keys
| Field | Purpose | Whether it can be exposed to the browser |
|---|---|---|
data.api_key |
Project API Key, used to call all proxy interfaces in this document | Cannot, only for Worker/server-side use |
data.public_client_config.auth_api_key |
Firebase Web API Key, initializes the frontend login SDK | Yes |
public_client_config field descriptions
| Field | Frontend use |
|---|---|
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(must be set, otherwise the token ownership is wrong) |
Frontend TypeScript example
import { 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
The frontend is only responsible for login and obtaining
id_token, do not directly hold the projectapi_key。verify_tokenmust be called on behalf of the client by the Worker/server.frontend/src/utils/config.tsis automatically generated bypinme create, no need to create it manually.
Typical call chain
Email/password registration flow:
create_user→ create user and send verification email- User clicks the email link to complete verification
- Frontend login obtains
id_token verify_token→ verify token, obtainuid- call again when needed
getAuthUserto read the full user information
Google sign-in flow:
- Frontend completes Google Sign-In, obtains
id_token verify_token→ verify token (no need to callcreate_user)
Common pitfalls
| Error | Correct approach |
|---|---|
Only pass X-API-Key, forgetting project_name |
Every request must include both the X-API-Key header and project_name query |
verify_token When a 403 is returned, treat it as token invalidation |
403 = email not verified, prompt the user to check their email; only 401 means the token is invalid |
create_user success is taken to mean the email is verified |
Creation success only means the verification email has been sent; the user must click it to be considered verified |
list_users only fetches the first page |
When there is next_page_token , continue requesting until it is empty |
Judging success only by resp.ok |
also judge resp.ok && result.code === 200 |