SuperAPI AI Directory

pinme-auth

Security & auditing

View repository

Updated 10-15 13:29

About this skill

PinMe Worker Auth API Integration

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_KEY and PROJECT_NAME are 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 beforeverify_token can 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 project api_key。verify_token must be called on behalf of the client by the Worker/server. frontend/src/utils/config.ts is automatically generated by pinme create , no need to create it manually.


Typical call chain

Email/password registration flow:

  1. create_user → create user and send verification email
  2. User clicks the email link to complete verification
  3. Frontend login obtains id_token
  4. verify_token → verify token, obtain uid
  5. call again when needed getAuthUser to read the full user information

Google sign-in flow:

  1. Frontend completes Google Sign-In, obtains id_token
  2. verify_token → verify token (no need to call create_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