{"items":[{"id":"cmugwhsew01gnqu06au6h66y7","slug":"glitternetwork-pinme-pinme-auth","name":"pinme-auth","description":"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.","authorId":"gh:glitternetwork","authorName":"glitternetwork","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3745,"pricePerCall":0,"manifest":{"name":"pinme-auth","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"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.","permissions":[],"systemPrompt":"# PinMe Worker Auth API Integration\n\nGuides how to call PinMe platform's Identity Platform auth proxy APIs in a PinMe Worker (TypeScript).\n\n## Environment Variables\n\n```typescript\n// backend/src/worker.ts\nexport interface Env {\n  DB: D1Database;\n  API_KEY: string;       // 项目 API Key — 用于所有 auth 接口认证\n  PROJECT_NAME: string;  // 项目名 — 所有 auth 接口必须同时传递\n  BASE_URL?: string;     // 可选，默认 https://pinme.cloud\n}\n```\n\n> `API_KEY` 和 `PROJECT_NAME` 是所有 auth 接口的必填凭证，缺一不可。\n\n---\n\n## 认证方式（所有接口通用）\n\n| 参数 | 传递方式 | 必填 | 说明 |\n|------|---------|------|------|\n| `X-API-Key` | 请求头 | 是 | 项目 API Key |\n| `project_name` | Query 参数 | 是 | 必须与 `X-API-Key` 对应同一个项目 |\n\n服务端会先校验这两个字段是否匹配同一个项目，再从项目配置中取出 `tenant_id`，然后转调 Identity Platform。\n\n---\n\n## 通用错误\n\n| 场景 | HTTP | `data.error` |\n|------|------|-------------|\n| 缺少 `X-API-Key` | 401 | `X-API-Key header is required` |\n| 缺少 `project_name` | 400 | `project_name is required` |\n| API Key 和项目不匹配 | 401 | `Invalid API key or project name` |\n| 项目未配置认证租户 | 400 | `Auth service not configured for this project` |\n\n---\n\n## 通用 TypeScript 类型\n\n```typescript\ntype ApiEnvelope<T> = {\n  code: number   // 200=成功，其他=失败\n  msg: string    // \"ok\" | \"fail\" | \"invalid param\"\n  data: T\n}\n\ntype ApiErrorData = { error?: string }\n\ntype UserInfo = {\n  uid: string\n  email: string\n  display_name: string\n  photo_url?: string\n  disabled: boolean\n  email_verified: boolean\n}\n```\n\n---\n\n## API 1: 创建用户\n\n**Endpoint:** `POST {BASE_URL}/api/v1/auth/create_user?project_name={project_name}`\n\n仅用于邮箱密码注册。成功时用户已创建且验证邮件已发出；失败时自动回滚，不会留下僵尸账号。\n\n> 创建成功后用户默认仍是\"未验证\"状态，需点击邮件验证链接后，`verify_token` 才能通过校验。\n\n### 请求体\n\n```json\n{ \"email\": \"alice@example.com\", \"password\": \"Test@12345678\", \"display_name\": \"Alice\" }\n```\n\n| 字段 | 类型 | 必填 |\n|------|------|------|\n| `email` | string | 是 |\n| `password` | string | 是 |\n| `display_name` | string | 否 |\n\n### 错误\n\n| 场景 | HTTP | `data.error` |\n|------|------|-------------|\n| 缺少 email/password | 400 | `email and password are required` |\n| 上游创建失败 | 502 | `Failed to create user` |\n| 发送验证邮件失败 | 500 | `Failed to send verification email. Please try again.` |\n\n### TypeScript 示例\n\n```typescript\nasync function createAuthUser(\n  env: Env,\n  payload: { email: string; password: string; display_name?: string }\n): Promise<{ user?: UserInfo; error?: string }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/auth/create_user?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' },\n      body: JSON.stringify(payload),\n    }\n  );\n  const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>;\n  if (!resp.ok || result.code !== 200) {\n    return { error: (result.data as ApiErrorData)?.error ?? result.msg };\n  }\n  return { user: result.data as UserInfo };\n}\n```\n\n---\n\n## API 2: 校验 id_token\n\n**Endpoint:** `POST {BASE_URL}/api/v1/auth/verify_token?project_name={project_name}`\n\n校验前端登录后拿到的 `id_token`（邮箱密码或 Google 登录均适用）。\n\n**注意：** token 合法但邮箱未验证时返回 `403`，不是 `401`。\n\n### 请求体\n\n```json\n{ \"id_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\" }\n```\n\n### 成功响应 data\n\n```typescript\ntype VerifyTokenData = {\n  uid: string\n  email?: string\n  tenant_id: string\n  claims: Record<string, unknown>\n}\n```\n\n### 错误\n\n| 场景 | HTTP | `data.error` |\n|------|------|-------------|\n| 缺少 `id_token` | 400 | `id_token is required` |\n| token 无效或过期 | 401 | `Invalid or expired token` |\n| 邮箱未验证 | 403 | `Email not verified. Please check your inbox and verify your email address.` |\n\n### TypeScript 示例\n\n```typescript\nasync function verifyAuthToken(\n  env: Env,\n  idToken: string\n): Promise<{ uid?: string; email?: string; error?: string; emailNotVerified?: boolean }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/auth/verify_token?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' },\n      body: JSON.stringify({ id_token: idToken }),\n    }\n  );\n  const result = await resp.json() as ApiEnvelope<VerifyTokenData | ApiErrorData>;\n  if (!resp.ok || result.code !== 200) {\n    const error = (result.data as ApiErrorData)?.error ?? result.msg;\n    return { error, emailNotVerified: resp.status === 403 };\n  }\n  const data = result.data as VerifyTokenData;\n  return { uid: data.uid, email: data.email };\n}\n```\n\n---\n\n## API 3: 查询单个用户\n\n**Endpoint:** `GET {BASE_URL}/api/v1/auth/user?project_name={project_name}&uid={uid}`\n\n### 错误\n\n| 场景 | HTTP | `data.error` |\n|------|------|-------------|\n| 缺少 `uid` | 400 | `uid is required` |\n| 用户不存在 | 404 | `User not found` |\n| 上游查询失败 | 502 | `Failed to get user` |\n\n### TypeScript 示例\n\n```typescript\nasync function getAuthUser(env: Env, uid: string): Promise<{ user?: UserInfo; error?: string }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/auth/user?project_name=${encodeURIComponent(env.PROJECT_NAME)}&uid=${encodeURIComponent(uid)}`,\n    { method: 'GET', headers: { 'X-API-Key': env.API_KEY } }\n  );\n  const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>;\n  if (!resp.ok || result.code !== 200) {\n    return { error: (result.data as ApiErrorData)?.error ?? result.msg };\n  }\n  return { user: result.data as UserInfo };\n}\n```\n\n---\n\n## API 4: 列出用户（分页）\n\n**Endpoint:** `GET {BASE_URL}/api/v1/auth/list_users?project_name={project_name}`\n\n默认 `max_results=100`，最大 `1000`。通过 `next_page_token` 循环翻页。\n\n### Query 参数\n\n| 参数 | 必填 | 说明 |\n|------|------|------|\n| `project_name` | 是 | 项目名 |\n| `page_token` | 否 | 分页游标 |\n| `max_results` | 否 | 每页数量，1–1000 |\n\n### TypeScript 示例\n\n```typescript\nasync function listAuthUsers(\n  env: Env,\n  options: { pageToken?: string; maxResults?: number } = {}\n): Promise<{ users?: UserInfo[]; nextPageToken?: string; error?: string }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const url = new URL('/api/v1/auth/list_users', baseUrl);\n  url.searchParams.set('project_name', env.PROJECT_NAME);\n  if (options.pageToken) url.searchParams.set('page_token', options.pageToken);\n  if (options.maxResults) url.searchParams.set('max_results', String(options.maxResults));\n\n  const resp = await fetch(url.toString(), { method: 'GET', headers: { 'X-API-Key': env.API_KEY } });\n  const result = await resp.json() as ApiEnvelope<{ users: UserInfo[]; next_page_token?: string } | ApiErrorData>;\n  if (!resp.ok || result.code !== 200) {\n    return { error: (result.data as ApiErrorData)?.error ?? result.msg };\n  }\n  const data = result.data as { users: UserInfo[]; next_page_token?: string };\n  return { users: data.users, nextPageToken: data.next_page_token };\n}\n\n// 批量遍历所有用户示例\nasync function* iterAllUsers(env: Env) {\n  let pageToken: string | undefined;\n  do {\n    const { users, nextPageToken, error } = await listAuthUsers(env, { pageToken, maxResults: 1000 });\n    if (error) throw new Error(error);\n    for (const user of users ?? []) yield user;\n    pageToken = nextPageToken;\n  } while (pageToken);\n}\n```\n\n---\n\n## 前端集成（Firebase Auth）\n\n`create_worker` 响应中包含 `public_client_config`，前端用它初始化 Firebase Auth SDK。\n\n### 两种 api_key 区分\n\n| 字段 | 用途 | 是否可暴露到浏览器 |\n|------|------|-----------------|\n| `data.api_key` | 项目 API Key，调用本文所有代理接口 | **不能**，只给 Worker/服务端 |\n| `data.public_client_config.auth_api_key` | Firebase Web API Key，初始化前端登录 SDK | 可以 |\n\n### public_client_config 字段说明\n\n| 字段 | 前端用途 |\n|------|---------|\n| `public_client_config.auth_api_key` | `initializeApp({ apiKey })` |\n| `public_client_config.auth_domain` | `initializeApp({ authDomain })` |\n| `public_client_config.auth_project_id` | `initializeApp({ projectId })` |\n| `public_client_config.tenant_id` | `auth.tenantId = config.tenant_id`（必须设置，否则 token 归属错误） |\n\n### 前端 TypeScript 示例\n\n```typescript\nimport { initializeApp } from 'firebase/app'\nimport {\n  type Auth,\n  getAuth,\n  GoogleAuthProvider,\n  signInWithEmailAndPassword,\n  signInWithPopup,\n} from 'firebase/auth'\n\ntype PublicClientConfig = {\n  tenant_id: string\n  auth_api_key: string\n  auth_domain: string\n  auth_project_id: string\n}\n\nexport function createProjectAuth(config: PublicClientConfig): Auth {\n  const app = initializeApp({\n    apiKey: config.auth_api_key,\n    authDomain: config.auth_domain,\n    projectId: config.auth_project_id,\n  })\n  const auth = getAuth(app)\n  auth.tenantId = config.tenant_id  // 必须设置，确保 token 归属正确租户\n  return auth\n}\n\n// 邮箱密码登录，返回 id_token\nexport async function loginWithEmail(auth: Auth, email: string, password: string): Promise<string> {\n  const credential = await signInWithEmailAndPassword(auth, email, password)\n  return credential.user.getIdToken()\n}\n\n// Google 登录，返回 id_token\nexport async function loginWithGoogle(auth: Auth): Promise<string> {\n  const credential = await signInWithPopup(auth, new GoogleAuthProvider())\n  return credential.user.getIdToken()\n}\n\n// 用法示例\n// pinme create 会自动将 public_client_config 写入 frontend/src/utils/config.ts\nimport { public_client_config } from '../utils/config'\n\nconst auth = createProjectAuth(public_client_config)\nconst idToken = await loginWithGoogle(auth)\n// 然后把 idToken 发给自己的 Worker，由 Worker 调用 verify_token\n```\n\n> 前端只负责登录和拿 `id_token`，不要直接持有项目 `api_key`。`verify_token` 必须由 Worker/服务端代调。\n> `frontend/src/utils/config.ts` 由 `pinme create` 自动生成，无需手动创建。\n\n---\n\n## 典型调用链路\n\n**邮箱密码注册流程：**\n1. `create_user` → 创建用户并发出验证邮件\n2. 用户点击邮件链接完成验证\n3. 前端登录拿到 `id_token`\n4. `verify_token` → 校验 token，取得 `uid`\n5. 需要时再调 `getAuthUser` 读取完整用户信息\n\n**Google 登录流程：**\n1. 前端完成 Google Sign-In，拿到 `id_token`\n2. `verify_token` → 校验 token（无需调用 `create_user`）\n\n---\n\n## 易错点\n\n| 错误 | 正确做法 |\n|------|---------|\n| 只传 `X-API-Key`，忘记 `project_name` | 每个请求都要同时带 `X-API-Key` header 和 `project_name` query |\n| `verify_token` 返回 403 时当 token 失效处理 | 403 = 邮箱未验证，提示用户检查邮箱；401 才是 token 失效 |\n| `create_user` 成功就认为邮箱已验证 | 创建成功只代表验证邮件已发，用户必须点击后才算验证 |\n| `list_users` 只取第一页 | 有 `next_page_token` 时需继续请求，直到为空 |\n| 成功判断只看 `resp.ok` | 同时判断 `resp.ok && result.code === 200` |","schemaVersion":1},"repoUrl":"https://github.com/glitternetwork/pinme/tree/main/skills/pinme-auth","tags":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"pinme","audit":{"files":["package-lock.json","package.json","pnpm-lock.yaml"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS).","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-7q85-xj36-vmfc · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip extraction follows destination symlinks, allowing arbitrary file overwrite.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-vwc7-r8mq-g2x9 · npm:adm-zip@0.5.17","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Crafted ZIP file triggers 4GB memory allocation.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-xcpc-8h2w-3j85 · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"uuid@9.0.1 has a known vulnerability: uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-w5hq-g745-h8pq · npm:uuid@9.0.1","severity":"medium"}],"packages":16,"auditedAt":"2026-09-25T11:51:56.672Z","lockfiles":["package-lock.json","pnpm-lock.yaml"]},"forks":277,"owner":"glitternetwork","stars":3745,"topics":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy","static-site-hosting","web-hosting","zero-configuration"],"license":"MIT","fullName":"glitternetwork/pinme","homepage":"https://pinme.eth.limo","language":"TypeScript","pushedAt":"2026-09-12T05:23:49Z","avatarUrl":"https://avatars.githubusercontent.com/u/102277171?v=4","crawledAt":"2026-09-25T11:51:44.477Z","openIssues":7,"manifestFile":"SKILL.md","manifestPath":"skills/pinme-auth/SKILL.md","defaultBranch":"main"},"readme":"# PinMe Worker Auth API Integration\n\nGuides how to call PinMe platform's Identity Platform auth proxy APIs in a PinMe Worker (TypeScript).\n\n## Environment Variables\n\n```typescript\n// backend/src/worker.ts\nexport interface Env {\n  DB: D1Database;\n  API_KEY: string;       // 项目 API Key — 用于所有 auth 接口认证\n  PROJECT_NAME: string;  // 项目名 — 所有 auth 接口必须同时传递\n  BASE_URL?: string;     // 可选，默认 https://pinme.cloud\n}\n```\n\n> `API_KEY` 和 `PROJECT_NAME` 是所有 auth 接口的必填凭证，缺一不可。\n\n---\n\n## 认证方式（所有接口通用）\n\n| 参数 | 传递方式 | 必填 | 说明 |\n|------|---------|------|------|\n| `X-API-Key` | 请求头 | 是 | 项目 API Key |\n| `project_name` | Query 参数 | 是 | 必须与 `X-API-Key` 对应同一个项目 |\n\n服务端会先校验这两个字段是否匹配同一个项目，再从项目配置中取出 `tenant_id`，然后转调 Identity Platform。\n\n---\n\n## 通用错误\n\n| 场景 | HTTP | `data.error` |\n|------|------|-------------|\n| 缺少 `X-API-Key` | 401 | `X-API-Key header is required` |\n| 缺少 `project_name` | 400 | `project_name is required` |\n| API Key 和项目不匹配 | 401 | `Invalid API key or project name` |\n| 项目未配置认证租户 | 400 | `Auth service not configured for this project` |\n\n---\n\n## 通用 TypeScript 类型\n\n```typescript\ntype ApiEnvelope<T> = {\n  code: number   // 200=成功，其他=失败\n  msg: string    // \"ok\" | \"fail\" | \"invalid param\"\n  data: T\n}\n\ntype ApiErrorData = { error?: string }\n\ntype UserInfo = {\n  uid: string\n  email: string\n  display_name: string\n  photo_url?: string\n  disabled: boolean\n  email_verified: boolean\n}\n```\n\n---\n\n## API 1: 创建用户\n\n**Endpoint:** `POST {BASE_URL}/api/v1/auth/create_user?project_name={project_name}`\n\n仅用于邮箱密码注册。成功时用户已创建且验证邮件已发出；失败时自动回滚，不会留下僵尸账号。\n\n> 创建成功后用户默认仍是\"未验证\"状态，需点击邮件验证链接后，`verify_token` 才能通过校验。\n\n### 请求体\n\n```json\n{ \"email\": \"alice@example.com\", \"password\": \"Test@12345678\", \"display_name\": \"Alice\" }\n```\n\n| 字段 | 类型 | 必填 |\n|------|------|------|\n| `email` | string | 是 |\n| `password` | string | 是 |\n| `display_name` | string | 否 |\n\n### 错误\n\n| 场景 | HTTP | `data.error` |\n|------|------|-------------|\n| 缺少 email/password | 400 | `email and password are required` |\n| 上游创建失败 | 502 | `Failed to create user` |\n| 发送验证邮件失败 | 500 | `Failed to send verification email. Please try again.` |\n\n### TypeScript 示例\n\n```typescript\nasync function createAuthUser(\n  env: Env,\n  payload: { email: string; password: string; display_name?: string }\n): Promise<{ user?: UserInfo; error?: string }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/auth/create_user?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' },\n      body: JSON.stringify(payload),\n    }\n  );\n  const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>;\n  if (!resp.ok || result.code !== 200) {\n    return { error: (result.data as ApiErrorData)?.error ?? result.msg };\n  }\n  return { user: result.data as UserInfo };\n}\n```\n\n---\n\n## API 2: 校验 id_token\n\n**Endpoint:** `POST {BASE_URL}/api/v1/auth/verify_token?project_name={project_name}`\n\n校验前端登录后拿到的 `id_token`（邮箱密码或 Google 登录均适用）。\n\n**注意：** token 合法但邮箱未验证时返回 `403`，不是 `401`。\n\n### 请求体\n\n```json\n{ \"id_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\" }\n```\n\n### 成功响应 data\n\n```typescript\ntype VerifyTokenData = {\n  uid: string\n  email?: string\n  tenant_id: string\n  claims: Record<string, unknown>\n}\n```\n\n### 错误\n\n| 场景 | HTTP | `data.error` |\n|------|------|-------------|\n| 缺少 `id_token` | 400 | `id_token is required` |\n| token 无效或过期 | 401 | `Invalid or expired token` |\n| 邮箱未验证 | 403 | `Email not verified. Please check your inbox and verify your email address.` |\n\n### TypeScript 示例\n\n```typescript\nasync function verifyAuthToken(\n  env: Env,\n  idToken: string\n): Promise<{ uid?: string; email?: string; error?: string; emailNotVerified?: boolean }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/auth/verify_token?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'applicati","createdAt":"2026-09-25T11:51:56.696Z","updatedAt":"2026-09-25T11:51:56.696Z"},{"id":"cmugwhsf701gqqu06a0r4op12","slug":"glitternetwork-pinme-pinme-email","name":"pinme-email","description":"Use this skill when a PinMe project (Worker TypeScript) needs to integrate email sending (send_email). Guides AI to generate correct Worker TS code.","authorId":"gh:glitternetwork","authorName":"glitternetwork","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3745,"pricePerCall":0,"manifest":{"name":"pinme-email","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use this skill when a PinMe project (Worker TypeScript) needs to integrate email sending (send_email). Guides AI to generate correct Worker TS code.","permissions":[],"systemPrompt":"# PinMe Worker Email API Integration\n\nGuides how to call PinMe platform's email sending API in a PinMe Worker (TypeScript).\n\n## Environment Variables\n\nThe following environment variables are automatically injected when the Worker is created — no manual configuration needed:\n\n```typescript\n// backend/src/worker.ts\nexport interface Env {\n  DB: D1Database;\n  API_KEY: string;      // Project API Key — used for send_email authentication\n  BASE_URL?: string;    // Optional override for PinMe API base URL, defaults to https://pinme.cloud\n}\n```\n\n> `API_KEY` is the sole credential for the Worker to call PinMe platform APIs. When `BASE_URL` is not set, it defaults to `https://pinme.cloud`.\n\n---\n\n## Send Email API\n\n**Endpoint:** `POST {BASE_URL}/api/v4/send_email`\n**Authentication:** `X-API-Key` header (using `env.API_KEY`)\n**Sender:** Automatically set to `{project_name}@pinme.cloud`\n\n### Request Format\n\n```json\n{\n  \"to\": \"user@example.com\",\n  \"subject\": \"Your verification code\",\n  \"html\": \"<p>Your code is <strong>123456</strong></p>\"\n}\n```\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `to` | string | Yes | Recipient email address |\n| `subject` | string | Yes | Email subject |\n| `html` | string | Yes | HTML body |\n\n### Response Format\n\n**Success (200):**\n```json\n{ \"code\": 200, \"msg\": \"ok\", \"data\": { \"ok\": true } }\n```\n\n**Errors:**\n\n| HTTP Status | Meaning | data.error Example |\n|-------------|---------|-------------------|\n| 401 | API Key missing or invalid | `\"X-API-Key header is required\"` / `\"Invalid API key\"` |\n| 400 | Parameter validation failed | `\"Invalid email address\"` / `\"Subject is required\"` |\n| 500 | Email service error | `\"Failed to send email\"` |\n\n### Worker Example Code\n\n```typescript\nasync function sendEmail(env: Env, to: string, subject: string, html: string): Promise<{ ok: boolean; error?: string }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(`${baseUrl}/api/v4/send_email`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'X-API-Key': env.API_KEY,\n    },\n    body: JSON.stringify({ to, subject, html }),\n  });\n\n  const result = await resp.json() as { code: number; msg: string; data?: { ok?: boolean; error?: string } };\n\n  if (resp.status !== 200 || result.code !== 200) {\n    return { ok: false, error: result.data?.error || result.msg || 'Unknown error' };\n  }\n  return { ok: true };\n}\n\n// Usage in routes\nasync function handleSendVerification(request: Request, env: Env): Promise<Response> {\n  const { email } = await request.json() as { email: string };\n  const code = Math.random().toString().slice(2, 8);\n\n  const result = await sendEmail(env, email, 'Verification Code',\n    `<p>Your code is <strong>${code}</strong></p>`);\n\n  if (!result.ok) {\n    return json({ error: result.error }, 500);\n  }\n  return json({ ok: true });\n}\n```\n\n---\n\n## Error Handling Pattern\n\nPinMe platform API unified response format:\n\n```typescript\ninterface PinmeResponse<T = unknown> {\n  code: number;   // 200=success, other=failure\n  msg: string;    // \"ok\" | \"error\" | \"invalid params\"\n  data?: T;       // Business data on success, may contain { error: string } on failure\n}\n```\n\n### Recommended Unified Error Handler\n\n```typescript\nasync function callPinmeAPI<T>(url: string, apiKey: string, body: unknown): Promise<{ data?: T; error?: string }> {\n  let resp: Response;\n  try {\n    resp = await fetch(url, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },\n      body: JSON.stringify(body),\n    });\n  } catch {\n    return { error: 'Network error' };\n  }\n\n  if (!resp.ok) {\n    try {\n      const err = await resp.json() as PinmeResponse;\n      return { error: err.data && typeof err.data === 'object' && 'error' in err.data\n        ? (err.data as { error: string }).error\n        : err.msg || `HTTP ${resp.status}` };\n    } catch {\n      return { error: `HTTP ${resp.status}` };\n    }\n  }\n\n  const result = await resp.json() as PinmeResponse<T>;\n  if (result.code !== 200) {\n    return { error: result.data && typeof result.data === 'object' && 'error' in result.data\n      ? (result.data as { error: string }).error\n      : result.msg };\n  }\n  return { data: result.data as T };\n}\n```\n\n### Usage Example\n\n```typescript\nconst baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n\n// Send email\nconst emailResult = await callPinmeAPI<{ ok: boolean }>(\n  `${baseUrl}/api/v4/send_email`, env.API_KEY,\n  { to: 'user@example.com', subject: 'Hello', html: '<p>Hi</p>' },\n);\nif (emailResult.error) return json({ error: emailResult.error }, 500);\n```","schemaVersion":1},"repoUrl":"https://github.com/glitternetwork/pinme/tree/main/skills/pinme-email","tags":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"pinme","audit":{"files":["package-lock.json","package.json","pnpm-lock.yaml"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS).","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-7q85-xj36-vmfc · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip extraction follows destination symlinks, allowing arbitrary file overwrite.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-vwc7-r8mq-g2x9 · npm:adm-zip@0.5.17","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Crafted ZIP file triggers 4GB memory allocation.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-xcpc-8h2w-3j85 · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"uuid@9.0.1 has a known vulnerability: uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-w5hq-g745-h8pq · npm:uuid@9.0.1","severity":"medium"}],"packages":16,"auditedAt":"2026-09-25T11:51:56.672Z","lockfiles":["package-lock.json","pnpm-lock.yaml"]},"forks":277,"owner":"glitternetwork","stars":3745,"topics":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy","static-site-hosting","web-hosting","zero-configuration"],"license":"MIT","fullName":"glitternetwork/pinme","homepage":"https://pinme.eth.limo","language":"TypeScript","pushedAt":"2026-09-12T05:23:49Z","avatarUrl":"https://avatars.githubusercontent.com/u/102277171?v=4","crawledAt":"2026-09-25T11:51:44.477Z","openIssues":7,"manifestFile":"SKILL.md","manifestPath":"skills/pinme-email/SKILL.md","defaultBranch":"main"},"readme":"# PinMe Worker Email API Integration\n\nGuides how to call PinMe platform's email sending API in a PinMe Worker (TypeScript).\n\n## Environment Variables\n\nThe following environment variables are automatically injected when the Worker is created — no manual configuration needed:\n\n```typescript\n// backend/src/worker.ts\nexport interface Env {\n  DB: D1Database;\n  API_KEY: string;      // Project API Key — used for send_email authentication\n  BASE_URL?: string;    // Optional override for PinMe API base URL, defaults to https://pinme.cloud\n}\n```\n\n> `API_KEY` is the sole credential for the Worker to call PinMe platform APIs. When `BASE_URL` is not set, it defaults to `https://pinme.cloud`.\n\n---\n\n## Send Email API\n\n**Endpoint:** `POST {BASE_URL}/api/v4/send_email`\n**Authentication:** `X-API-Key` header (using `env.API_KEY`)\n**Sender:** Automatically set to `{project_name}@pinme.cloud`\n\n### Request Format\n\n```json\n{\n  \"to\": \"user@example.com\",\n  \"subject\": \"Your verification code\",\n  \"html\": \"<p>Your code is <strong>123456</strong></p>\"\n}\n```\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `to` | string | Yes | Recipient email address |\n| `subject` | string | Yes | Email subject |\n| `html` | string | Yes | HTML body |\n\n### Response Format\n\n**Success (200):**\n```json\n{ \"code\": 200, \"msg\": \"ok\", \"data\": { \"ok\": true } }\n```\n\n**Errors:**\n\n| HTTP Status | Meaning | data.error Example |\n|-------------|---------|-------------------|\n| 401 | API Key missing or invalid | `\"X-API-Key header is required\"` / `\"Invalid API key\"` |\n| 400 | Parameter validation failed | `\"Invalid email address\"` / `\"Subject is required\"` |\n| 500 | Email service error | `\"Failed to send email\"` |\n\n### Worker Example Code\n\n```typescript\nasync function sendEmail(env: Env, to: string, subject: string, html: string): Promise<{ ok: boolean; error?: string }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(`${baseUrl}/api/v4/send_email`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'X-API-Key': env.API_KEY,\n    },\n    body: JSON.stringify({ to, subject, html }),\n  });\n\n  const result = await resp.json() as { code: number; msg: string; data?: { ok?: boolean; error?: string } };\n\n  if (resp.status !== 200 || result.code !== 200) {\n    return { ok: false, error: result.data?.error || result.msg || 'Unknown error' };\n  }\n  return { ok: true };\n}\n\n// Usage in routes\nasync function handleSendVerification(request: Request, env: Env): Promise<Response> {\n  const { email } = await request.json() as { email: string };\n  const code = Math.random().toString().slice(2, 8);\n\n  const result = await sendEmail(env, email, 'Verification Code',\n    `<p>Your code is <strong>${code}</strong></p>`);\n\n  if (!result.ok) {\n    return json({ error: result.error }, 500);\n  }\n  return json({ ok: true });\n}\n```\n\n---\n\n## Error Handling Pattern\n\nPinMe platform API unified response format:\n\n```typescript\ninterface PinmeResponse<T = unknown> {\n  code: number;   // 200=success, other=failure\n  msg: string;    // \"ok\" | \"error\" | \"invalid params\"\n  data?: T;       // Business data on success, may contain { error: string } on failure\n}\n```\n\n### Recommended Unified Error Handler\n\n```typescript\nasync function callPinmeAPI<T>(url: string, apiKey: string, body: unknown): Promise<{ data?: T; error?: string }> {\n  let resp: Response;\n  try {\n    resp = await fetch(url, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },\n      body: JSON.stringify(body),\n    });\n  } catch {\n    return { error: 'Network error' };\n  }\n\n  if (!resp.ok) {\n    try {\n      const err = await resp.json() as PinmeResponse;\n      return { error: err.data && typeof err.data === 'object' && 'error' in err.data\n        ? (err.data as { error: string }).error\n        : err.msg || `HTTP ${resp.status}` };\n    } catch {\n      return { error: `HTTP ${resp.status}` };\n    }\n  }\n\n  const res","createdAt":"2026-09-25T11:51:56.708Z","updatedAt":"2026-09-25T11:51:56.708Z"},{"id":"cmugwhsfk01gtqu06qllnm31q","slug":"glitternetwork-pinme-pinme-llm","name":"pinme-llm","description":"Use this skill when a PinMe project (Worker TypeScript) needs to call OpenRouter-backed LLM APIs, including models, chat/completions, streaming, or OpenRouter web search. Guides AI to generate correct Worker TS code.","authorId":"gh:glitternetwork","authorName":"glitternetwork","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3745,"pricePerCall":0,"manifest":{"name":"pinme-llm","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use this skill when a PinMe project (Worker TypeScript) needs to call OpenRouter-backed LLM APIs, including models, chat/completions, streaming, or OpenRouter web search. Guides AI to generate correct Worker TS code.","permissions":[],"systemPrompt":"# PinMe Worker OpenRouter API Integration\n\nGuides how to call PinMe platform's OpenRouter proxy APIs in a PinMe Worker (TypeScript). Workers use the PinMe project API key; they never hold the real OpenRouter API key.\n\n## Environment Variables\n\nThe following environment variables are automatically injected when the Worker is created — no manual configuration needed:\n\n```typescript\n// backend/src/worker.ts\nexport interface Env {\n  DB: D1Database;\n  API_KEY: string;       // Project API Key from create_worker\n  PROJECT_NAME: string;  // Actual project_name from create_worker; must match API_KEY\n  BASE_URL?: string;     // Optional override for PinMe API base URL, defaults to https://pinme.cloud\n}\n```\n\n> `API_KEY` authenticates the Worker to PinMe. `PROJECT_NAME` is required for `chat/completions` and must belong to the same project as `API_KEY`. When `BASE_URL` is not set, use `https://pinme.cloud`.\n\n---\n\n## Models API\n\n**Endpoint:** `GET {BASE_URL}/api/v1/models`\n**Authentication:** `X-API-Key` header (using `env.API_KEY`)\n**Request Body:** none\n\nUse this when the Worker needs to list available OpenRouter models. The response body, status, and headers are passed through from OpenRouter `/models`.\n\n```typescript\nasync function listModels(env: Env): Promise<unknown> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(`${baseUrl}/api/v1/models`, {\n    headers: { 'X-API-Key': env.API_KEY },\n  });\n\n  if (!resp.ok) {\n    throw new Error(await extractPinmeOpenRouterError(resp));\n  }\n\n  return await resp.json();\n}\n```\n\n---\n\n## Chat Completions API\n\n**Endpoint:** `POST {BASE_URL}/api/v1/chat/completions?project_name={project_name}`\n**Authentication:** `X-API-Key` header (using `env.API_KEY`)\n**Request Body:** OpenRouter chat/completions format, passed through as-is after a 1MB size check\n**Streaming:** Supports SSE (`stream: true`)\n**Web Search:** Supports OpenRouter `openrouter:web_search` server tool via the `tools` array\n\n### Request Format\n\n```json\n{\n  \"model\": \"openai/gpt-4o-mini\",\n  \"messages\": [\n    { \"role\": \"system\", \"content\": \"You are a helpful assistant.\" },\n    { \"role\": \"user\", \"content\": \"Hello!\" }\n  ],\n  \"stream\": true\n}\n```\n\n> Use `env.PROJECT_NAME` from `create_worker`; always URL-encode it in the query string. For available models, call `GET /api/v1/models` or refer to OpenRouter model IDs.\n\n### OpenRouter Web Search\n\nPinMe does not provide a raw search endpoint. To search the web, pass OpenRouter's `openrouter:web_search` server tool to `chat/completions`; the model decides whether and when to search.\n\nAlways set `max_results` and `max_total_results` to keep search volume and cost bounded.\n\n```typescript\nasync function searchWithLLM(env: Env, query: string): Promise<string> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'X-API-Key': env.API_KEY,\n      },\n      body: JSON.stringify({\n        model: 'openai/gpt-5.2',\n        messages: [{ role: 'user', content: query }],\n        tools: [\n          {\n            type: 'openrouter:web_search',\n            parameters: {\n              engine: 'auto',\n              max_results: 5,\n              max_total_results: 10,\n            },\n          },\n        ],\n      }),\n    },\n  );\n\n  if (!resp.ok) {\n    throw new Error(await extractPinmeOpenRouterError(resp));\n  }\n\n  const data = await resp.json() as { choices: Array<{ message?: { content?: string } }> };\n  return data.choices[0]?.message?.content ?? '';\n}\n```\n\n### Response Format\n\nSuccessful requests return OpenRouter's raw response body.\n\n**Non-streaming Success (200):**\n```json\n{\n  \"id\": \"chatcmpl-...\",\n  \"choices\": [{ \"message\": { \"role\": \"assistant\", \"content\": \"Hello!\" }, \"finish_reason\": \"stop\" }],\n  \"usage\": { \"prompt_tokens\": 10, \"completion_tokens\": 5, \"total_tokens\": 15 }\n}\n```\n\n**Streaming Success (200):** SSE format\n```\ndata: {\"choices\":[{\"delta\":{\"content\":\"Hello\"}}]}\ndata: {\"choices\":[{\"delta\":{\"content\":\" there\"}}]}\ndata: [DONE]\n```\n\n**Errors:**\n\n| HTTP Status | Meaning | data.error Example |\n|-------------|---------|-------------------|\n| 401 | API Key missing, invalid, or mismatched with project_name | `\"X-API-Key header is required\"` / `\"Invalid API key\"` / `\"Invalid API key or project name\"` |\n| 400 | project_name missing or OpenRouter key not configured | `\"project_name is required\"` / `\"LLM service not configured for this project\"` |\n| 403 | LLM balance insufficient or disabled | `\"Insufficient balance, please recharge to continue using LLM service\"` |\n| 413 | Request body exceeds 1MB | `\"Request body too large (max 1MB)\"` |\n| 500 | Proxy failed before upstream request | `\"Failed to build request\"` |\n| 502 | LLM service unavailable | `\"LLM service unavailable\"` |\n\nIf OpenRouter receives the request and returns a 4xx/5xx, PinMe passes through OpenRouter's status, headers, and response body instead of wrapping it.\n\n### Worker Example Code — Non-streaming\n\n```typescript\nasync function callLLM(\n  env: Env,\n  messages: Array<{ role: string; content: string }>,\n  model = 'openai/gpt-4o-mini',\n): Promise<{ content: string; error?: string }> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'X-API-Key': env.API_KEY,\n      },\n      body: JSON.stringify({ model, messages }),\n    },\n  );\n\n  if (!resp.ok) {\n    return { content: '', error: await extractPinmeOpenRouterError(resp) };\n  }\n\n  const data = await resp.json() as { choices: Array<{ message: { content: string } }> };\n  return { content: data.choices[0]?.message?.content || '' };\n}\n\n// Usage in routes\nasync function handleChat(request: Request, env: Env): Promise<Response> {\n  const { question } = await request.json() as { question: string };\n\n  const result = await callLLM(env, [\n    { role: 'system', content: 'You are a helpful assistant.' },\n    { role: 'user', content: question },\n  ]);\n\n  if (result.error) {\n    return json({ error: result.error }, 502);\n  }\n  return json({ answer: result.content });\n}\n```\n\n### Worker Example Code — Streaming (SSE Passthrough)\n\n```typescript\nasync function handleChatStream(request: Request, env: Env): Promise<Response> {\n  const body = await request.text();\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n\n  // Ensure stream=true in the request\n  let parsed = JSON.parse(body);\n  parsed.stream = true;\n\n  const resp = await fetch(\n    `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'X-API-Key': env.API_KEY,\n      },\n      body: JSON.stringify(parsed),\n    },\n  );\n\n  if (!resp.ok) {\n    return json({ error: await extractPinmeOpenRouterError(resp) }, resp.status);\n  }\n\n  // Pass through SSE stream directly\n  return new Response(resp.body, {\n    status: 200,\n    headers: {\n      'Content-Type': 'text/event-stream',\n      'Cache-Control': 'no-cache',\n      'Connection': 'keep-alive',\n      ...CORS_HEADERS,\n    },\n  });\n}\n```\n\n### Frontend SSE Stream Consumer Example\n\n```typescript\nasync function streamChat(question: string, onChunk: (text: string) => void): Promise<void> {\n  const resp = await fetch(getApiUrl('/api/chat/stream'), {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ question }),\n  });\n\n  const reader = resp.body!.getReader();\n  const decoder = new TextDecoder();\n  let buffer = '';\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n\n    buffer += decoder.decode(value, { stream: true });\n    const lines = buffer.split('\\n');\n    buffer = lines.pop()!; // Keep incomplete line\n\n    for (const line of lines) {\n      if (!line.startsWith('data: ')) continue;\n      const payload = line.slice(6);\n      if (payload === '[DONE]') return;\n\n      const chunk = JSON.parse(payload) as { choices: Array<{ delta: { content?: string } }> };\n      const content = chunk.choices[0]?.delta?.content;\n      if (content) onChunk(content);\n    }\n  }\n}\n```\n\n---\n\n## Error Handling Pattern\n\nFor `/api/v1/models` and `/api/v1/chat/completions`, successful responses are raw OpenRouter responses. Proxy failures before the OpenRouter request use PinMe's wrapped error format:\n\n```typescript\ninterface PinmeResponse<T = unknown> {\n  code: number;   // 200=success, other=failure\n  msg: string;    // \"ok\" | \"error\" | \"invalid params\"\n  data?: T;       // Business data on success, may contain { error: string } on failure\n}\n```\n\n### Recommended Error Extractor\n\n```typescript\nasync function extractPinmeOpenRouterError(resp: Response): Promise<string> {\n  const fallback = `HTTP ${resp.status}`;\n  try {\n    const body = await resp.clone().json() as PinmeResponse | { error?: { message?: string } } | { error?: string };\n    if ('data' in body && body.data && typeof body.data === 'object' && 'error' in body.data) {\n      return String((body.data as { error: unknown }).error);\n    }\n    if ('msg' in body && typeof body.msg === 'string' && body.msg) {\n      return body.msg;\n    }\n    if ('error' in body) {\n      const error = body.error;\n      if (typeof error === 'string') return error;\n      if (error && typeof error === 'object' && 'message' in error) {\n        return String((error as { message: unknown }).message);\n      }\n    }\n  } catch {\n    try {\n      const text = await resp.text();\n      if (text) return text;\n    } catch {\n      // Ignore and return fallback below.\n    }\n  }\n  return fallback;\n}\n```\n\n### Optional JSON Helper\n\nUse this helper for non-streaming `POST` calls. It returns the raw OpenRouter JSON on success.\n\n```typescript\nasync function callOpenRouterJSON<T>(url: string, apiKey: string, body: unknown): Promise<{ data?: T; error?: string }> {\n  let resp: Response;\n  try {\n    resp = await fetch(url, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },\n      body: JSON.stringify(body),\n    });\n  } catch {\n    return { error: 'Network error' };\n  }\n\n  if (!resp.ok) {\n    return { error: await extractPinmeOpenRouterError(resp) };\n  }\n\n  return { data: await resp.json() as T };\n}\n```\n\n### Usage Example\n\n```typescript\nconst baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n\n// Call LLM (non-streaming)\nconst llmResult = await callOpenRouterJSON<{ choices: Array<{ message: { content: string } }> }>(\n  `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`, env.API_KEY,\n  { model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] },\n);\nif (llmResult.error) return json({ error: llmResult.error }, 502);\n```","schemaVersion":1},"repoUrl":"https://github.com/glitternetwork/pinme/tree/main/skills/pinme-llm","tags":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"pinme","audit":{"files":["package-lock.json","package.json","pnpm-lock.yaml"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS).","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-7q85-xj36-vmfc · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip extraction follows destination symlinks, allowing arbitrary file overwrite.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-vwc7-r8mq-g2x9 · npm:adm-zip@0.5.17","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Crafted ZIP file triggers 4GB memory allocation.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-xcpc-8h2w-3j85 · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"uuid@9.0.1 has a known vulnerability: uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-w5hq-g745-h8pq · npm:uuid@9.0.1","severity":"medium"}],"packages":16,"auditedAt":"2026-09-25T11:51:56.672Z","lockfiles":["package-lock.json","pnpm-lock.yaml"]},"forks":277,"owner":"glitternetwork","stars":3745,"topics":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy","static-site-hosting","web-hosting","zero-configuration"],"license":"MIT","fullName":"glitternetwork/pinme","homepage":"https://pinme.eth.limo","language":"TypeScript","pushedAt":"2026-09-12T05:23:49Z","avatarUrl":"https://avatars.githubusercontent.com/u/102277171?v=4","crawledAt":"2026-09-25T11:51:44.477Z","openIssues":7,"manifestFile":"SKILL.md","manifestPath":"skills/pinme-llm/SKILL.md","defaultBranch":"main"},"readme":"# PinMe Worker OpenRouter API Integration\n\nGuides how to call PinMe platform's OpenRouter proxy APIs in a PinMe Worker (TypeScript). Workers use the PinMe project API key; they never hold the real OpenRouter API key.\n\n## Environment Variables\n\nThe following environment variables are automatically injected when the Worker is created — no manual configuration needed:\n\n```typescript\n// backend/src/worker.ts\nexport interface Env {\n  DB: D1Database;\n  API_KEY: string;       // Project API Key from create_worker\n  PROJECT_NAME: string;  // Actual project_name from create_worker; must match API_KEY\n  BASE_URL?: string;     // Optional override for PinMe API base URL, defaults to https://pinme.cloud\n}\n```\n\n> `API_KEY` authenticates the Worker to PinMe. `PROJECT_NAME` is required for `chat/completions` and must belong to the same project as `API_KEY`. When `BASE_URL` is not set, use `https://pinme.cloud`.\n\n---\n\n## Models API\n\n**Endpoint:** `GET {BASE_URL}/api/v1/models`\n**Authentication:** `X-API-Key` header (using `env.API_KEY`)\n**Request Body:** none\n\nUse this when the Worker needs to list available OpenRouter models. The response body, status, and headers are passed through from OpenRouter `/models`.\n\n```typescript\nasync function listModels(env: Env): Promise<unknown> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(`${baseUrl}/api/v1/models`, {\n    headers: { 'X-API-Key': env.API_KEY },\n  });\n\n  if (!resp.ok) {\n    throw new Error(await extractPinmeOpenRouterError(resp));\n  }\n\n  return await resp.json();\n}\n```\n\n---\n\n## Chat Completions API\n\n**Endpoint:** `POST {BASE_URL}/api/v1/chat/completions?project_name={project_name}`\n**Authentication:** `X-API-Key` header (using `env.API_KEY`)\n**Request Body:** OpenRouter chat/completions format, passed through as-is after a 1MB size check\n**Streaming:** Supports SSE (`stream: true`)\n**Web Search:** Supports OpenRouter `openrouter:web_search` server tool via the `tools` array\n\n### Request Format\n\n```json\n{\n  \"model\": \"openai/gpt-4o-mini\",\n  \"messages\": [\n    { \"role\": \"system\", \"content\": \"You are a helpful assistant.\" },\n    { \"role\": \"user\", \"content\": \"Hello!\" }\n  ],\n  \"stream\": true\n}\n```\n\n> Use `env.PROJECT_NAME` from `create_worker`; always URL-encode it in the query string. For available models, call `GET /api/v1/models` or refer to OpenRouter model IDs.\n\n### OpenRouter Web Search\n\nPinMe does not provide a raw search endpoint. To search the web, pass OpenRouter's `openrouter:web_search` server tool to `chat/completions`; the model decides whether and when to search.\n\nAlways set `max_results` and `max_total_results` to keep search volume and cost bounded.\n\n```typescript\nasync function searchWithLLM(env: Env, query: string): Promise<string> {\n  const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';\n  const resp = await fetch(\n    `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'X-API-Key': env.API_KEY,\n      },\n      body: JSON.stringify({\n        model: 'openai/gpt-5.2',\n        messages: [{ role: 'user', content: query }],\n        tools: [\n          {\n            type: 'openrouter:web_search',\n            parameters: {\n              engine: 'auto',\n              max_results: 5,\n              max_total_results: 10,\n            },\n          },\n        ],\n      }),\n    },\n  );\n\n  if (!resp.ok) {\n    throw new Error(await extractPinmeOpenRouterError(resp));\n  }\n\n  const data = await resp.json() as { choices: Array<{ message?: { content?: string } }> };\n  return data.choices[0]?.message?.content ?? '';\n}\n```\n\n### Response Format\n\nSuccessful requests return OpenRouter's raw response body.\n\n**Non-streaming Success (200):**\n```json\n{\n  \"id\": \"chatcmpl-...\",\n  \"choices\": [{ \"message\": { \"role\": \"assistant\", \"content\": \"Hello!\" }, \"finish_reason\": \"stop\" }],\n  \"usage\": { \"prompt_tokens\": 10, \"completion_tokens\": 5, \"total","createdAt":"2026-09-25T11:51:56.720Z","updatedAt":"2026-09-25T11:51:56.720Z"},{"id":"cmugwhsfv01gwqu06caq63e28","slug":"glitternetwork-pinme-pinme-r2","name":"pinme-r2","description":"Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination. Guides AI to use PinMe's automatically injected env.R2 binding without R2 credentials or manual Wrangler configuration.","authorId":"gh:glitternetwork","authorName":"glitternetwork","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3745,"pricePerCall":0,"manifest":{"name":"pinme-r2","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination. Guides AI to use PinMe's automatically injected env.R2 binding without R2 credentials or manual Wrangler configuration.","permissions":[],"systemPrompt":"# PinMe Worker R2 Storage\n\nUse the project-scoped R2 bucket that PinMe binds to every deployed Worker as `env.R2`. Do not create credentials, choose a bucket name, or edit generated Wrangler configuration.\n\n## Runtime Contract\n\nPinMe rebuilds trusted Worker metadata on create, save, and update. Client metadata cannot replace the R2 binding.\n\n| Binding | TypeScript type | Availability |\n| --- | --- | --- |\n| `DB` | `D1Database` | Always injected |\n| `R2` | `R2Bucket` | Always injected; current project's bucket |\n| `API_KEY` | `string` | Always injected |\n| `LLM_API_KEY` | `string` | Always injected |\n| `BASE_URL` | `string` | Always injected |\n| `WORKER_URL` | `string` | Always injected |\n| `PROJECT_NAME` | `string` | Always injected |\n\nPayment-specific bindings such as `UNIWEB_SECRET` are conditional and unrelated to R2 access.\n\nDeclare only the bindings used by the Worker module. R2 code normally starts with:\n\n```typescript\nexport interface Env {\n  R2: R2Bucket;\n  PROJECT_NAME: string;\n  WORKER_URL: string;\n}\n```\n\nWhen the same module coordinates file metadata in D1, also declare `DB: D1Database` as a required field.\n\n## Choose R2 or D1\n\n- Use R2 for file bodies, images, attachments, media, exports, and other objects addressed by key.\n- Use D1 for searchable business metadata, ownership, relations, status, and audit fields.\n- For managed files, store the body in R2 and store only its key and business metadata in D1.\n- Never use Worker local filesystem state for persistence and never store complete files or base64 payloads in D1.\n\n## Required Security Workflow\n\nApply this sequence to every upload, download, metadata, delete, and list route:\n\n```text\nauthenticate request\n→ authorize the project/user action\n→ validate size and media policy\n→ generate or normalize a scoped object key\n→ call env.R2\n→ return a sanitized response\n```\n\nUse the application's existing authentication. The examples below accept a trusted `userId` that the route must obtain from verified identity claims, never from an untrusted request body or query parameter.\n\nKeep object keys server-controlled. Prefer opaque IDs under an owner prefix:\n\n```typescript\nconst FILE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\nfunction ownerPrefix(userId: string): string {\n  if (!userId) throw new Error('Authenticated user id is required');\n  return `users/${encodeURIComponent(userId)}/files/`;\n}\n\nfunction objectKey(userId: string, fileId: string): string {\n  if (!FILE_ID_RE.test(fileId)) throw new Error('Invalid file id');\n  return `${ownerPrefix(userId)}${fileId}`;\n}\n```\n\nNever accept a complete object key from the client. Reject empty identifiers, `.` or `..` segments, backslashes, control characters, and any attempt to access another user's prefix.\n\n## Shared Helpers\n\nUse small helpers and explicit business limits. Adapt the allowlist to the product rather than accepting every client-supplied media type.\n\n```typescript\nconst MAX_UPLOAD_BYTES = 25 * 1024 * 1024;\nconst ALLOWED_CONTENT_TYPES = new Set([\n  'image/jpeg',\n  'image/png',\n  'image/webp',\n  'application/pdf',\n]);\n\nfunction json(data: unknown, status = 200): Response {\n  return Response.json(data, { status });\n}\n\nfunction safeDownloadName(value: string | null): string {\n  const cleaned = (value || 'download')\n    .replace(/[\\r\\n\"\\\\]/g, '_')\n    .replace(/[\\x00-\\x1f\\x7f]/g, '')\n    .trim();\n  return (cleaned || 'download').slice(0, 128);\n}\n\nfunction requestedFileId(request: Request): string | null {\n  const url = new URL(request.url);\n  const value = url.pathname.split('/').filter(Boolean).at(-1) || '';\n  return FILE_ID_RE.test(value) ? value : null;\n}\n```\n\nClient filenames and `Content-Type` are hints, not proof of content. For sensitive formats, inspect magic bytes or send the object through an asynchronous validation/scanning workflow before marking it ready.\n\n## Stream an Upload\n\nRequire authentication before calling this handler. Pass `request.body` directly to R2; do not call `arrayBuffer()`, `text()`, `json()`, `formData()`, or base64 conversion first.\n\n```typescript\nasync function handleUpload(\n  request: Request,\n  env: Env,\n  userId: string,\n): Promise<Response> {\n  if (!request.body) return json({ error: 'File body is required' }, 400);\n\n  const lengthHeader = request.headers.get('content-length');\n  if (!lengthHeader) return json({ error: 'Content-Length is required' }, 411);\n\n  const declaredSize = Number(lengthHeader);\n  if (!Number.isSafeInteger(declaredSize) || declaredSize < 0) {\n    return json({ error: 'Invalid Content-Length' }, 400);\n  }\n  if (declaredSize > MAX_UPLOAD_BYTES) {\n    return json({ error: 'File is too large' }, 413);\n  }\n\n  const contentType = (request.headers.get('content-type') || '')\n    .split(';', 1)[0]\n    .trim()\n    .toLowerCase();\n  if (!ALLOWED_CONTENT_TYPES.has(contentType)) {\n    return json({ error: 'Unsupported media type' }, 400);\n  }\n\n  const fileId = crypto.randomUUID();\n  const key = objectKey(userId, fileId);\n  const filename = safeDownloadName(request.headers.get('x-file-name'));\n\n  const object = await env.R2.put(key, request.body, {\n    httpMetadata: {\n      contentType,\n      contentDisposition: `attachment; filename=\"${filename}\"`,\n    },\n    customMetadata: { ownerId: userId },\n  });\n\n  if (object === null) return json({ error: 'Upload precondition failed' }, 412);\n\n  // Content-Length is only a precheck. Enforce the actual stored size too.\n  if (object.size > MAX_UPLOAD_BYTES) {\n    await env.R2.delete(key);\n    return json({ error: 'File is too large' }, 413);\n  }\n\n  return json({ id: fileId, size: object.size, etag: object.httpEtag }, 201);\n}\n```\n\nDo not return the bucket name or internal object-key layout. Return an opaque file ID that later routes resolve under the authenticated owner's prefix.\n\n## Stream a Download\n\nValidate a single Range header before passing it to R2. R2 may return `null` when the object does not exist, or metadata without a body when a conditional request fails.\n\n```typescript\nfunction validRangeHeader(value: string | null): boolean {\n  if (!value) return true;\n  const match = /^bytes=(\\d*)-(\\d*)$/.exec(value);\n  return Boolean(match && (match[1] || match[2]));\n}\n\nasync function handleDownload(\n  request: Request,\n  env: Env,\n  userId: string,\n): Promise<Response> {\n  const fileId = requestedFileId(request);\n  if (!fileId) return json({ error: 'Invalid file id' }, 400);\n  if (!validRangeHeader(request.headers.get('range'))) {\n    return json({ error: 'Invalid Range header' }, 416);\n  }\n\n  const object = await env.R2.get(objectKey(userId, fileId), {\n    onlyIf: request.headers,\n    range: request.headers,\n  });\n  if (object === null) return json({ error: 'Not found' }, 404);\n  if (!('body' in object)) return new Response(null, { status: 412 });\n\n  const headers = new Headers();\n  object.writeHttpMetadata(headers);\n  headers.set('etag', object.httpEtag);\n  headers.set('accept-ranges', 'bytes');\n  if (object.range) {\n    const { offset, length } = object.range;\n    headers.set(\n      'content-range',\n      `bytes ${offset}-${offset + length - 1}/${object.size}`,\n    );\n    headers.set('content-length', String(length));\n  } else {\n    headers.set('content-length', String(object.size));\n  }\n\n  return new Response(object.body, {\n    status: object.range ? 206 : 200,\n    headers,\n  });\n}\n```\n\nFor routes backed by D1 metadata, authorize the D1 row's owner before calling `env.R2.get`. Do not infer ownership only from a client-provided path.\n\n## Read Metadata with HEAD\n\n```typescript\nasync function handleHead(\n  request: Request,\n  env: Env,\n  userId: string,\n): Promise<Response> {\n  const fileId = requestedFileId(request);\n  if (!fileId) return json({ error: 'Invalid file id' }, 400);\n\n  const object = await env.R2.head(objectKey(userId, fileId));\n  if (object === null) return new Response(null, { status: 404 });\n\n  const headers = new Headers();\n  object.writeHttpMetadata(headers);\n  headers.set('etag', object.httpEtag);\n  headers.set('content-length', String(object.size));\n  return new Response(null, { status: 200, headers });\n}\n```\n\nUse `head()` when only size, ETag, upload time, or metadata is needed. Do not download the body to answer metadata requests.\n\n## Delete an Object\n\n```typescript\nasync function handleDelete(\n  request: Request,\n  env: Env,\n  userId: string,\n): Promise<Response> {\n  const fileId = requestedFileId(request);\n  if (!fileId) return json({ error: 'Invalid file id' }, 400);\n\n  const key = objectKey(userId, fileId);\n  const object = await env.R2.head(key);\n  if (object === null) return json({ error: 'Not found' }, 404);\n\n  await env.R2.delete(key);\n  return new Response(null, { status: 204 });\n}\n```\n\nR2 can delete up to 1000 keys in one `delete([...keys])` call. Batch deletion must still derive and authorize every key server-side.\n\n## List an Owner's Objects\n\nNever list the whole bucket for an end-user request. Derive the prefix from verified identity and treat the cursor as opaque.\n\n```typescript\nasync function handleList(\n  request: Request,\n  env: Env,\n  userId: string,\n): Promise<Response> {\n  const cursor = new URL(request.url).searchParams.get('cursor');\n  if (cursor && cursor.length > 2048) {\n    return json({ error: 'Invalid cursor' }, 400);\n  }\n\n  const page = await env.R2.list({\n    prefix: ownerPrefix(userId),\n    cursor: cursor || undefined,\n    limit: 100,\n    include: ['httpMetadata', 'customMetadata'],\n  });\n\n  return json({\n    objects: page.objects.map((object) => ({\n      id: object.key.slice(ownerPrefix(userId).length),\n      size: object.size,\n      uploaded: object.uploaded.toISOString(),\n      etag: object.httpEtag,\n      contentType: object.httpMetadata?.contentType,\n    })),\n    nextCursor: page.truncated ? page.cursor : null,\n  });\n}\n```\n\nAn R2 list call returns at most 1000 entries and may return fewer than the requested limit when metadata is included. Continue only when `page.truncated` is true; never use `objects.length === limit` as the pagination condition.\n\n## Route and Error Semantics\n\nAuthenticate once in the router, derive a trusted `userId`, then pass it to the handlers. Return an `Allow` header for unsupported methods.\n\n| Status | Meaning |\n| --- | --- |\n| 400 | Invalid file ID, body, cursor, or media type |\n| 401 | Missing or invalid authentication |\n| 403 | Authenticated but not allowed to access the object |\n| 404 | Object or owned metadata record not found |\n| 411 | A capped upload route requires `Content-Length` but it is absent |\n| 412 | Conditional R2 operation failed |\n| 413 | Business or platform upload limit exceeded |\n| 416 | Invalid or unsatisfiable Range request |\n| 500 | Sanitized internal storage failure |\n\nCatch storage failures at the route boundary, log only non-sensitive context, and return a generic error. Never return a raw provider error, bucket name, credential, or internal object key.\nTranslate a valid-but-unsatisfiable R2 Range failure to `416` without returning the provider error text.\n\n## Coordinate R2 with D1\n\nR2 and D1 do not share a transaction. Use an explicit state transition when business metadata is required:\n\n```text\ninsert D1 row with status=pending\n→ stream body to R2\n→ update D1 row to status=ready\n```\n\n- If upload fails, delete the pending row or mark it failed.\n- If the final D1 update fails, delete the newly uploaded object or retain a durable pending state for a compensation job.\n- Store at least: public file ID, internal object key, owner ID, original name, size, MIME, status, and timestamps.\n- For download and delete, load the row by public file ID and owner ID before touching R2.\n- Delete the R2 object and D1 row with an explicit retry/compensation policy; do not pretend the two operations are atomic.\n\n## Large Files\n\nUse `request.body → env.R2.put` for small and medium uploads. Streaming avoids Worker memory amplification but does not bypass the Cloudflare request-body limit for the account plan.\n\nUse multipart only when the object exceeds that request limit or resumability is an explicit product requirement. A multipart API must:\n\n- authenticate every create, upload-part, complete, resume, and abort action;\n- bind the object key and upload ID to an owner in durable state;\n- validate part number, part size, total size, and declared content type;\n- make completion idempotent and abort stale uploads;\n- avoid accepting an arbitrary key or upload ID from an untrusted client.\n\nDo not generate a public multipart controller by default. Multipart state and security are substantially more complex than a single streaming upload.\n\n## Local Development\n\n- Do not edit PinMe-generated `backend/wrangler.toml` to add an R2 binding.\n- Unit-test key generation, authorization, routing, and failure handling with a narrow `R2Bucket` mock.\n- Verify real metadata, Range, conditional requests, and streaming after `pinme update-worker` or `pinme save`.\n- Treat the mock as a logic test, not proof of production R2 behavior.\n\n## Anti-Patterns\n\n| Do not | Use instead |\n| --- | --- |\n| Expose an unauthenticated upload route | Authenticate and authorize before every mutation |\n| Accept a complete object key from the client | Generate an opaque ID under a server-derived owner prefix |\n| Trust a user ID from JSON or query parameters | Derive identity from verified claims |\n| Read a large body into an ArrayBuffer or base64 string | Stream `request.body` directly into `env.R2.put` |\n| Store file bodies or base64 in D1 | Store bodies in R2 and searchable metadata in D1 |\n| List the whole bucket | Restrict with an owner prefix and paginate |\n| Stop pagination based on returned object count | Check `page.truncated` and return `page.cursor` |\n| Drop response metadata | Apply `writeHttpMetadata`, `httpEtag`, length, and Range headers |\n| Persist with `fs` or local directories | Use the injected R2 binding |\n| Add R2 keys or secrets to source/config | Use `env.R2`; PinMe owns the binding |\n| Edit generated Wrangler binding configuration | Deploy through `pinme save` or `pinme update-worker` |","schemaVersion":1},"repoUrl":"https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2","tags":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"pinme","audit":{"files":["package-lock.json","package.json","pnpm-lock.yaml"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS).","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-7q85-xj36-vmfc · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip extraction follows destination symlinks, allowing arbitrary file overwrite.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-vwc7-r8mq-g2x9 · npm:adm-zip@0.5.17","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Crafted ZIP file triggers 4GB memory allocation.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-xcpc-8h2w-3j85 · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"uuid@9.0.1 has a known vulnerability: uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-w5hq-g745-h8pq · npm:uuid@9.0.1","severity":"medium"}],"packages":16,"auditedAt":"2026-09-25T11:51:56.672Z","lockfiles":["package-lock.json","pnpm-lock.yaml"]},"forks":277,"owner":"glitternetwork","stars":3745,"topics":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy","static-site-hosting","web-hosting","zero-configuration"],"license":"MIT","fullName":"glitternetwork/pinme","homepage":"https://pinme.eth.limo","language":"TypeScript","pushedAt":"2026-09-12T05:23:49Z","avatarUrl":"https://avatars.githubusercontent.com/u/102277171?v=4","crawledAt":"2026-09-25T11:51:44.477Z","openIssues":7,"manifestFile":"SKILL.md","manifestPath":"skills/pinme-r2/SKILL.md","defaultBranch":"main"},"readme":"# PinMe Worker R2 Storage\n\nUse the project-scoped R2 bucket that PinMe binds to every deployed Worker as `env.R2`. Do not create credentials, choose a bucket name, or edit generated Wrangler configuration.\n\n## Runtime Contract\n\nPinMe rebuilds trusted Worker metadata on create, save, and update. Client metadata cannot replace the R2 binding.\n\n| Binding | TypeScript type | Availability |\n| --- | --- | --- |\n| `DB` | `D1Database` | Always injected |\n| `R2` | `R2Bucket` | Always injected; current project's bucket |\n| `API_KEY` | `string` | Always injected |\n| `LLM_API_KEY` | `string` | Always injected |\n| `BASE_URL` | `string` | Always injected |\n| `WORKER_URL` | `string` | Always injected |\n| `PROJECT_NAME` | `string` | Always injected |\n\nPayment-specific bindings such as `UNIWEB_SECRET` are conditional and unrelated to R2 access.\n\nDeclare only the bindings used by the Worker module. R2 code normally starts with:\n\n```typescript\nexport interface Env {\n  R2: R2Bucket;\n  PROJECT_NAME: string;\n  WORKER_URL: string;\n}\n```\n\nWhen the same module coordinates file metadata in D1, also declare `DB: D1Database` as a required field.\n\n## Choose R2 or D1\n\n- Use R2 for file bodies, images, attachments, media, exports, and other objects addressed by key.\n- Use D1 for searchable business metadata, ownership, relations, status, and audit fields.\n- For managed files, store the body in R2 and store only its key and business metadata in D1.\n- Never use Worker local filesystem state for persistence and never store complete files or base64 payloads in D1.\n\n## Required Security Workflow\n\nApply this sequence to every upload, download, metadata, delete, and list route:\n\n```text\nauthenticate request\n→ authorize the project/user action\n→ validate size and media policy\n→ generate or normalize a scoped object key\n→ call env.R2\n→ return a sanitized response\n```\n\nUse the application's existing authentication. The examples below accept a trusted `userId` that the route must obtain from verified identity claims, never from an untrusted request body or query parameter.\n\nKeep object keys server-controlled. Prefer opaque IDs under an owner prefix:\n\n```typescript\nconst FILE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\nfunction ownerPrefix(userId: string): string {\n  if (!userId) throw new Error('Authenticated user id is required');\n  return `users/${encodeURIComponent(userId)}/files/`;\n}\n\nfunction objectKey(userId: string, fileId: string): string {\n  if (!FILE_ID_RE.test(fileId)) throw new Error('Invalid file id');\n  return `${ownerPrefix(userId)}${fileId}`;\n}\n```\n\nNever accept a complete object key from the client. Reject empty identifiers, `.` or `..` segments, backslashes, control characters, and any attempt to access another user's prefix.\n\n## Shared Helpers\n\nUse small helpers and explicit business limits. Adapt the allowlist to the product rather than accepting every client-supplied media type.\n\n```typescript\nconst MAX_UPLOAD_BYTES = 25 * 1024 * 1024;\nconst ALLOWED_CONTENT_TYPES = new Set([\n  'image/jpeg',\n  'image/png',\n  'image/webp',\n  'application/pdf',\n]);\n\nfunction json(data: unknown, status = 200): Response {\n  return Response.json(data, { status });\n}\n\nfunction safeDownloadName(value: string | null): string {\n  const cleaned = (value || 'download')\n    .replace(/[\\r\\n\"\\\\]/g, '_')\n    .replace(/[\\x00-\\x1f\\x7f]/g, '')\n    .trim();\n  return (cleaned || 'download').slice(0, 128);\n}\n\nfunction requestedFileId(request: Request): string | null {\n  const url = new URL(request.url);\n  const value = url.pathname.split('/').filter(Boolean).at(-1) || '';\n  return FILE_ID_RE.test(value) ? value : null;\n}\n```\n\nClient filenames and `Content-Type` are hints, not proof of content. For sensitive formats, inspect magic bytes or send the object through an asynchronous validation/scanning workflow before marking it ready.\n\n## Stream an Upload\n\nRequire authentication before calling this handler. Pass `request.body` directly to R2; do not c","createdAt":"2026-09-25T11:51:56.731Z","updatedAt":"2026-09-25T11:51:56.731Z"},{"id":"cmugwhsg601gzqu06q48km47z","slug":"glitternetwork-pinme-pinme-share","name":"pinme-share","description":"Use this skill when the user wants to share, publish, or upload a static result through PinMe, especially by generating a static HTML share page for a PinMe project link, deployed full-stack app, Codex conversation summary, report, file, demo, or any 分享/发布/上传分享页 request that should end with `pinme upload`.","authorId":"gh:glitternetwork","authorName":"glitternetwork","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3745,"pricePerCall":0,"manifest":{"name":"pinme-share","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use this skill when the user wants to share, publish, or upload a static result through PinMe, especially by generating a static HTML share page for a PinMe project link, deployed full-stack app, Codex conversation summary, report, file, demo, or any 分享/发布/上传分享页 request that should end with `pinme upload`.","permissions":[],"systemPrompt":"# PinMe Share\n\nCreate a polished static share artifact, upload it with `pinme upload`, and return the final URL.\n\n## When to Use\n\nUse this skill when the user asks to:\n\n- Share or publish a result using PinMe.\n- Create a static page that wraps a deployed project link, demo, report, or artifact.\n- Summarize a Codex conversation/session and share it as a page.\n- Turn a project handoff into a public landing or summary page.\n- Upload an existing static file or folder for lightweight distribution.\n\nIf the user needs a backend, database, auth, email, or LLM functionality, use the main `pinme` skill and any relevant PinMe integration skill first. Use `pinme-share` at the end to package and publish the result.\n\n## Core Workflow\n\n1. Identify what is being shared:\n   - **PinMe/full-stack project**: deployed URL, short description, key features, tech stack, usage notes.\n   - **Codex conversation**: goal, decisions, implementation summary, important outputs, next steps.\n   - **Static file/report/demo**: title, purpose, file contents or preview, context for the recipient.\n2. Create a static share artifact:\n   - Prefer a single self-contained `index.html`.\n   - Use `share/<slug>/index.html` in the current workspace unless the repo has an existing output/share convention.\n   - Keep CSS inline for portability.\n   - Do not require JavaScript unless interaction is valuable.\n3. Sanitize before publishing:\n   - Remove secrets, tokens, API keys, `.env` values, internal-only URLs, private user data, and unrelated logs.\n   - For conversation summaries, summarize rather than dumping raw transcript unless the user explicitly asks for verbatim sharing.\n   - Make links explicit and clickable.\n4. Upload with PinMe:\n   ```bash\n   pinme upload share/<slug>\n   ```\n5. Return the URL printed by PinMe. If PinMe outputs multiple URLs, prefer DNS domain, then PinMe subdomain, then short URL, then full preview URL. Never truncate hash fragments.\n\n## Share Page Content\n\nFor a project share page, include:\n\n- Project name and one-sentence description.\n- Primary launch/demo link as the first action.\n- What it does, who it is for, and why it matters.\n- Feature list focused on user-visible behavior.\n- Build/deploy details only when useful to the recipient.\n- Date and provenance such as \"Created with Codex\" only if appropriate.\n\nFor a conversation share page, include:\n\n- Conversation title.\n- Initial goal or question.\n- Key context and constraints.\n- Decisions made.\n- Work completed or answer summary.\n- Files changed, commands run, links produced, or artifacts created when relevant.\n- Follow-up items.\n\nFor a file/report share page, include:\n\n- Clear title and short abstract.\n- Download/open link to the uploaded artifact if there is a separate file.\n- Important excerpts or generated summary.\n- Source/context notes.\n\n## HTML Guidelines\n\n- Build a real share page, not a generic placeholder.\n- Make the most important link visible in the first viewport.\n- Use clean, responsive HTML/CSS that works as a standalone static file.\n- Keep the design restrained and readable; avoid overdecorated marketing layouts for technical handoffs.\n- Use semantic sections, accessible contrast, descriptive link text, and sensible mobile spacing.\n- Escape user-provided text before inserting it into HTML.\n- If showing code or command output, wrap it in `<pre><code>` and keep it short.\n\n## PinMe Upload Checklist\n\nBefore upload:\n\n```bash\npinme --version\n```\n\nIf PinMe is missing or stale, install or update it according to the main `pinme` skill. Authentication is required for upload:\n\n```bash\npinme login\n# or: pinme set-appkey <AppKey>\n```\n\nUpload examples:\n\n```bash\npinme upload share/my-project\npinme upload share/conversation-summary\npinme upload ./report.html\npinme upload ./dist\n```\n\nDo not upload:\n\n- `.env`, `.git`, `node_modules`, source trees, private datasets, raw logs with credentials, or unrelated build cache.\n- Raw conversation transcripts that may include secrets or private context unless the user explicitly approves the exact content.\n\n## Final Response\n\nTell the user:\n\n- What share artifact was created.\n- The PinMe URL returned by upload.\n- Any important caveat, such as skipped upload because PinMe was not authenticated or unavailable.\n\nKeep the response short. The URL is the main deliverable.","schemaVersion":1},"repoUrl":"https://github.com/glitternetwork/pinme/tree/main/skills/pinme-share","tags":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"pinme","audit":{"files":["package-lock.json","package.json","pnpm-lock.yaml"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS).","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-7q85-xj36-vmfc · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip extraction follows destination symlinks, allowing arbitrary file overwrite.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-vwc7-r8mq-g2x9 · npm:adm-zip@0.5.17","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Crafted ZIP file triggers 4GB memory allocation.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-xcpc-8h2w-3j85 · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"uuid@9.0.1 has a known vulnerability: uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-w5hq-g745-h8pq · npm:uuid@9.0.1","severity":"medium"}],"packages":16,"auditedAt":"2026-09-25T11:51:56.672Z","lockfiles":["package-lock.json","pnpm-lock.yaml"]},"forks":277,"owner":"glitternetwork","stars":3745,"topics":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy","static-site-hosting","web-hosting","zero-configuration"],"license":"MIT","fullName":"glitternetwork/pinme","homepage":"https://pinme.eth.limo","language":"TypeScript","pushedAt":"2026-09-12T05:23:49Z","avatarUrl":"https://avatars.githubusercontent.com/u/102277171?v=4","crawledAt":"2026-09-25T11:51:44.477Z","openIssues":7,"manifestFile":"SKILL.md","manifestPath":"skills/pinme-share/SKILL.md","defaultBranch":"main"},"readme":"# PinMe Share\n\nCreate a polished static share artifact, upload it with `pinme upload`, and return the final URL.\n\n## When to Use\n\nUse this skill when the user asks to:\n\n- Share or publish a result using PinMe.\n- Create a static page that wraps a deployed project link, demo, report, or artifact.\n- Summarize a Codex conversation/session and share it as a page.\n- Turn a project handoff into a public landing or summary page.\n- Upload an existing static file or folder for lightweight distribution.\n\nIf the user needs a backend, database, auth, email, or LLM functionality, use the main `pinme` skill and any relevant PinMe integration skill first. Use `pinme-share` at the end to package and publish the result.\n\n## Core Workflow\n\n1. Identify what is being shared:\n   - **PinMe/full-stack project**: deployed URL, short description, key features, tech stack, usage notes.\n   - **Codex conversation**: goal, decisions, implementation summary, important outputs, next steps.\n   - **Static file/report/demo**: title, purpose, file contents or preview, context for the recipient.\n2. Create a static share artifact:\n   - Prefer a single self-contained `index.html`.\n   - Use `share/<slug>/index.html` in the current workspace unless the repo has an existing output/share convention.\n   - Keep CSS inline for portability.\n   - Do not require JavaScript unless interaction is valuable.\n3. Sanitize before publishing:\n   - Remove secrets, tokens, API keys, `.env` values, internal-only URLs, private user data, and unrelated logs.\n   - For conversation summaries, summarize rather than dumping raw transcript unless the user explicitly asks for verbatim sharing.\n   - Make links explicit and clickable.\n4. Upload with PinMe:\n   ```bash\n   pinme upload share/<slug>\n   ```\n5. Return the URL printed by PinMe. If PinMe outputs multiple URLs, prefer DNS domain, then PinMe subdomain, then short URL, then full preview URL. Never truncate hash fragments.\n\n## Share Page Content\n\nFor a project share page, include:\n\n- Project name and one-sentence description.\n- Primary launch/demo link as the first action.\n- What it does, who it is for, and why it matters.\n- Feature list focused on user-visible behavior.\n- Build/deploy details only when useful to the recipient.\n- Date and provenance such as \"Created with Codex\" only if appropriate.\n\nFor a conversation share page, include:\n\n- Conversation title.\n- Initial goal or question.\n- Key context and constraints.\n- Decisions made.\n- Work completed or answer summary.\n- Files changed, commands run, links produced, or artifacts created when relevant.\n- Follow-up items.\n\nFor a file/report share page, include:\n\n- Clear title and short abstract.\n- Download/open link to the uploaded artifact if there is a separate file.\n- Important excerpts or generated summary.\n- Source/context notes.\n\n## HTML Guidelines\n\n- Build a real share page, not a generic placeholder.\n- Make the most important link visible in the first viewport.\n- Use clean, responsive HTML/CSS that works as a standalone static file.\n- Keep the design restrained and readable; avoid overdecorated marketing layouts for technical handoffs.\n- Use semantic sections, accessible contrast, descriptive link text, and sensible mobile spacing.\n- Escape user-provided text before inserting it into HTML.\n- If showing code or command output, wrap it in `<pre><code>` and keep it short.\n\n## PinMe Upload Checklist\n\nBefore upload:\n\n```bash\npinme --version\n```\n\nIf PinMe is missing or stale, install or update it according to the main `pinme` skill. Authentication is required for upload:\n\n```bash\npinme login\n# or: pinme set-appkey <AppKey>\n```\n\nUpload examples:\n\n```bash\npinme upload share/my-project\npinme upload share/conversation-summary\npinme upload ./report.html\npinme upload ./dist\n```\n\nDo not upload:\n\n- `.env`, `.git`, `node_modules`, source trees, private datasets, raw logs with credentials, or unrelated build cache.\n- Raw conversation transcripts that may include secrets or private context unless th","createdAt":"2026-09-25T11:51:56.743Z","updatedAt":"2026-09-25T11:51:56.743Z"},{"id":"cmugwhsgg01h2qu06355ifb4x","slug":"glitternetwork-pinme-pinme-uniwebpay","name":"pinme-uniwebpay","description":"Use when generating, modifying, or reviewing PinMe Worker (Cloudflare Worker TypeScript) code that accepts payments through UniwebPay — payment links, products/prices, checkout sessions, payment status reads, refunds, subscriptions, or handling UniwebPay webhooks with @uniwebpay/sdk in a PinMe project.","authorId":"gh:glitternetwork","authorName":"glitternetwork","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3745,"pricePerCall":0,"manifest":{"name":"pinme-uniwebpay","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use when generating, modifying, or reviewing PinMe Worker (Cloudflare Worker TypeScript) code that accepts payments through UniwebPay — payment links, products/prices, checkout sessions, payment status reads, refunds, subscriptions, or handling UniwebPay webhooks with @uniwebpay/sdk in a PinMe project.","permissions":[],"systemPrompt":"# PinMe UniwebPay Payment Integration\n\nGuides writing payment services in a PinMe Worker (Cloudflare Worker TypeScript) that call UniwebPay directly through `@uniwebpay/sdk`.\n\nCore model: PinMe provisions the UniwebPay wallet and keys per **PinMe user** (not per project) and injects `UNIWEB_*` environment bindings at Worker deploy time; Worker code calls UniwebPay **directly with the SDK** — it does not go through PinMe payment proxy routes, and it must not call the legacy VibeCash APIs.\n\n## Environment Binding Contract\n\n```typescript\nexport interface Env {\n  UNIWEB_SECRET: string;           // PinMe-provisioned sk_server_ key (server-side only)\n  UNIWEB_WEBHOOK_SECRET?: string;  // wallet-level whsec_, used to verify webhook signatures\n  UNIWEB_API_URL?: string;         // UniwebPay API endpoint override (default https://apiskill.uniwebpay.com)\n  UNIWEB_PAY_URL?: string;         // UniwebPay checkout host override (default https://skill.uniwebpay.com)\n  UNIWEB_WALLET_ID?: string;       // user-level wallet id (wal_), diagnostics/reconciliation only\n  WORKER_URL?: string;             // this project's public URL: https://{projectName}.{platform api domain}\n  PROJECT_NAME?: string;           // PinMe project name\n  DB?: D1Database;                 // project D1 (if enabled)\n}\n```\n\nInjection rules (metadata is rebuilt server-side by PinMe at deploy time; client-supplied bindings are ignored):\n\n- The `UNIWEB_*` bindings are injected only after the user's UniwebPay credentials have been provisioned. Newly created projects are provisioned automatically and get them immediately; **existing projects must be redeployed after enabling UniwebPay or rotating keys** to pick up new bindings.\n- `WORKER_URL`, `PROJECT_NAME`, `API_KEY`, `DB` and other base bindings are injected on every deploy, independent of UniwebPay.\n- All projects owned by the same PinMe user share one wallet, one `sk_server_`, and one `whsec_`.\n- PinMe never gives the full wallet secret (`sk_live_`) to a Worker. Do not ask the user for it, and do not put it in code, `wrangler.toml`, `.dev.vars`, responses, logs, D1, or frontend bundles.\n- If `UNIWEB_SECRET` is missing at runtime, the user has not enabled UniwebPay or has not redeployed — tell the user to enable it and redeploy; never fabricate a value.\n\n## SDK Client\n\nAlways instantiate on the server side (the Worker); the SDK throws when run in a browser:\n\n```typescript\nimport Uniweb from \"@uniwebpay/sdk\";\n\nfunction uniwebClient(env: Env): Uniweb {\n  return new Uniweb(env.UNIWEB_SECRET, {\n    baseUrl: env.UNIWEB_API_URL,\n    payUrl: env.UNIWEB_PAY_URL,\n  });\n}\n```\n\n- The constructor's first positional argument is the key (must have an `sk_server_` or `sk_live_` prefix); the second is optional options: `{ baseUrl?, payUrl?, timeout? (default 30s), maxRetries? (default 2) }`.\n- The SDK auto-retries only GET/DELETE on 429/5xx; POST/PATCH are never retried (avoids duplicate charges).\n- Install `@uniwebpay/sdk` only when Worker code imports it; pick the package manager from the project's existing lockfile.\n\n## Choosing an Integration Path\n\n| Scenario | Approach | Returns |\n|------|------|------|\n| Fixed-amount one-time collection | `uniweb.links.create(...)` | Permanent, reusable `/p/` link (one-time payments only) |\n| Stable product catalog | `products.create` + `prices.create` once, store the `priceId` | Price carries a permanent `paymentUrl` (`/buy/` link) |\n| Dynamic cart/order | Reuse or create a price, then `uniweb.checkout.create(...)` | `session.url` — **one-time, expires in 24 hours** |\n| Subscriptions | Recurring price + `checkout.create({ mode: \"subscription\" })` or `subscriptions.create` | Same as above |\n| Server-side payment status checks | `payments.get / list` | Server routes only |\n\nAmounts are always **integer minor units** (cents). Default currency convention is `SGD` unless the app has a stronger existing convention. Do not create a new product/price on every page view — create stable catalog items once and persist the `priceId`.\n\n## Payment Methods and Currency Rules\n\n| Method | Supported currencies |\n|------|---------|\n| `card` | SGD, USD, EUR, GBP, JPY, CNY, HKD, AUD, MYR, THB (minimum 10 minor units) |\n| `wechat` | SGD only |\n| `alipay` | SGD only |\n| `paynow` | SGD only |\n\n- The QR methods (wechat/alipay/paynow) **all support SGD only** — never generate \"CNY via WeChat/Alipay\" code.\n- Subscriptions (recurring / `mode: \"subscription\"`) use `card` only.\n- When `paymentMethodTypes` is omitted, the server picks sensible defaults for the currency; when passed explicitly, validate user input against the table above first.\n\n## SDK Surface Quick Reference\n\nThe surface below is verified against source. All parameter fields are camelCase (`priceId`, `webhookUrl`, `startingAfter`, …); the SDK handles wire-level conversion itself. `list()` returns `{ data: T[], hasMore: boolean }`; `listAll()` is an async generator available on products, prices, payments, customers, subscriptions, and links (not on checkout or refunds).\n\nProducts (`webhookUrl` is the per-product callback override):\n\n```typescript\nawait uniweb.products.create({ name, description?, webhookUrl?, metadata? });\nawait uniweb.products.list({ limit?, startingAfter? });\nawait uniweb.products.get(productId);\nawait uniweb.products.update(productId, { name?, description?, webhookUrl?, active?, metadata? });\nawait uniweb.products.del(productId);\nfor await (const product of uniweb.products.listAll()) {}\n```\n\nPrices (the returned price carries a permanent `paymentUrl`; `deactivate` takes it off sale):\n\n```typescript\nawait uniweb.prices.create({\n  productId,\n  amount,        // integer minor units\n  currency,      // e.g. \"SGD\"\n  type,          // \"one_time\" | \"recurring\"\n  interval?,     // \"day\" | \"week\" | \"month\" | \"year\"; recurring only\n  intervalCount?,\n  trialPeriodDays?,\n  metadata?,\n});\nawait uniweb.prices.list({ productId?, limit?, startingAfter? });\nawait uniweb.prices.get(priceId);\nawait uniweb.prices.update(priceId, { active });\nawait uniweb.prices.activate(priceId);\nawait uniweb.prices.deactivate(priceId);\nfor await (const price of uniweb.prices.listAll({ productId? })) {}\n```\n\nCheckout sessions (**do not accept `webhookUrl`** — events resolve through the price → product → wallet chain; the URL is one-time and expires after 24 hours):\n\n```typescript\nawait uniweb.checkout.create({\n  mode,           // \"payment\" | \"subscription\"\n  lineItems: [{ priceId, quantity }],\n  successUrl?,\n  cancelUrl?,\n  customerEmail?,\n  customerId?,\n  trialPeriodDays?,\n  paymentMethodTypes?, // [\"card\", \"wechat\", \"alipay\", \"paynow\"]\n  metadata?,\n});\nawait uniweb.checkout.list({ limit?, startingAfter? });\nawait uniweb.checkout.get(checkoutSessionId);\n```\n\nPayments (for server-side status checks; only mark a local order paid when amount, currency, metadata, and order state all match expectations):\n\n```typescript\nawait uniweb.payments.create({ amount, currency, customerId?, metadata? });\nawait uniweb.payments.list({ status?, customerId?, limit?, startingAfter? });\nawait uniweb.payments.get(paymentId, { gateway? });\nawait uniweb.payments.listRefunds(paymentId);\nawait uniweb.payments.sync(paymentId);\nawait uniweb.payments.void(paymentId);\nfor await (const payment of uniweb.payments.listAll({ status?, customerId? })) {}\n```\n\nRefunds (no list/listAll — use `payments.listRefunds`):\n\n```typescript\nawait uniweb.refunds.create({ paymentId, amount?, reason?, offlineRefundFlag? });\nawait uniweb.refunds.get(refundId, { gateway? });\n```\n\nCustomers:\n\n```typescript\nawait uniweb.customers.create({ email, name?, metadata? });\nawait uniweb.customers.list({ email?, limit?, startingAfter? });\nawait uniweb.customers.get(customerId);\nawait uniweb.customers.update(customerId, { email?, name?, metadata? });\nawait uniweb.customers.del(customerId);\nfor await (const customer of uniweb.customers.listAll({ email? })) {}\n```\n\nSubscriptions (states include `trialing` / `active` / `past_due` / `unpaid` / `canceled`; update access only from verified webhooks or a trusted server-side reconciliation job):\n\n```typescript\nawait uniweb.subscriptions.create({ customerId, priceId, paymentMethodId?, trialPeriodDays?, metadata? });\nawait uniweb.subscriptions.list({ customerId?, status?, limit?, startingAfter? });\nawait uniweb.subscriptions.get(subscriptionId);\nawait uniweb.subscriptions.update(subscriptionId, { cancelAtPeriodEnd? });\nawait uniweb.subscriptions.cancel(subscriptionId); // cancel immediately\nawait uniweb.subscriptions.resume(subscriptionId); // undo cancelAtPeriodEnd\nfor await (const subscription of uniweb.subscriptions.listAll({ customerId?, status? })) {}\n```\n\nPayment links (permanent reusable `/p/` links, one-time collection only; `webhookUrl` is the per-link callback override):\n\n```typescript\nawait uniweb.links.create({\n  amount,\n  currency,\n  name?,\n  description?,\n  successUrl?,\n  cancelUrl?,\n  webhookUrl?,\n  paymentMethodTypes?,\n  metadata?,\n});\nawait uniweb.links.list({ limit?, startingAfter? });\nawait uniweb.links.get(paymentLinkId);\nawait uniweb.links.update(paymentLinkId, { name?, description?, successUrl?, cancelUrl?, webhookUrl?, active? });\nawait uniweb.links.deactivate(paymentLinkId);\nfor await (const link of uniweb.links.listAll()) {}\n```\n\nWallet and wallet-level webhook configuration (**danger zone**: affects the wallet shared by ALL of the user's projects):\n\n```typescript\nawait uniweb.wallet.current();\nawait uniweb.wallet.update({ merchantName?, merchantCity?, merchantCountry?, webhookUrl? });\nawait uniweb.webhooks.set(url);       // overwrites the wallet-level callback URL\nawait uniweb.webhooks.info();\nawait uniweb.webhooks.remove();\nawait uniweb.webhooks.rollSecret();   // rotates the shared whsec_\n```\n\nOrdinary project routes must **not** call `webhooks.set / remove / rollSecret` or `wallet.update` — they mutate the wallet callback fallback and signing secret shared across all of the user's projects. Generate them only when the user explicitly asks for wallet administration and the route has project/admin-level authorization. Same for refunds, subscription mutations, payouts, KYC, and bank account APIs: generate only when the user explicitly requests that business flow and the code has validation, persistence, and authorization.\n\n## Webhook Integration\n\n### Callback URL: set it on the link/product, pointing at this Worker\n\nEvent delivery precedence: **per-link `webhookUrl` > per-product `webhookUrl` > wallet-level fallback**. The signing secret is always the wallet-level `whsec_` (i.e. `env.UNIWEB_WEBHOOK_SECRET`).\n\nPinMe sets a managed fallback callback URL on the wallet, but it exists only to obtain and preserve the signing secret — **PinMe's server discards events it receives there (204); it never forwards them to the Worker**. Business events must therefore set this project's `webhookUrl` explicitly on the resource that creates the payment:\n\n- Payment links: pass `webhookUrl` on `links.create`.\n- Checkout sessions: `checkout.create` has no `webhookUrl` field; events route through the price's product — set `webhookUrl` on `products.create` (or on the reused product).\n\nRules for building the `webhookUrl`:\n\n- Keep the callback path in a single constant (e.g. `const WEBHOOK_PATH = \"/api/pay/webhook\"`) shared by the router and the `webhookUrl` construction, so a path mismatch can't 404 the callbacks and leave orders stuck in pending.\n- Use `env.WORKER_URL` as the base: `new URL(WEBHOOK_PATH, env.WORKER_URL).toString()`. It is the only public address available at runtime (the platform subdomain); the user's custom domain is not in `env`. Prefer it over `request.url` (the current request's host is not necessarily the deployed address), and non-HTTP contexts (cron/queue) have no request at all — fail loudly if it's missing rather than emitting a broken URL.\n- Local dev has no `WORKER_URL`; a `request.url` fallback resolves to localhost, which UniwebPay cannot reach. To test webhooks locally, expose the Worker through a tunnel (cloudflared / ngrok).\n- `webhookUrl` must be HTTPS. Put no secrets or trust-bearing data in the URL query — carry correlation like `orderId` in `metadata`, and verify identity from the signature plus `metadata` (a query string can be forged).\n\n### Verification and Handling\n\n```typescript\nimport { verifyWebhook } from \"@uniwebpay/sdk\";\n\nconst rawBody = await request.text();              // read the raw body exactly once\nconst event = await verifyWebhook(                  // note: async\n  rawBody,\n  request.headers.get(\"uniweb-Signature\") || \"\",   // format: t=<unix>,v1=<hex>\n  env.UNIWEB_WEBHOOK_SECRET,\n);\n```\n\n- `verifyWebhook(rawBody, signature, secret)` returns `Promise<WebhookEvent>`; signature timestamp tolerance is ±5 minutes; it throws on failure.\n- Event shape: `{ id: \"evt_...\", type, created, data: { object, productId?, priceId?, productName? } }`. The business object is in `event.data.object`; correlation like `orderId` is in `event.data.object.metadata`.\n- Event types verified as actually delivered: `payment.succeeded` / `payment.failed` / `payment.refunded` / `payment.partially_refunded`, `refund.succeeded` / `refund.failed` / `refund.abandoned`, `checkout.session.completed` / `checkout.session.expired`, `subscription.created` / `subscription.renewed` / `subscription.past_due` / `subscription.unpaid` / `subscription.trial_ending` / `subscription.canceled`.\n\nHandling rules:\n\n- **The webhook route must bypass the project's own auth.** UniwebPay callbacks carry only `uniweb-Signature` (plus `uniweb-Event-Id` / `uniweb-Timestamp`), never the project `API_KEY`. If a global auth guard wraps all routes, exempt `WEBHOOK_PATH` — trust comes solely from signature verification. Otherwise callbacks get 401/403 and orders never fulfill.\n- Return 400 for invalid signatures (so UniwebPay stops pointless retries); return 500 for temporary processing failures (so it retries).\n- Enforce idempotency with `event.id` (combined with payment id / checkout session id / local order id).\n- Before fulfillment, verify amount, currency, metadata, and current order state.\n- Respond within 10 seconds; delivery does not follow redirects, so do not put the webhook route behind a redirect. The first delivery is immediate; on failure, retries follow at 5-minute, 30-minute, 2-hour, and 12-hour intervals, up to 6 attempts before the event is marked failed.\n- Keep `UNIWEB_WEBHOOK_SECRET` optional in TypeScript — a project can exist before provisioning or redeploy; return 501 with a hint when it's missing at runtime.\n\n## Security Rules\n\n- No Uniweb secret (`UNIWEB_SECRET`, `UNIWEB_WEBHOOK_SECRET`) may appear in responses, logs, metadata, test snapshots, D1, source code, committed `wrangler.toml` / `.dev.vars`, or browser code. For local dev, use an uncommitted `.dev.vars` only.\n- Do not import the SDK in browser-side code; do not use `process.env` in Cloudflare Workers — use the `env` argument.\n- Do not call legacy VibeCash APIs or PinMe payment proxy routes; do not call PinMe payment APIs with `X-API-Key` for UniwebPay collection — the Worker calls UniwebPay directly through the SDK.\n- `successUrl` / `cancelUrl` are UX redirects only. **Never fulfill based on a browser redirect**; grant access only after verified webhook processing (or another explicit server-side verification).\n- Validate user input before SDK calls: amount (integer minor units), currency (ISO 4217 and within the payment-method constraints), quantity, product/price IDs, payment method types, order ownership, and metadata shape.\n\n## Persistence Guidance (D1)\n\nAdd tables/migrations only when the project already uses D1 or the user asks for persistence. For order flows, store at least: local order id, the corresponding Uniweb link/session/payment/subscription id, amount and currency, status, timestamps, and processed webhook event ids (for idempotency). Never store secrets.\n\n```sql\nCREATE TABLE IF NOT EXISTS orders (\n  order_id TEXT PRIMARY KEY,\n  checkout_session_id TEXT UNIQUE,\n  status TEXT NOT NULL DEFAULT 'pending',\n  amount_cents INTEGER NOT NULL,\n  currency TEXT NOT NULL,\n  paid_at INTEGER,\n  created_at INTEGER NOT NULL,\n  updated_at INTEGER\n);\n\nCREATE TABLE IF NOT EXISTS payment_events (\n  event_id TEXT PRIMARY KEY,\n  event_type TEXT NOT NULL,\n  order_id TEXT,\n  raw_payload TEXT NOT NULL,\n  created_at INTEGER NOT NULL\n);\n```\n\n## Worker Reference Implementation\n\n```typescript\nimport Uniweb, { verifyWebhook } from \"@uniwebpay/sdk\";\n\nexport interface Env {\n  UNIWEB_SECRET: string;\n  UNIWEB_WEBHOOK_SECRET?: string;\n  UNIWEB_API_URL?: string;\n  UNIWEB_PAY_URL?: string;\n  UNIWEB_WALLET_ID?: string;\n  WORKER_URL?: string;\n  PROJECT_NAME?: string;\n  DB?: D1Database;\n}\n\ntype PaymentMethod = \"card\" | \"wechat\" | \"alipay\" | \"paynow\";\n\nconst VALID_PAYMENT_METHODS = new Set<PaymentMethod>([\"card\", \"wechat\", \"alipay\", \"paynow\"]);\nconst CARD_CURRENCIES = new Set([\"SGD\", \"USD\", \"EUR\", \"GBP\", \"JPY\", \"CNY\", \"HKD\", \"AUD\", \"MYR\", \"THB\"]);\n\n// Single source of truth for the webhook path: shared by the router and the\n// webhookUrl construction so the address sent to UniwebPay always matches the\n// route the Worker actually serves.\nconst WEBHOOK_PATH = \"/api/pay/webhook\";\n\nfunction uniwebClient(env: Env): Uniweb {\n  return new Uniweb(env.UNIWEB_SECRET, {\n    baseUrl: env.UNIWEB_API_URL,\n    payUrl: env.UNIWEB_PAY_URL,\n  });\n}\n\nfunction json(data: unknown, init: ResponseInit = {}): Response {\n  return Response.json(data, init);\n}\n\nfunction assertAmountCents(value: unknown): number {\n  const amount = Number(value);\n  if (!Number.isInteger(amount) || amount < 10) {\n    throw new Error(\"amountCents must be an integer minor-unit amount >= 10\");\n  }\n  return amount;\n}\n\nfunction normalizeCurrency(value: unknown): string {\n  const currency = String(value || \"SGD\").toUpperCase();\n  if (!/^[A-Z]{3}$/.test(currency)) throw new Error(\"currency must be an ISO 4217 code\");\n  return currency;\n}\n\n// QR methods (wechat/alipay/paynow) support SGD only; card supports 10 currencies.\nfunction defaultPaymentMethods(currency: string): PaymentMethod[] {\n  if (currency === \"SGD\") return [\"card\", \"wechat\", \"alipay\", \"paynow\"];\n  if (CARD_CURRENCIES.has(currency)) return [\"card\"];\n  throw new Error(`unsupported currency: ${currency}`);\n}\n\nfunction normalizePaymentMethods(value: unknown, currency: string): PaymentMethod[] {\n  const requested = Array.isArray(value) && value.length > 0 ? value : defaultPaymentMethods(currency);\n  const methods = requested.map((m) => String(m).toLowerCase() as PaymentMethod);\n  for (const method of methods) {\n    if (!VALID_PAYMENT_METHODS.has(method)) {\n      throw new Error(\"paymentMethodTypes contains an unsupported method\");\n    }\n    if (method !== \"card\" && currency !== \"SGD\") {\n      throw new Error(`${method} only supports SGD payments`);\n    }\n    if (method === \"card\" && !CARD_CURRENCIES.has(currency)) {\n      throw new Error(`card does not support ${currency}`);\n    }\n  }\n  return Array.from(new Set(methods));\n}\n\n// Prefer the PinMe-injected WORKER_URL (the project's platform subdomain, the\n// only public address available at runtime). request.url is only a fallback for\n// older deploys; cron/queue contexts have no request, so WORKER_URL is required.\nfunction projectWebhookUrl(env: Env, request?: Request): string {\n  const base = env.WORKER_URL || request?.url;\n  if (!base) throw new Error(\"WORKER_URL binding is missing\");\n  return new URL(WEBHOOK_PATH, base).toString();\n}\n\n// ---- One-time collection: payment link ----\n\nasync function createPaymentLink(request: Request, env: Env): Promise<Response> {\n  if (request.method !== \"POST\") return json({ error: \"method not allowed\" }, { status: 405 });\n\n  let input: any;\n  try {\n    input = await request.json();\n  } catch {\n    return json({ error: \"invalid JSON body\" }, { status: 400 });\n  }\n\n  let amount: number, currency: string, paymentMethodTypes: PaymentMethod[];\n  try {\n    amount = assertAmountCents(input.amountCents);\n    currency = normalizeCurrency(input.currency);\n    paymentMethodTypes = normalizePaymentMethods(input.paymentMethodTypes, currency);\n  } catch (err) {\n    return json({ error: (err as Error).message }, { status: 400 });\n  }\n\n  const orderId = input.orderId || crypto.randomUUID();\n  const uniweb = uniwebClient(env);\n\n  try {\n    const link = await uniweb.links.create({\n      amount,\n      currency,\n      name: input.name || \"Payment\",\n      description: input.description,\n      successUrl: input.successUrl,\n      cancelUrl: input.cancelUrl,\n      webhookUrl: projectWebhookUrl(env, request),\n      paymentMethodTypes,\n      metadata: { orderId, projectName: env.PROJECT_NAME },\n    });\n    return json({ orderId, linkId: link.id, url: link.url });\n  } catch {\n    return json({ error: \"failed to create payment link\" }, { status: 502 });\n  }\n}\n\n// ---- Dynamic orders: checkout session ----\n// Note: checkout.create has no webhookUrl; the callback is set on the product.\n// Stable catalog items should create the product/price once and persist the\n// priceId — do not create new ones on every request.\n\nasync function createCheckoutSession(request: Request, env: Env): Promise<Response> {\n  if (request.method !== \"POST\") return json({ error: \"method not allowed\" }, { status: 405 });\n\n  let input: any;\n  try {\n    input = await request.json();\n  } catch {\n    return json({ error: \"invalid JSON body\" }, { status: 400 });\n  }\n\n  let amount: number, currency: string, paymentMethodTypes: PaymentMethod[];\n  try {\n    amount = assertAmountCents(input.amountCents);\n    currency = normalizeCurrency(input.currency);\n    paymentMethodTypes = normalizePaymentMethods(input.paymentMethodTypes, currency);\n  } catch (err) {\n    return json({ error: (err as Error).message }, { status: 400 });\n  }\n\n  const orderId = input.orderId || crypto.randomUUID();\n  const quantity = Math.max(1, Math.floor(Number(input.quantity || 1)));\n  const uniweb = uniwebClient(env);\n\n  try {\n    const product = await uniweb.products.create({\n      name: input.productName,\n      webhookUrl: projectWebhookUrl(env, request),\n      metadata: { orderId, projectName: env.PROJECT_NAME },\n    });\n    const price = await uniweb.prices.create({\n      productId: product.id,\n      amount,\n      currency,\n      type: \"one_time\",\n      metadata: { orderId, projectName: env.PROJECT_NAME },\n    });\n    const session = await uniweb.checkout.create({\n      mode: \"payment\",\n      lineItems: [{ priceId: price.id, quantity }],\n      successUrl: input.successUrl,\n      cancelUrl: input.cancelUrl,\n      customerEmail: input.customerEmail,\n      paymentMethodTypes,\n      metadata: { orderId, projectName: env.PROJECT_NAME },\n    });\n\n    if (env.DB) {\n      await env.DB.prepare(\n        `INSERT INTO orders(order_id, checkout_session_id, status, amount_cents, currency, created_at)\n         VALUES (?, ?, 'pending', ?, ?, ?)\n         ON CONFLICT(order_id) DO UPDATE SET checkout_session_id = excluded.checkout_session_id`,\n      )\n        .bind(orderId, session.id, amount * quantity, currency, Math.floor(Date.now() / 1000))\n        .run();\n    }\n\n    return json({ orderId, checkoutSessionId: session.id, url: session.url });\n  } catch {\n    return json({ error: \"failed to create checkout session\" }, { status: 502 });\n  }\n}\n\n// ---- Webhook handling ----\n\nasync function handleUniwebWebhook(request: Request, env: Env): Promise<Response> {\n  if (request.method !== \"POST\") return json({ error: \"method not allowed\" }, { status: 405 });\n  if (!env.UNIWEB_WEBHOOK_SECRET) {\n    return json({ error: \"webhook verification is not configured\" }, { status: 501 });\n  }\n\n  const rawBody = await request.text();\n  let event: Awaited<ReturnType<typeof verifyWebhook>>;\n  try {\n    event = await verifyWebhook(\n      rawBody,\n      request.headers.get(\"uniweb-Signature\") || \"\",\n      env.UNIWEB_WEBHOOK_SECRET,\n    );\n  } catch {\n    return json({ error: \"invalid signature\" }, { status: 400 });\n  }\n\n  try {\n    if (env.DB) {\n      const object = event.data.object as { metadata?: Record<string, unknown> };\n      const orderId = String(object?.metadata?.orderId || \"\");\n      const now = Math.floor(Date.now() / 1000);\n\n      // event.id idempotency: duplicate deliveries become no-ops\n      await env.DB.prepare(\n        `INSERT INTO payment_events(event_id, event_type, order_id, raw_payload, created_at)\n         VALUES (?, ?, ?, ?, ?)\n         ON CONFLICT(event_id) DO NOTHING`,\n      )\n        .bind(event.id, event.type, orderId, rawBody, now)\n        .run();\n\n      if (event.type === \"payment.succeeded\" && orderId) {\n        // Production code should verify amount/currency/order state here before fulfilling\n        await env.DB.prepare(\n          `UPDATE orders SET status = 'paid', paid_at = ?, updated_at = ? WHERE order_id = ? AND status != 'paid'`,\n        )\n          .bind(now, now, orderId)\n          .run();\n      }\n    }\n    return json({ ok: true });\n  } catch {\n    // 500 makes UniwebPay retry on the 5m/30m/2h/12h schedule\n    return json({ error: \"processing error\" }, { status: 500 });\n  }\n}\n\n// ---- Router ----\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n\n    // The webhook must come before any project auth guard: callbacks carry only\n    // uniweb-Signature, never the project API_KEY.\n    if (url.pathname === WEBHOOK_PATH) return handleUniwebWebhook(request, env);\n\n    // The project's own auth guard goes after this point, on the business routes.\n    if (url.pathname === \"/api/pay/link\") return createPaymentLink(request, env);\n    if (url.pathname === \"/api/pay/checkout\") return createCheckoutSession(request, env);\n\n    return json({ error: \"not found\" }, { status: 404 });\n  },\n};\n```\n\n## Common Mistakes\n\n| Mistake | Consequence / fix |\n|------|----------|\n| Webhook route blocked by the project's `API_KEY` / bearer auth guard | Callbacks get 401/403 and orders stay pending forever. Exempt `WEBHOOK_PATH`; trust comes solely from signature verification |\n| Forgetting to set `webhookUrl` on the link/product | Events fall through to PinMe's managed fallback and are discarded; the Worker never sees them. Business events must point per-link/per-product at this Worker |\n| Passing `webhookUrl` to `checkout.create` | The field does not exist. Set it on the backing product |\n| Fulfilling on a `successUrl` redirect | Forgeable. Fulfill only from verified webhooks or server-side verification |\n| Hardcoding the callback host or relying only on `request.url` | Build from `env.WORKER_URL`; `request.url` is a fallback only |\n| Pairing CNY with wechat/alipay | Source limits QR methods to SGD only; CNY can only use card |\n| Creating a new product/price on every request | Create stable catalog items once, persist and reuse the `priceId` |\n| Calling `webhooks.set/remove/rollSecret` or `wallet.update` in ordinary business flows | Mutates/rotates the wallet callback and `whsec_` shared by ALL of the user's projects. Generate only for explicit wallet-administration requests with admin authorization |\n| Using floating-point major units for amounts | Always integer minor units |\n| Using `process.env` in the Worker or importing the SDK in the browser | Use the `env` argument; the SDK rejects browser environments |\n| Putting `UNIWEB_SECRET` / `whsec_` in `wrangler.toml`, source, logs, or D1 | PinMe injects them at deploy time; locally use an uncommitted `.dev.vars` only |\n| Reading the body more than once, or as JSON, before verification | Read with `request.text()` exactly once and pass the raw string to `verifyWebhook` |\n\n## Finish Checklist\n\n- [ ] `@uniwebpay/sdk` is installed only when Worker code imports it; package manager follows the lockfile.\n- [ ] `Env` includes the PinMe-injected bindings the code uses; `UNIWEB_WEBHOOK_SECRET` / `WORKER_URL` stay optional and their absence is handled.\n- [ ] The client is instantiated with `new Uniweb(env.UNIWEB_SECRET, { baseUrl: env.UNIWEB_API_URL, payUrl: env.UNIWEB_PAY_URL })`.\n- [ ] No VibeCash or PinMe payment proxy routes are used; no secret appears in source, responses, logs, D1, tests, or docs.\n- [ ] Amounts are validated integer minor units; payment methods and currencies follow the constraint table (QR methods SGD only).\n- [ ] Every payment creation that needs events carries a per-link/per-product `webhookUrl` built from `env.WORKER_URL`.\n- [ ] Webhook: raw body read exactly once, `verifyWebhook` verification, correct 400/500 semantics, `event.id` idempotency, route bypasses project auth, responds within 10 seconds.\n- [ ] Fulfillment does not rely on browser redirects; amount, currency, metadata, and order state are verified before granting access.","schemaVersion":1},"repoUrl":"https://github.com/glitternetwork/pinme/tree/main/skills/pinme-uniwebpay","tags":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"pinme","audit":{"files":["package-lock.json","package.json","pnpm-lock.yaml"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS).","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-7q85-xj36-vmfc · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip extraction follows destination symlinks, allowing arbitrary file overwrite.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-vwc7-r8mq-g2x9 · npm:adm-zip@0.5.17","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Crafted ZIP file triggers 4GB memory allocation.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-xcpc-8h2w-3j85 · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"uuid@9.0.1 has a known vulnerability: uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-w5hq-g745-h8pq · npm:uuid@9.0.1","severity":"medium"}],"packages":16,"auditedAt":"2026-09-25T11:51:56.672Z","lockfiles":["package-lock.json","pnpm-lock.yaml"]},"forks":277,"owner":"glitternetwork","stars":3745,"topics":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy","static-site-hosting","web-hosting","zero-configuration"],"license":"MIT","fullName":"glitternetwork/pinme","homepage":"https://pinme.eth.limo","language":"TypeScript","pushedAt":"2026-09-12T05:23:49Z","avatarUrl":"https://avatars.githubusercontent.com/u/102277171?v=4","crawledAt":"2026-09-25T11:51:44.477Z","openIssues":7,"manifestFile":"SKILL.md","manifestPath":"skills/pinme-uniwebpay/SKILL.md","defaultBranch":"main"},"readme":"# PinMe UniwebPay Payment Integration\n\nGuides writing payment services in a PinMe Worker (Cloudflare Worker TypeScript) that call UniwebPay directly through `@uniwebpay/sdk`.\n\nCore model: PinMe provisions the UniwebPay wallet and keys per **PinMe user** (not per project) and injects `UNIWEB_*` environment bindings at Worker deploy time; Worker code calls UniwebPay **directly with the SDK** — it does not go through PinMe payment proxy routes, and it must not call the legacy VibeCash APIs.\n\n## Environment Binding Contract\n\n```typescript\nexport interface Env {\n  UNIWEB_SECRET: string;           // PinMe-provisioned sk_server_ key (server-side only)\n  UNIWEB_WEBHOOK_SECRET?: string;  // wallet-level whsec_, used to verify webhook signatures\n  UNIWEB_API_URL?: string;         // UniwebPay API endpoint override (default https://apiskill.uniwebpay.com)\n  UNIWEB_PAY_URL?: string;         // UniwebPay checkout host override (default https://skill.uniwebpay.com)\n  UNIWEB_WALLET_ID?: string;       // user-level wallet id (wal_), diagnostics/reconciliation only\n  WORKER_URL?: string;             // this project's public URL: https://{projectName}.{platform api domain}\n  PROJECT_NAME?: string;           // PinMe project name\n  DB?: D1Database;                 // project D1 (if enabled)\n}\n```\n\nInjection rules (metadata is rebuilt server-side by PinMe at deploy time; client-supplied bindings are ignored):\n\n- The `UNIWEB_*` bindings are injected only after the user's UniwebPay credentials have been provisioned. Newly created projects are provisioned automatically and get them immediately; **existing projects must be redeployed after enabling UniwebPay or rotating keys** to pick up new bindings.\n- `WORKER_URL`, `PROJECT_NAME`, `API_KEY`, `DB` and other base bindings are injected on every deploy, independent of UniwebPay.\n- All projects owned by the same PinMe user share one wallet, one `sk_server_`, and one `whsec_`.\n- PinMe never gives the full wallet secret (`sk_live_`) to a Worker. Do not ask the user for it, and do not put it in code, `wrangler.toml`, `.dev.vars`, responses, logs, D1, or frontend bundles.\n- If `UNIWEB_SECRET` is missing at runtime, the user has not enabled UniwebPay or has not redeployed — tell the user to enable it and redeploy; never fabricate a value.\n\n## SDK Client\n\nAlways instantiate on the server side (the Worker); the SDK throws when run in a browser:\n\n```typescript\nimport Uniweb from \"@uniwebpay/sdk\";\n\nfunction uniwebClient(env: Env): Uniweb {\n  return new Uniweb(env.UNIWEB_SECRET, {\n    baseUrl: env.UNIWEB_API_URL,\n    payUrl: env.UNIWEB_PAY_URL,\n  });\n}\n```\n\n- The constructor's first positional argument is the key (must have an `sk_server_` or `sk_live_` prefix); the second is optional options: `{ baseUrl?, payUrl?, timeout? (default 30s), maxRetries? (default 2) }`.\n- The SDK auto-retries only GET/DELETE on 429/5xx; POST/PATCH are never retried (avoids duplicate charges).\n- Install `@uniwebpay/sdk` only when Worker code imports it; pick the package manager from the project's existing lockfile.\n\n## Choosing an Integration Path\n\n| Scenario | Approach | Returns |\n|------|------|------|\n| Fixed-amount one-time collection | `uniweb.links.create(...)` | Permanent, reusable `/p/` link (one-time payments only) |\n| Stable product catalog | `products.create` + `prices.create` once, store the `priceId` | Price carries a permanent `paymentUrl` (`/buy/` link) |\n| Dynamic cart/order | Reuse or create a price, then `uniweb.checkout.create(...)` | `session.url` — **one-time, expires in 24 hours** |\n| Subscriptions | Recurring price + `checkout.create({ mode: \"subscription\" })` or `subscriptions.create` | Same as above |\n| Server-side payment status checks | `payments.get / list` | Server routes only |\n\nAmounts are always **integer minor units** (cents). Default currency convention is `SGD` unless the app has a stronger existing convention. Do not create a new product/price on every page view — create stable catalog items once a","createdAt":"2026-09-25T11:51:56.752Z","updatedAt":"2026-09-25T11:51:56.752Z"},{"id":"cmugwhsgt01h5qu061uk1kqqe","slug":"glitternetwork-pinme-pinme","name":"pinme","description":"Use this skill when the user mentions \"pinme\", or needs to upload files, store to IPFS, create/publish/deploy websites or full-stack services (including frontend pages, backend APIs, database storage, email sending, etc.), or any feature requiring backend database/server support.","authorId":"gh:glitternetwork","authorName":"glitternetwork","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3745,"pricePerCall":0,"manifest":{"name":"pinme","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use this skill when the user mentions \"pinme\", or needs to upload files, store to IPFS, create/publish/deploy websites or full-stack services (including frontend pages, backend APIs, database storage, email sending, etc.), or any feature requiring backend database/server support.","permissions":[],"systemPrompt":"# PinMe\n\nZero-config deployment tool: upload static files to IPFS, or create and deploy full-stack web projects (React+Vite + Cloudflare Worker + D1 database). Workers also support sending emails via the PinMe platform API.\n\n## When to Use\n\n```dot\ndigraph pinme_decision {\n    \"User Request\" [shape=doublecircle];\n    \"Needs backend API or database?\" [shape=diamond];\n    \"Upload Files (Path 1)\" [shape=box];\n    \"Full-Stack Project (Path 2)\" [shape=box];\n\n    \"User Request\" -> \"Needs backend API or database?\";\n    \"Needs backend API or database?\" -> \"Upload Files (Path 1)\" [label=\"No\"];\n    \"Needs backend API or database?\" -> \"Full-Stack Project (Path 2)\" [label=\"Yes\"];\n}\n```\n\n## Path 1: Upload Files / Static Sites\n\n> Login required. Use `pinme login` or `pinme set-appkey <AppKey>` before `pinme upload` or `pinme import`.\n\n```dot\ndigraph upload_flow {\n    \"Install/update pinme to latest\" [shape=box];\n    \"Authenticate\" [shape=box];\n    \"Determine build artifacts\" [shape=box];\n    \"pinme upload <path>\" [shape=box];\n    \"Return preview URL\" [shape=doublecircle];\n\n    \"Install/update pinme to latest\" -> \"Authenticate\";\n    \"Authenticate\" -> \"Determine build artifacts\";\n    \"Determine build artifacts\" -> \"pinme upload <path>\";\n    \"pinme upload <path>\" -> \"Return preview URL\";\n}\n```\n\n**1. Check installation and update to latest:**\n```bash\nLOCAL=$(pinme --version 2>/dev/null || echo \"0.0.0\")\nLATEST=$(npm view pinme version)\n[ \"$LOCAL\" != \"$LATEST\" ] && npm install -g pinme@latest || echo \"pinme is up to date ($LOCAL)\"\n```\n\n**2. Authenticate:**\n```bash\npinme login\n# or: pinme set-appkey <AppKey>\n```\n\n**3. Determine upload target** (priority order):\n1. `dist/` — Vite / Vue / React\n2. `build/` — Create React App\n3. `out/` — Next.js static export\n4. `public/` — Plain static files\n\n**4. Upload:**\n```bash\npinme upload <path>\npinme upload ./dist --domain my-site  # Optional: bind subdomain (wallet balance required)\n```\n\n**5. Return** the final URL printed by PinMe to the user. URL priority is: DNS domain > PinMe subdomain > short URL > preview URL. If it falls back to preview, return the **full URL** including all hash characters — do not truncate.\n\n### Common Examples\n\n```bash\npinme upload ./document.pdf          # Single file\npinme upload ./my-folder             # Folder\npinme upload dist                    # Vite/Vue build artifacts\npinme upload build                   # CRA build artifacts\npinme upload out                     # Next.js static export\npinme upload ./dist --domain my-site # Bind PinMe subdomain (wallet balance required)\npinme import ./my-archive.car        # Import CAR file\n```\n\n### Do NOT Upload\n- `node_modules/`, `.env`, `.git/`, `src/`\n- Only upload build artifacts, never upload source code\n\n---\n\n## Path 2: Full-Stack Project\n\n> Login required. Uses React+Vite frontend + Cloudflare Worker backend + D1 SQLite database.\n> When designing frontend projects, use Ant Design as the primary design reference, and prioritize following its conventions for layout, components, spacing, and interaction patterns.\n\n```dot\ndigraph fullstack_flow {\n    \"Install/update pinme to latest\" [shape=box];\n    \"pinme login\" [shape=box];\n    \"pinme create <name>\" [shape=box];\n    \"Modify template code\" [shape=box];\n    \"pinme save\" [shape=box];\n    \"Return preview URL\" [shape=doublecircle];\n\n    \"Install/update pinme to latest\" -> \"pinme login\";\n    \"pinme login\" -> \"pinme create <name>\";\n    \"pinme create <name>\" -> \"Modify template code\";\n    \"Modify template code\" -> \"pinme save\";\n    \"pinme save\" -> \"Return preview URL\";\n}\n```\n\n### Architecture\n\n| Layer | Tech Stack | Deploy Target |\n|-------|-----------|---------------|\n| Frontend | React + Vite (`frontend/`) | IPFS |\n| Backend | Cloudflare Worker (`backend/src/worker.ts`) | `{name}.pinme.pro` |\n| Database | D1 SQLite (`db/*.sql`) | Cloudflare D1 |\n| Object storage | R2 (`env.R2`) | Cloudflare R2 |\n\n### Capability-Specific Skills\n\n- For Worker file uploads, downloads, images, attachments, media, or object storage, use the `pinme-r2` skill. PinMe injects the project bucket as `env.R2`; do not replace it with D1 BLOBs or Worker filesystem state.\n\n### Core Commands\n\n```bash\npinme login                  # Login (only needed once)\npinme create <dirName>       # Clone template and create project (auto-fills API URL)\npinme save                   # First deploy / full update (frontend + backend + database, single command)\npinme update-worker          # Update backend only (when only backend/src/worker.ts was modified)\npinme update-web             # Update frontend only (when only frontend/src/ was modified)\npinme update-db              # Run SQL migrations only (when only db/ was modified)\n```\n\n> `pinme save` deploys frontend + backend + database all at once. Only use `pinme update-*` when you're certain only one part was modified.\n\n### Project Structure\n\n```\n{project}/\n├── pinme.toml              # Root config (auto-generated, do not modify)\n├── package.json            # Monorepo root (workspaces: frontend + backend)\n├── backend/\n│   ├── wrangler.toml       # Worker config (auto-generated, do not modify)\n│   ├── package.json\n│   └── src/\n│       └── worker.ts       # Backend entry — primarily used for JSON APIs in this template\n├── db/\n│   └── 001_init.sql        # SQL table definitions\n├── frontend/\n│   ├── package.json\n│   ├── vite.config.ts      # Dev proxy: /api → localhost:8787\n│   ├── index.html\n│   ├── .env                # Auto-generated: VITE_API_URL (do not modify)\n│   └── src/\n│       ├── main.tsx\n│       ├── App.tsx\n│       ├── utils/\n│       │   ├── api.ts      # export const API = import.meta.env.VITE_WORKER_URL || ''\n│       │   └── config.ts   # Auto-generated: public_client_config (only when auth is enabled)\n│       └── pages/\n│           └── Home/\n│               └── index.tsx\n└── .gitignore\n```\n\n### First Deployment\n\n```bash\nLOCAL=$(pinme --version 2>/dev/null || echo \"0.0.0\")\nLATEST=$(npm view pinme version)\n[ \"$LOCAL\" != \"$LATEST\" ] && npm install -g pinme@latest\npinme login\npinme create my-app\ncd my-app\n```\n\n`pinme create` generates a working Hello World template (includes frontend page + backend API routes + database schema). **Modify the template** to match the user's business logic — do not write from scratch:\n\n- Modify `backend/src/worker.ts` — replace API routes\n- Modify `frontend/src/pages/` — replace page components\n- Modify `db/001_init.sql` — replace table definitions\n\n```bash\npinme save\n# Single command deploys frontend + backend + database\n# Outputs preview URL: https://pinme.eth.limo/#/preview/{CID}\n```\n\n**Return** the preview URL to the user. Note: return the **full URL** including all hash characters — do not truncate.\n\nThe backend Worker is deployed at `https://{name}.pinme.pro`. Frontend API requests are automatically configured to point to that address — no manual setup needed.\n\n### Subsequent Updates\n\n| Changes | Command | Notes |\n|---------|---------|-------|\n| Backend only (`backend/src/worker.ts`) | `pinme update-worker` | Faster |\n| Frontend only (`frontend/src/`) | `pinme update-web` | Generates new CID |\n| Database only (`db/`) | `pinme update-db` | Runs new migrations |\n| Multiple changes or uncertain | `pinme save` | Safe full deployment |\n\n> Each frontend deployment generates a new CID and preview URL. Old URLs remain accessible.\n\n---\n\n## Worker Code Patterns (`backend/src/worker.ts`)\n\nIn this template, the Worker backend is primarily used for JSON APIs. Prefer standard Web APIs and simple manual routing by default. Worker-compatible libraries can be added when needed, but the default template does not rely on extra frameworks. Avoid packages that depend on a full Node.js runtime, a persistent local filesystem, native binaries, or child processes.\n\n```typescript\nexport interface Env {\n  DB: D1Database;           // When using database\n  R2: R2Bucket;             // Project object storage; injected by PinMe\n  API_KEY?: string;         // When using email sending\n  JWT_SECRET: string;       // When using JWT auth\n  ADMIN_PASSWORD: string;   // When using password auth\n}\n\nconst CORS_HEADERS = {\n  'Access-Control-Allow-Origin': '*',\n  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',\n  'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',\n};\n\nfunction json(data: unknown, status = 200): Response {\n  return Response.json(data, { status, headers: CORS_HEADERS });\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const { pathname } = new URL(request.url);\n    const method = request.method;\n\n    if (method === 'OPTIONS') return new Response(null, { status: 204, headers: CORS_HEADERS });\n\n    try {\n      if (pathname === '/api/items' && method === 'GET')  return handleGetItems(env);\n      if (pathname === '/api/items' && method === 'POST') return handleCreateItem(request, env);\n      return json({ error: 'Not found' }, 404);\n    } catch {\n      return json({ error: 'Internal server error' }, 500);\n    }\n  },\n};\n```\n\n### Worker Constraints and Default Conventions\n\n| Item | Notes |\n|------|------|\n| Dependency choice | Prefer standard Web APIs and simple manual routing by default. If extra dependencies are needed, prefer Worker-compatible libraries. |\n| Node.js capability | Workers now support part of Node.js compatibility, but they are not a full Node.js runtime. Do not assume all Node.js built-in modules are available or behave exactly the same. |\n| Filesystem | Do not treat a Worker like a server with a persistent local disk. Even if some `fs` capabilities are available, do not rely on persistence across requests. |\n| Response types | This template mainly uses the Worker for JSON APIs. If there is a clear need, it can also be adapted to return HTML or other content. |\n| Password storage | Never store passwords in plaintext. Use a dedicated password hashing algorithm such as bcrypt, scrypt, or Argon2. |\n| SQL | Do not build SQL by string concatenation. Use parameterized queries such as `.bind()`. |\n\n### Email API Reference (for Worker Backend)\n\nWhen the backend needs email sending, use the PinMe platform API (`https://pinme.cloud/api/v4/send_email`).\n\n**1. Configure API_KEY**\n\nAdd to the `Env` interface:\n\n```typescript\nexport interface Env {\n  DB: D1Database;\n  API_KEY?: string;  // Required for email sending\n}\n```\n\n**2. Email Handler Code**\n\n```typescript\nasync function handleSendEmail(request: Request, env: Env): Promise<Response> {\n  const apiKey = env.API_KEY;\n  if (!apiKey) {\n    return json({ error: 'API_KEY not configured' }, 500);\n  }\n\n  const body = await request.json() as {\n    to?: string;\n    subject?: string;\n    html?: string;\n  };\n\n  if (!body.to) return json({ error: 'Email address is required' }, 400);\n  if (!body.subject) return json({ error: 'Subject is required' }, 400);\n  if (!body.html) return json({ error: 'HTML content is required' }, 400);\n\n  const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n  if (!emailRegex.test(body.to)) {\n    return json({ error: 'Invalid email address' }, 400);\n  }\n\n  const response = await fetch('https://pinme.cloud/api/v4/send_email', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'X-API-Key': apiKey,\n    },\n    body: JSON.stringify({\n      to: body.to,\n      subject: body.subject,\n      html: body.html,\n    }),\n  });\n\n  const result = await response.json();\n  return json(result);\n}\n```\n\n## Frontend API Utility (frontend/src/utils/api.ts)\n\n```typescript\n// Development: Vite proxies /api to localhost:8787\n// Production: VITE_API_URL is auto-injected by pinme create\nexport const API = import.meta.env.VITE_API_URL || '';\n\nexport function getApiUrl(path: string): string {\n  return API ? `${API}${path}` : path;\n}\n```\n\n## D1 Database Operations\n\n```typescript\n// Query multiple rows\nconst { results } = await env.DB.prepare('SELECT * FROM t WHERE x = ?').bind(val).all();\n\n// Query single row (returns null if not found)\nconst row = await env.DB.prepare('SELECT * FROM t WHERE id = ?').bind(id).first();\n\n// Insert and return new row\nconst row = await env.DB.prepare('INSERT INTO t (a, b) VALUES (?, ?) RETURNING *').bind(a, b).first();\n\n// Update\nawait env.DB.prepare('UPDATE t SET a = ? WHERE id = ?').bind(val, id).run();\n\n// Delete (check if affected)\nconst { meta } = await env.DB.prepare('DELETE FROM t WHERE id = ?').bind(id).run();\nif (meta.changes === 0) return json({ error: 'Not found' }, 404);\n```\n\n### SQL Migration Files\n\n**Format:** `db/NNN_description.sql` (for example, `001_init.sql`). Files are executed in filename order.\n\n**SQLite Type Constraints:**\n\n| Do Not Use | Alternative |\n|-----------|-------------|\n| `BOOLEAN` | `INTEGER` (0 = false, 1 = true) |\n| `DATETIME` / `TIMESTAMP` | `TEXT`, stored as ISO 8601 (default: `datetime('now')`) |\n| `JSON` type | `TEXT`, using `JSON.stringify()` / `JSON.parse()` |\n| `VARCHAR(n)` | `TEXT` |\n\n## Template Architecture Suggestions\n\n| Scenario | Default Suggestion |\n|-----------|-------------|\n| File storage (images, attachments, media) | Use the project R2 binding through `env.R2`; use the `pinme-r2` skill for secure routes and metadata patterns |\n| Real-time communication | This template defaults to regular HTTP APIs. If there is no clear real-time requirement, start with polling |\n| Multiple Workers | This template defaults to combining functionality into a single Worker and separating routes by prefix |\n| Multiple databases | This template defaults to combining data into one D1 database and only splitting when isolation is truly needed |\n\n## Important Notes\n\n- `pinme.toml`, `backend/wrangler.toml`, and `frontend/.env` are generated by PinMe. Do not edit them manually by default. If extra runtime configuration is truly needed, prefer doing it through PinMe-supported mechanisms.\n- Obtain the frontend API URL from the `VITE_API_URL` environment variable. Do not hardcode it.\n- Passwords, tokens, and API keys must be stored in secrets. Never put them in config files.\n\n## Common Errors\n\n| Error | Solution |\n|-------|----------|\n| `command not found: pinme` | `npm install -g pinme` |\n| `No such file or directory` | Verify that the path exists |\n| `Permission denied` | Check file or directory permissions |\n| Upload failed | Check the network connection and retry |\n| Not logged in | Run `pinme login` first |\n\n## Other Commands\n\n```bash\npinme list / pinme ls -l 5     # View upload history\npinme list -c                  # Clear upload history\npinme rm <hash>                # Delete uploaded content\npinme bind <path> --domain <domain>  # Bind domain (VIP + AppKey required)\npinme export <CID>             # Export as CAR file\npinme set-appkey               # Set/view AppKey\npinme my-domains               # List bound domains\npinme delete <project>          # Delete project (Worker + domain + D1)\npinme logout                   # Log out\n```","schemaVersion":1},"repoUrl":"https://github.com/glitternetwork/pinme/tree/main/skills/pinme","tags":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"pinme","audit":{"files":["package-lock.json","package.json","pnpm-lock.yaml"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS).","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-7q85-xj36-vmfc · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip extraction follows destination symlinks, allowing arbitrary file overwrite.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-vwc7-r8mq-g2x9 · npm:adm-zip@0.5.17","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"adm-zip@0.5.17 has a known vulnerability: adm-zip: Crafted ZIP file triggers 4GB memory allocation.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-xcpc-8h2w-3j85 · npm:adm-zip@0.5.17","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"uuid@9.0.1 has a known vulnerability: uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided.","surface":"package-lock.json, package.json, pnpm-lock.yaml","evidence":"GHSA-w5hq-g745-h8pq · npm:uuid@9.0.1","severity":"medium"}],"packages":16,"auditedAt":"2026-09-25T11:51:56.672Z","lockfiles":["package-lock.json","pnpm-lock.yaml"]},"forks":277,"owner":"glitternetwork","stars":3745,"topics":["ai-tools","claude-code-skill","claude-skills","deployment","deployment-tools","frontend","frontend-deployment","hosting","serverless","skills","static-site","static-site-deploy","static-site-hosting","web-hosting","zero-configuration"],"license":"MIT","fullName":"glitternetwork/pinme","homepage":"https://pinme.eth.limo","language":"TypeScript","pushedAt":"2026-09-12T05:23:49Z","avatarUrl":"https://avatars.githubusercontent.com/u/102277171?v=4","crawledAt":"2026-09-25T11:51:44.477Z","openIssues":7,"manifestFile":"SKILL.md","manifestPath":"skills/pinme/SKILL.md","defaultBranch":"main"},"readme":"# PinMe\n\nZero-config deployment tool: upload static files to IPFS, or create and deploy full-stack web projects (React+Vite + Cloudflare Worker + D1 database). Workers also support sending emails via the PinMe platform API.\n\n## When to Use\n\n```dot\ndigraph pinme_decision {\n    \"User Request\" [shape=doublecircle];\n    \"Needs backend API or database?\" [shape=diamond];\n    \"Upload Files (Path 1)\" [shape=box];\n    \"Full-Stack Project (Path 2)\" [shape=box];\n\n    \"User Request\" -> \"Needs backend API or database?\";\n    \"Needs backend API or database?\" -> \"Upload Files (Path 1)\" [label=\"No\"];\n    \"Needs backend API or database?\" -> \"Full-Stack Project (Path 2)\" [label=\"Yes\"];\n}\n```\n\n## Path 1: Upload Files / Static Sites\n\n> Login required. Use `pinme login` or `pinme set-appkey <AppKey>` before `pinme upload` or `pinme import`.\n\n```dot\ndigraph upload_flow {\n    \"Install/update pinme to latest\" [shape=box];\n    \"Authenticate\" [shape=box];\n    \"Determine build artifacts\" [shape=box];\n    \"pinme upload <path>\" [shape=box];\n    \"Return preview URL\" [shape=doublecircle];\n\n    \"Install/update pinme to latest\" -> \"Authenticate\";\n    \"Authenticate\" -> \"Determine build artifacts\";\n    \"Determine build artifacts\" -> \"pinme upload <path>\";\n    \"pinme upload <path>\" -> \"Return preview URL\";\n}\n```\n\n**1. Check installation and update to latest:**\n```bash\nLOCAL=$(pinme --version 2>/dev/null || echo \"0.0.0\")\nLATEST=$(npm view pinme version)\n[ \"$LOCAL\" != \"$LATEST\" ] && npm install -g pinme@latest || echo \"pinme is up to date ($LOCAL)\"\n```\n\n**2. Authenticate:**\n```bash\npinme login\n# or: pinme set-appkey <AppKey>\n```\n\n**3. Determine upload target** (priority order):\n1. `dist/` — Vite / Vue / React\n2. `build/` — Create React App\n3. `out/` — Next.js static export\n4. `public/` — Plain static files\n\n**4. Upload:**\n```bash\npinme upload <path>\npinme upload ./dist --domain my-site  # Optional: bind subdomain (wallet balance required)\n```\n\n**5. Return** the final URL printed by PinMe to the user. URL priority is: DNS domain > PinMe subdomain > short URL > preview URL. If it falls back to preview, return the **full URL** including all hash characters — do not truncate.\n\n### Common Examples\n\n```bash\npinme upload ./document.pdf          # Single file\npinme upload ./my-folder             # Folder\npinme upload dist                    # Vite/Vue build artifacts\npinme upload build                   # CRA build artifacts\npinme upload out                     # Next.js static export\npinme upload ./dist --domain my-site # Bind PinMe subdomain (wallet balance required)\npinme import ./my-archive.car        # Import CAR file\n```\n\n### Do NOT Upload\n- `node_modules/`, `.env`, `.git/`, `src/`\n- Only upload build artifacts, never upload source code\n\n---\n\n## Path 2: Full-Stack Project\n\n> Login required. Uses React+Vite frontend + Cloudflare Worker backend + D1 SQLite database.\n> When designing frontend projects, use Ant Design as the primary design reference, and prioritize following its conventions for layout, components, spacing, and interaction patterns.\n\n```dot\ndigraph fullstack_flow {\n    \"Install/update pinme to latest\" [shape=box];\n    \"pinme login\" [shape=box];\n    \"pinme create <name>\" [shape=box];\n    \"Modify template code\" [shape=box];\n    \"pinme save\" [shape=box];\n    \"Return preview URL\" [shape=doublecircle];\n\n    \"Install/update pinme to latest\" -> \"pinme login\";\n    \"pinme login\" -> \"pinme create <name>\";\n    \"pinme create <name>\" -> \"Modify template code\";\n    \"Modify template code\" -> \"pinme save\";\n    \"pinme save\" -> \"Return preview URL\";\n}\n```\n\n### Architecture\n\n| Layer | Tech Stack | Deploy Target |\n|-------|-----------|---------------|\n| Frontend | React + Vite (`frontend/`) | IPFS |\n| Backend | Cloudflare Worker (`backend/src/worker.ts`) | `{name}.pinme.pro` |\n| Database | D1 SQLite (`db/*.sql`) | Cloudflare D1 |\n| Object storage | R2 (`env.R2`) | Cloudflare R2 |\n\n### Capability-Specific Skills\n\n- For Worker file uploads, downloads, images, attachments, medi","createdAt":"2026-09-25T11:51:56.766Z","updatedAt":"2026-09-25T11:51:56.766Z"},{"id":"cmugwi43h01sequ06w4x1h2q5","slug":"codeaashu-claude-code-claude-code-skill","name":"claude-code-skill","description":"Development conventions and architecture guide for the Claude Code CLI repository.","authorId":"gh:codeaashu","authorName":"codeaashu","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":3354,"pricePerCall":0,"manifest":{"name":"claude-code-skill","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Development conventions and architecture guide for the Claude Code CLI repository.","permissions":[],"systemPrompt":"# Claude Code — Repository Skill\n\n## Project Overview\n\nClaude Code is Anthropic's CLI tool for interacting with Claude from the terminal. It supports file editing, shell commands, git workflows, code review, multi-agent coordination, IDE integration (VS Code, JetBrains), and Model Context Protocol (MCP).\n\n**Codebase:** ~1,900 files, 512,000+ lines of TypeScript under `src/`.\n\n## Tech Stack\n\n| Component        | Technology                                      |\n|------------------|------------------------------------------------|\n| Language         | TypeScript (strict mode, ES modules)           |\n| Runtime          | Bun (JSX support, `bun:bundle` feature flags)  |\n| Terminal UI      | React + Ink (React for CLI)                    |\n| CLI Parser       | Commander.js (`@commander-js/extra-typings`)   |\n| API Client       | `@anthropic-ai/sdk`                            |\n| Validation       | Zod v4                                         |\n| Linter/Formatter | Biome                                          |\n| Analytics        | GrowthBook (feature flags & A/B testing)       |\n| Protocol         | Model Context Protocol (MCP)                   |\n\n## Architecture\n\n### Directory Map (`src/`)\n\n| Directory        | Purpose                                                         |\n|------------------|-----------------------------------------------------------------|\n| `commands/`      | ~50 slash commands (`/commit`, `/review`, `/config`, etc.)      |\n| `tools/`         | ~40 agent tools (Bash, FileRead, FileWrite, Glob, Grep, etc.)  |\n| `components/`    | ~140 Ink/React UI components for terminal rendering             |\n| `services/`      | External integrations (API, OAuth, MCP, LSP, analytics, plugins)|\n| `bridge/`        | Bidirectional IDE communication layer                           |\n| `state/`         | React context + custom store (AppState)                         |\n| `hooks/`         | React hooks (permissions, keybindings, commands, settings)      |\n| `types/`         | TypeScript type definitions                                     |\n| `utils/`         | Utilities (shell, file ops, permissions, config, git)           |\n| `screens/`       | Full-screen UIs (Doctor, REPL, Resume, Compact)                 |\n| `skills/`        | Bundled skills + skill loader system                            |\n| `plugins/`       | Plugin system (marketplace + bundled plugins)                   |\n| `coordinator/`   | Multi-agent coordination & supervisor logic                     |\n| `tasks/`         | Task management (shell tasks, agent tasks, teammates)           |\n| `context/`       | React context providers (notifications, stats, FPS)             |\n| `memdir/`        | Persistent memory system (CLAUDE.md, user/project memory)       |\n| `entrypoints/`   | Initialization logic, Agent SDK, MCP entry                      |\n| `voice/`         | Voice input/output (STT, keyterms)                              |\n| `vim/`           | Vim mode keybinding support                                     |\n| `schemas/`       | Zod configuration schemas                                       |\n| `keybindings/`   | Keybinding configuration & resolver                             |\n| `migrations/`    | Config migrations between versions                              |\n| `outputStyles/`  | Output formatting & theming                                     |\n| `query/`         | Query pipeline & processing                                     |\n| `server/`        | Server/daemon mode                                              |\n| `remote/`        | Remote session handling                                         |\n\n### Key Files\n\n| File                | Role                                                |\n|---------------------|-----------------------------------------------------|\n| `src/main.tsx`      | CLI entry point (Commander parser, startup profiling)|\n| `src/QueryEngine.ts`| Core LLM API caller (streaming, tool-call loops)    |\n| `src/Tool.ts`       | Tool type definitions & `buildTool` factory          |\n| `src/tools.ts`      | Tool registry & presets                              |\n| `src/commands.ts`   | Command registry                                     |\n| `src/context.ts`    | System/user context collection (git status, memory)  |\n| `src/cost-tracker.ts`| Token cost tracking                                 |\n\n### Entry Points & Initialization Sequence\n\n1. `src/main.tsx` — Commander CLI parser, startup profiling\n2. `src/entrypoints/init.ts` — Config, telemetry, OAuth, MDM\n3. `src/entrypoints/cli.tsx` — CLI session orchestration\n4. `src/entrypoints/mcp.ts` — MCP server mode\n5. `src/entrypoints/sdk/` — Agent SDK (programmatic API)\n6. `src/replLauncher.tsx` — REPL session launcher\n\nStartup performs parallel initialization: MDM policy reads, Keychain prefetch, feature flag checks, then core init.\n\n## Patterns & Conventions\n\n### Tool Definition\n\nEach tool lives in `src/tools/{ToolName}/` and uses `buildTool`:\n\n```typescript\nexport const MyTool = buildTool({\n  name: 'MyTool',\n  aliases: ['my_tool'],\n  description: 'What this tool does',\n  inputSchema: z.object({\n    param: z.string(),\n  }),\n  async call(args, context, canUseTool, parentMessage, onProgress) {\n    // Execute and return { data: result, newMessages?: [...] }\n  },\n  async checkPermissions(input, context) { /* Permission checks */ },\n  isConcurrencySafe(input) { /* Can run in parallel? */ },\n  isReadOnly(input) { /* Non-destructive? */ },\n  prompt(options) { /* System prompt injection */ },\n  renderToolUseMessage(input, options) { /* UI for invocation */ },\n  renderToolResultMessage(content, progressMessages, options) { /* UI for result */ },\n})\n```\n\n**Directory structure per tool:** `{ToolName}.ts` or `.tsx` (main), `UI.tsx` (rendering), `prompt.ts` (system prompt), plus utility files.\n\n### Command Definition\n\nCommands live in `src/commands/` and follow three types:\n\n- **PromptCommand** — Sends a formatted prompt with injected tools (most commands)\n- **LocalCommand** — Runs in-process, returns text\n- **LocalJSXCommand** — Runs in-process, returns React JSX\n\n```typescript\nconst command = {\n  type: 'prompt',\n  name: 'my-command',\n  description: 'What this command does',\n  progressMessage: 'working...',\n  allowedTools: ['Bash(git *)', 'FileRead(*)'],\n  source: 'builtin',\n  async getPromptForCommand(args, context) {\n    return [{ type: 'text', text: '...' }]\n  },\n} satisfies Command\n```\n\nCommands are registered in `src/commands.ts` and invoked via `/command-name` in the REPL.\n\n### Component Structure\n\n- Functional React components with Ink primitives (`Box`, `Text`, `useInput()`)\n- Styled with Chalk for terminal colors\n- React Compiler for optimized re-renders\n- Design system primitives in `src/components/design-system/`\n\n### State Management\n\n- `AppState` via React context + custom store (`src/state/AppStateStore.ts`)\n- Mutable state object passed to tool contexts\n- Selector functions for derived state\n- Change observers in `src/state/onChangeAppState.ts`\n\n### Permission System\n\n- **Modes:** `default` (prompt per operation), `plan` (show plan, ask once), `bypassPermissions` (auto-approve), `auto` (ML classifier)\n- **Rules:** Wildcard patterns — `Bash(git *)`, `FileEdit(/src/*)`\n- Tools implement `checkPermissions()` returning `{ granted: boolean, reason?, prompt? }`\n\n### Feature Flags & Build\n\nBun's `bun:bundle` feature flags enable dead-code elimination at build time:\n\n```typescript\nimport { feature } from 'bun:bundle'\nif (feature('PROACTIVE')) { /* proactive agent tools */ }\n```\n\nNotable flags: `PROACTIVE`, `KAIROS`, `BRIDGE_MODE`, `VOICE_MODE`, `COORDINATOR_MODE`, `DAEMON`, `WORKFLOW_SCRIPTS`.\n\nSome features are also gated via `process.env.USER_TYPE === 'ant'`.\n\n## Naming Conventions\n\n| Element      | Convention           | Example                          |\n|-------------|---------------------|----------------------------------|\n| Files       | PascalCase (exports) or kebab-case (commands) | `BashTool.tsx`, `commit-push-pr.ts` |\n| Components  | PascalCase           | `App.tsx`, `PromptInput.tsx`     |\n| Types       | PascalCase, suffix with Props/State/Context | `ToolUseContext`     |\n| Hooks       | `use` prefix         | `useCanUseTool`, `useSettings`   |\n| Constants   | SCREAMING_SNAKE_CASE | `MAX_TOKENS`, `DEFAULT_TIMEOUT_MS`|\n\n## Import Practices\n\n- ES modules with `.js` extensions (Bun convention)\n- Lazy imports for circular dependency breaking: `const getModule = () => require('./heavy.js')`\n- Conditional imports via feature flags or `process.env`\n- `biome-ignore` markers for manual import ordering where needed\n\n## Services\n\n| Service             | Path                          | Purpose                           |\n|--------------------|-------------------------------|-----------------------------------|\n| API                | `services/api/`               | Anthropic SDK client, file uploads|\n| MCP                | `services/mcp/`               | MCP client, tool/resource discovery|\n| OAuth              | `services/oauth/`             | OAuth 2.0 auth flow               |\n| LSP                | `services/lsp/`               | Language Server Protocol manager   |\n| Analytics          | `services/analytics/`         | GrowthBook, telemetry, events     |\n| Plugins            | `services/plugins/`           | Plugin loader, marketplace         |\n| Compact            | `services/compact/`           | Context compression                |\n| Policy Limits      | `services/policyLimits/`      | Org rate limits, quota checking    |\n| Remote Settings    | `services/remoteManagedSettings/` | Managed settings sync (Enterprise) |\n| Token Estimation   | `services/tokenEstimation.ts` | Token count estimation             |\n\n## Configuration\n\n**Settings locations:**\n- **Global:** `~/.claude/config.json`, `~/.claude/settings.json`\n- **Project:** `.claude/config.json`, `.claude/settings.json`\n- **System:** macOS Keychain + MDM, Windows Registry + MDM\n- **Managed:** Remote sync for Enterprise users\n\n## Guidelines\n\n1. Read relevant source files before making changes — understand existing patterns first.\n2. Follow the tool/command/component patterns above when adding new ones.\n3. Keep edits minimal and focused — avoid unnecessary refactoring.\n4. Use Zod for all input validation at system boundaries.\n5. Gate experimental features behind `bun:bundle` feature flags or env checks.\n6. Respect the permission system — tools that modify state must implement `checkPermissions()`.\n7. Use lazy imports when adding dependencies that could create circular references.\n8. Update this file as project conventions evolve.","schemaVersion":1},"repoUrl":"https://github.com/codeaashu/claude-code","tags":["aashuu","claude","claude-ai","claude-code","claude-code-leaked","claude-code-skill","claude-desktop","claude-leak","claude-skills","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"claude-code","audit":{"files":["bun.lock","package-lock.json","package.json"],"binaries":[],"findings":[{"kind":"dependency","rule":"DP-01","message":"@opentelemetry/core@1.30.1 has a known vulnerability: OpenTelemetry Core: Unbounded memory allocation in W3C Baggage propagation.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-8988-4f7v-96qf · npm:@opentelemetry/core@1.30.1","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: axios Vulnerable to Full Man-in-the-Middle via Prototype Pollution Gadget in `config.proxy`.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-35jp-ww65-95wh · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollution Gadget in Config Merge.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-3g43-6gmg-66jw · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: Axios has a NO_PROXY Hostname Normalization Bypass that Leads to SSRF.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-3p68-rc4w-qgx5 · npm:axios@1.14.0","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: Axios: Invisible JSON Response Tampering via Prototype Pollution Gadget in `parseReviver`.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-3w6x-2g7m-8v23 · npm:axios@1.14.0","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: Axios: Excessive recursion in formDataToJSON can cause denial of service.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-42h9-826w-cgv3 · npm:axios@1.14.0","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: Axios: CRLF Injection in multipart/form-data body via unsanitized blob.type in formDataToStream.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-445q-vr5w-6q77 · npm:axios@1.14.0","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: Axios' HTTP adapter-streamed uploads bypass maxBodyLength when maxRedirects: 0.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-5c9x-8gcm-mpgx · npm:axios@1.14.0","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: Axios: unbounded recursion in toFormData causes DoS via deeply nested request data.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-62hf-57xw-28j9 · npm:axios@1.14.0","severity":"medium"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability: Axios: Header Injection via Prototype Pollution.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-6chq-wfr3-2hj9 · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-777c-7fjr-54vf · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-7q8q-rj6j-mhjq · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-898c-q2cr-xwhg · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-fvcv-3m26-pcqx · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-hfxv-24rg-xrqf · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-j5f8-grm9-p9fc · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-jqh4-m9w3-8hp9 · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-m7pr-hjqh-92cm · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-mmx7-hfxf-jppx · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-mwf2-3pr3-8698 · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-p92q-9vqr-4j8v · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-pf86-5x62-jrwf · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-pmv8-rq9r-6j72 · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-pmwg-cvhr-8vh7 · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-q8qp-cvcw-x6jj · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-vf2m-468p-8v99 · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-w9j2-pvgh-6h63 · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-xhjh-pmcv-23jw · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"axios@1.14.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-xx6v-rp6x-q39c · npm:axios@1.14.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"diff@7.0.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-73rr-hh4g-fpgx · npm:diff@7.0.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"lodash-es@4.17.23 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-f23m-r3pf-42rh · npm:lodash-es@4.17.23","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"lodash-es@4.17.23 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-r5fr-rjxr-66jc · npm:lodash-es@4.17.23","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-35p6-xmwp-9g52 · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-4cwx-7wf7-3272 · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-8xcm-r25x-g524 · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-g8m3-5g58-fq7m · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-hm92-r4w5-c3mj · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-jr45-8vmc-qm54 · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-m8rv-5g2x-5cg5 · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-p88m-4jfj-68fv · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-pr7r-676h-xcf6 · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-v3r7-h72x-cjcm · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-vmh5-mc38-953g · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"undici@7.24.6 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-vxpw-j846-p89q · npm:undici@7.24.6","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"ws@8.20.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-58qx-3vcg-4xpx · npm:ws@8.20.0","severity":"high"},{"kind":"dependency","rule":"DP-01","message":"ws@8.20.0 has a known vulnerability.","surface":"bun.lock, package-lock.json, package.json","evidence":"GHSA-96hv-2xvq-fx4p · npm:ws@8.20.0","severity":"high"}],"packages":49,"auditedAt":"2026-09-25T11:52:11.817Z","lockfiles":["bun.lock","package-lock.json"]},"forks":3674,"owner":"codeaashu","stars":3354,"topics":["aashuu","claude","claude-ai","claude-code","claude-code-leaked","claude-code-skill","claude-desktop","claude-leak","claude-skills"],"license":null,"fullName":"codeaashu/claude-code","homepage":null,"language":"TypeScript","pushedAt":"2026-08-29T20:16:27Z","avatarUrl":"https://avatars.githubusercontent.com/u/130897584?v=4","crawledAt":"2026-09-25T11:51:57.794Z","openIssues":16,"manifestFile":"SKILL.md","manifestPath":"Skill.md","defaultBranch":"main"},"readme":"# Claude Code — Repository Skill\n\n## Project Overview\n\nClaude Code is Anthropic's CLI tool for interacting with Claude from the terminal. It supports file editing, shell commands, git workflows, code review, multi-agent coordination, IDE integration (VS Code, JetBrains), and Model Context Protocol (MCP).\n\n**Codebase:** ~1,900 files, 512,000+ lines of TypeScript under `src/`.\n\n## Tech Stack\n\n| Component        | Technology                                      |\n|------------------|------------------------------------------------|\n| Language         | TypeScript (strict mode, ES modules)           |\n| Runtime          | Bun (JSX support, `bun:bundle` feature flags)  |\n| Terminal UI      | React + Ink (React for CLI)                    |\n| CLI Parser       | Commander.js (`@commander-js/extra-typings`)   |\n| API Client       | `@anthropic-ai/sdk`                            |\n| Validation       | Zod v4                                         |\n| Linter/Formatter | Biome                                          |\n| Analytics        | GrowthBook (feature flags & A/B testing)       |\n| Protocol         | Model Context Protocol (MCP)                   |\n\n## Architecture\n\n### Directory Map (`src/`)\n\n| Directory        | Purpose                                                         |\n|------------------|-----------------------------------------------------------------|\n| `commands/`      | ~50 slash commands (`/commit`, `/review`, `/config`, etc.)      |\n| `tools/`         | ~40 agent tools (Bash, FileRead, FileWrite, Glob, Grep, etc.)  |\n| `components/`    | ~140 Ink/React UI components for terminal rendering             |\n| `services/`      | External integrations (API, OAuth, MCP, LSP, analytics, plugins)|\n| `bridge/`        | Bidirectional IDE communication layer                           |\n| `state/`         | React context + custom store (AppState)                         |\n| `hooks/`         | React hooks (permissions, keybindings, commands, settings)      |\n| `types/`         | TypeScript type definitions                                     |\n| `utils/`         | Utilities (shell, file ops, permissions, config, git)           |\n| `screens/`       | Full-screen UIs (Doctor, REPL, Resume, Compact)                 |\n| `skills/`        | Bundled skills + skill loader system                            |\n| `plugins/`       | Plugin system (marketplace + bundled plugins)                   |\n| `coordinator/`   | Multi-agent coordination & supervisor logic                     |\n| `tasks/`         | Task management (shell tasks, agent tasks, teammates)           |\n| `context/`       | React context providers (notifications, stats, FPS)             |\n| `memdir/`        | Persistent memory system (CLAUDE.md, user/project memory)       |\n| `entrypoints/`   | Initialization logic, Agent SDK, MCP entry                      |\n| `voice/`         | Voice input/output (STT, keyterms)                              |\n| `vim/`           | Vim mode keybinding support                                     |\n| `schemas/`       | Zod configuration schemas                                       |\n| `keybindings/`   | Keybinding configuration & resolver                             |\n| `migrations/`    | Config migrations between versions                              |\n| `outputStyles/`  | Output formatting & theming                                     |\n| `query/`         | Query pipeline & processing                                     |\n| `server/`        | Server/daemon mode                                              |\n| `remote/`        | Remote session handling                                         |\n\n### Key Files\n\n| File                | Role                                                |\n|---------------------|-----------------------------------------------------|\n| `src/main.tsx`      | CLI entry point (Commander parser, startup profiling)|\n| `src/QueryEngine.ts`| Core LLM API caller (streaming, tool-call loops)    |\n| `src/Tool.ts`       | T","createdAt":"2026-09-25T11:52:11.837Z","updatedAt":"2026-09-25T11:52:11.837Z"},{"id":"cmugwi4z001shqu06knr2ah0v","slug":"appllama-appllama-skills-appllama","name":"Appllama","description":"A builder, not just a researcher. Agent skills that turn top-grossing app patterns into native-quality mobile screens.","authorId":"gh:appllama","authorName":"Appllama","version":"0.1.0","category":"MCP","securityLevel":"Community","downloadsCount":0,"githubStars":2056,"pricePerCall":0,"manifest":{"name":"Appllama","tools":[],"category":"MCP","entrypoint":{"url":"https://mcp.appllama.io/mcp","type":"mcp-sse"},"description":"","permissions":["network"],"requiredEnv":[],"schemaVersion":1},"repoUrl":"https://github.com/Appllama/appllama-skills","tags":["agent-skills","ai-agents","claude","claude-code","claude-code-skill","claude-skills","codex","codex-skill","cursor","design-system","expo","mcp"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"appllama-skills","audit":{"files":[],"binaries":[],"findings":[],"packages":0,"auditedAt":"2026-09-25T11:52:12.962Z","lockfiles":[]},"forks":97,"owner":"Appllama","stars":2056,"topics":["agent-skills","ai-agents","claude","claude-code","claude-code-skill","claude-skills","codex","codex-skill","cursor","design-system","expo","mcp","mobile-app-development","mobile-design","mobile-ui","model-context-protocol","react-native","skills","ui-design"],"license":"MIT","fullName":"Appllama/appllama-skills","homepage":"https://appllama.io/mcp","language":null,"pushedAt":"2026-09-06T09:01:39Z","avatarUrl":"https://avatars.githubusercontent.com/u/313732614?v=4","crawledAt":"2026-09-25T11:52:12.306Z","openIssues":1,"manifestFile":".mcp.json","manifestPath":".mcp.json","defaultBranch":"main"},"readme":"<p align=\"center\">\n  <a href=\"https://appllama.io\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://public.appllama.io/appllama-logo-dark.png\">\n      <img src=\"https://public.appllama.io/appllama-logo-light.png\" alt=\"Appllama\" width=\"360\">\n    </picture>\n  </a>\n</p>\n\n<h3 align=\"center\">A builder, not just a researcher.</h3>\n\n<p align=\"center\">\n  Agent skills that make AI agents genuinely good at building mobile apps —<br>\n  studied against the top-grossing apps, finished to a simulator-verified bar.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://skills.sh/appllama/appllama-skills\"><img src=\"https://skills.sh/b/appllama/appllama-skills\" alt=\"skills.sh installs\"></a>\n  <a href=\"https://appllama.io\"><img src=\"https://img.shields.io/badge/Appllama-official-1a1a1a\" alt=\"Appllama official\"></a>\n  <a href=\"LICENSE\"><img src=\"https://img.shields.io/badge/license-MIT-blue\" alt=\"License: MIT\"></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://appllama.io\">appllama.io</a> ·\n  <a href=\"https://appllama.io/mcp\">MCP</a> ·\n  <a href=\"https://x.com/appllamaio\">X</a> ·\n  <a href=\"https://www.linkedin.com/company/appllama\">LinkedIn</a> ·\n  <a href=\"https://www.producthunt.com/products/appllama\">Product Hunt</a>\n</p>\n\n---\n\n[Appllama](https://appllama.io) is the design library of top-grossing mobile\napps — their real screens, flows, and UI patterns, with revenue and download\ncontext. These skills turn that library into an agent's working method:\nstudy every screen of the apps that already win, extract the category's\ndesign language, then build screens that hold up next to them.\n\n## The skills\n\n| Skill | What it does |\n|---|---|\n| [`appllama-usage`](skills/appllama-usage/SKILL.md) | The research engine: how to use the [Appllama MCP](https://appllama.io/mcp) like a design director — the full tool map, and the playbooks for building an app from scratch, improving an existing screen, and flow & element research. |\n| [`appllama-app-design-skill`](skills/appllama-app-design-skill/SKILL.md) | The build bar: native-feeling Expo / React Native screens — Apple HIG fidelity, semantic colors, native controls, anti-slop discipline, navigation that behaves (push vs replace, sheets and overlays, the one-way doors where back must not exist), a strict motion bar (should it animate at all, springs that carry the finger's velocity, nothing on the JS thread), generated image assets, and a full-motion simulator loop (whole flows recorded and scrubbed frame by frame, not screenshots). |\n\nThey are designed as a pair: **usage** decides what to study, **design**\ndecides how to build, and both insist the loop only ends in a simulator\nwith a screen you can't fault.\n\n## Install\n\nOne command, from your project root — works with Claude Code, Cursor,\nCodex, and [70+ other agents](https://skills.sh):\n\n```bash\nnpx skills@latest add appllama/appllama-skills\n```\n\nVariations:\n\n```bash\n# install for specific agents, no prompts\nnpx skills@latest add appllama/appllama-skills -a claude-code -a cursor -y\n\n# install user-wide instead of per-project\nnpx skills@latest add appllama/appllama-skills -g\n```\n\n### Only want the app design skill?\n\n`appllama-app-design-skill` stands on its own — the native-quality build\nbar, anti-slop discipline, and the full-motion simulator loop work with or\nwithout the Appllama MCP connected:\n\n```bash\nnpx skills@latest add appllama/appllama-skills --skill appllama-app-design-skill\n```\n\n(The same `--skill` flag installs only `appllama-usage` if you want just the\nresearch engine.)\n\n<details>\n<summary>Manual install</summary>\n\nSkills are plain directories — copy them into your agent's skills folder\n(`.claude/skills/` per project, `~/.claude/skills/` user-wide, or your\nharness's equivalent):\n\n```bash\ngit clone https://github.com/appllama/appllama-skills\ncp -r appllama-skills/skills/* ~/.claude/skills/\n```\n\n</details>\n\n## Connect the Appllama MCP\n\n`appllama-usage` runs on the Appllama MCP; `appllama-app-design-skill` is\nsharper with it co","createdAt":"2026-09-25T11:52:12.972Z","updatedAt":"2026-09-25T11:52:12.972Z"},{"id":"cmugwi4z901skqu069lq2pxgq","slug":"appllama-appllama-skills-appllama-app-design-skill","name":"appllama-app-design-skill","description":"Build native-feeling, benchmark-quality mobile app screens (Expo / React Native). Use when designing or implementing any mobile UI — screens, flows, onboarding, paywalls, tab bars, sheets, settings, empty states — or when polishing motion, navigation, typography, dark mode, or perceived performance. Enforces Apple HIG fidelity, semantic colors, native controls, anti-slop discipline, navigation semantics (push vs replace, modal vs sheet vs overlay, the one-way doors where back must not exist), purposeful Reanimated motion, a full-motion simulator-verified iteration loop, and a study-real-apps-first workflow (pairs with the Appllama MCP). Trigger on \"build a screen\", \"make this screen better\", \"design the onboarding\", \"wire up this flow\", \"polish the UI\", \"make it feel native\", or any mobile design/implementation task.","authorId":"gh:appllama","authorName":"Appllama","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":2056,"pricePerCall":0,"manifest":{"name":"appllama-app-design-skill","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Build native-feeling, benchmark-quality mobile app screens (Expo / React Native). Use when designing or implementing any mobile UI — screens, flows, onboarding, paywalls, tab bars, sheets, settings, empty states — or when polishing motion, navigation, typography, dark mode, or perceived performance. Enforces Apple HIG fidelity, semantic colors, native controls, anti-slop discipline, navigation semantics (push vs replace, modal vs sheet vs overlay, the one-way doors where back must not exist), purposeful Reanimated motion, a full-motion simulator-verified iteration loop, and a study-real-apps-first workflow (pairs with the Appllama MCP). Trigger on \"build a screen\", \"make this screen better\", \"design the onboarding\", \"wire up this flow\", \"polish the UI\", \"make it feel native\", or any mobile design/implementation task.","permissions":[],"systemPrompt":"# Appllama App Design Skill\n\nYou are building screens that will sit on a phone next to the best-designed apps\nin the world. The user will compare your output to those apps within seconds of\nlaunching it. This skill defines the bar and the method for clearing it.\n\n## The Prime Directive: study before you draw\n\nNever design a screen from imagination when you can study how top apps solved\nthe same screen. Real, shipping, revenue-ranked apps encode thousands of hours\nof design iteration and A/B testing. Your first move on any screen is research:\n\n1. If the **Appllama MCP** is connected, pull real screens for the category and\n   screen type you are building (see the `appllama-usage` skill for the exact\n   research playbooks). Study 20–30 screens before writing a line of UI code.\n2. Extract the **pattern, not the pixels**: layout skeleton, information\n   hierarchy, control choices, spacing rhythm, where the primary CTA sits, what\n   gets an illustration vs. plain text, how progress is communicated.\n   Note: every Appllama image and video carries a small Appllama watermark in\n   the top-left corner. It is provenance, not design — ignore it when reading\n   a screen (it may sit over the status bar or a back button) and never\n   reproduce it in anything you build.\n3. Then design **your** screen: same proven skeleton, your product's voice.\n   Copying a competitor's screen 1:1 is both lazy and legally risky; shipping a\n   screen that ignores every convention users already know is worse.\n\n## Platform baseline\n\nDefault stack assumptions (override only if the project already differs):\n\n- **Expo + Expo Router**, React Native, TypeScript.\n- `react-native-reanimated` for motion, `react-native-gesture-handler` for\n  gestures, `@shopify/flash-list` (or FlashList v2) for any list that can grow.\n- `expo-image` for images (and SF Symbols via `source=\"sf:name\"` on iOS),\n  `expo-video` / `expo-audio` (never the deprecated `expo-av`).\n- `react-native-safe-area-context` for insets. Never hard-code notch numbers.\n- `process.env.EXPO_OS` over `Platform.OS` for compile-time platform checks.\n\n## Native fidelity laws\n\nThese are the details that separate \"web page in a wrapper\" from \"native app\".\nViolating any of them is a finding, not a style preference.\n\n1. **Semantic colors, both themes, day one.** Use system/semantic color tokens\n   (e.g. `Color` from `expo-router` on iOS: `Color.ios.label`,\n   `Color.ios.secondarySystemBackground`; Material dynamic colors on Android).\n   Every screen must render correctly in light AND dark before it is \"done\".\n   Never pass semantic color objects into Reanimated animated styles — resolve\n   to strings first.\n2. **Native controls over rebuilt ones.** Switch, Slider, SegmentedControl,\n   context menus, date pickers: use the native control or a faithful wrapper.\n   A rebuilt toggle that animates 50 ms differently than iOS's reads as fake\n   instantly.\n3. **SF Symbols / Material Symbols for iconography.** On iOS prefer SF Symbols\n   (`expo-image` with `sf:` sources, or `expo-symbols`); they inherit weight,\n   optical size, and Dynamic Type behavior. Do not mix three icon families on\n   one screen.\n4. **Typography is hierarchy.** Use the platform type ramp (Large Title / Title\n   / Headline / Body / Footnote on iOS). One display size per screen. Tabular\n   numerals (`fontVariant: ['tabular-nums']`) for anything that counts, times,\n   or prices. `Text selectable` on data users may want to copy.\n5. **Continuous corners.** `borderCurve: 'continuous'` on every rounded\n   rectangle. Squircles are the single cheapest \"feels iOS\" win that exists.\n6. **Shadows via CSS `boxShadow`**, not legacy `shadow*`/`elevation` props.\n   Shadows are for elevation logic, not decoration — one elevation system per\n   app.\n7. **Spacing rhythm.** Pick a base unit (4 or 8) and never leave it. Prefer\n   flexbox `gap` over margin stacking. ScrollView padding goes in\n   `contentContainerStyle`, never on the ScrollView itself.\n8. **Safe areas and the Dynamic Island are part of the design.** Screens must\n   be verified with content scrolled under the island / status bar (does the\n   blur/fade treatment hold?), with the home indicator (does the bottom CTA\n   clear it?), and in landscape if supported.\n9. **Navigation titles belong to the navigator.** Use the stack's native title\n   (and large-title collapse behavior on iOS) rather than a hand-rolled header\n   whenever possible.\n10. **Haptics are punctuation.** Selection tick when a value passes a step,\n    light impact when something snaps home, notification success/error for\n    outcomes — on the same frame as the visual, one per user action, never\n    the only feedback. Never on scroll, never in loops.\n11. **Format numbers like a product, not a database**: 1.4M, 38k, $4.99. Trim\n    trailing zeros. Localize dates.\n12. **Root scroll behavior**: screens that can ever overflow wrap content in a\n    ScrollView (first component in the route) with\n    `contentInsetAdjustmentBehavior=\"automatic\"`. Use `useWindowDimensions`,\n    never `Dimensions.get()`.\n\n## Navigation laws\n\nNavigation is the part of a screen a screenshot can't show, and users feel\nit in ten seconds. Every transition answers three questions: what is the\ndestination to here, must the user be able to come back, and what does back\n(chevron, iOS edge swipe, Android hardware back) do afterwards.\n\n1. **Push goes deeper, replace moves on.** `router.push` when the user will\n   want to return here; `router.replace` / `<Redirect>` when coming back\n   would land in a state the world has moved past; `router.dismissTo(href)`\n   for \"finish this flow and land on X\". Back undoes *navigation*, never\n   *events*.\n2. **Presentation is meaning.** A self-contained task with steps →\n   `presentation: 'modal'` with its own stack and its own Cancel/Done; a\n   short interruption (picker, filters, item options) → `formSheet` with\n   detents, drag-to-dismiss; immersive content → `fullScreenModal` with an\n   explicit Close; something floating over a still-visible screen (confirm\n   card, lightbox, coach mark) → `transparentModal` overlay; destructive\n   confirms → action sheet; item actions → native context menu; share /\n   web / photo picking → the system controller, never a rebuilt route. A\n   sheet that grows a second step was a modal all along; if a link could\n   open it, it is a route, not a `useState` sheet.\n3. **One-way doors leave the stack.** Sign-in on a wall app, finished\n   onboarding (Skip included), a purchase, a completed session: guard with\n   `Stack.Protected` and land with `replace`, so back can never re-enter\n   the old state — Android back from home exits the app, never shows\n   Login; a paid paywall never re-opens. But keep the user's *place*:\n   sign-in demanded by one action (save, follow, buy) is a modal over the\n   screen that completes the action where it was tapped, and a paywall\n   opened from a feature dismisses back onto the feature, unlocked — never\n   `replace('/(tabs)')` from there.\n4. **Back is blocked in exactly two cases** — an irreversible request in\n   flight (seconds, with visible progress) and unsaved work in a modal\n   (ask first), both via `usePreventRemove` on the modal's root screen.\n   Transient in-screen state (selection mode, an expanded search, an open\n   in-screen sheet) consumes the first back, then back leaves. Anything\n   else that traps back — a funnel, a rating prompt — is a defect; the\n   edge swipe works everywhere else.\n5. **Tabs are peers.** No slide between tabs, each tab keeps its own stack,\n   re-tapping the active tab pops to its root; full-attention screens\n   (composer, player, checkout) live in the root stack *above* the tabs.\n   Deep links land with a real stack underneath (`initialRouteName` /\n   `withAnchor`); cold start lands by state, splash held until session\n   state has resolved — never a Login flash before Home.\n6. **Study the grammar, not just the pixels.** Walking a winning flow on\n   Appllama, note what each step *is* — push, modal, sheet — and copy that\n   consistency.\n\n## Anti-slop laws\n\nAI-built apps share a look, and users file it under \"template\" within seconds.\nEach of these is a *default ban* — there is always an override when the brand\nexplicitly asks for the thing AND you can articulate why it fits this product.\n\n1. **No AI-default styling.** Purple/indigo gradient CTAs with a glow,\n   glassmorphism on every card, mesh-gradient heroes, confetti for minor\n   events, sparkles in headings — that is the model's house style, not\n   design. Your palette, materials, and layout come from the reference\n   screens you studied, never from the priors you'd reach for unprompted.\n2. **One accent, locked.** Pick one accent color and it is THE accent on\n   every screen — no blue CTA on one screen and teal on the next, no new hue\n   appearing in screen seven. Neutrals carry the app; the accent is spent\n   where the money is (primary action, active state, progress).\n3. **One grey family.** Warm greys or cool greys — never both in one app.\n4. **Shape lock.** One corner-radius scale, stated as a rule (\"actions are\n   pills, cards 16, inputs 8\") and never violated. Mixed radii without a\n   stated rule read as assembled-from-parts.\n5. **No emoji as iconography.** Icons are SF Symbols / Material Symbols\n   (fidelity law 3). Emoji appear only when the product's voice is genuinely\n   chat-native or playful — sparingly, in content, never in chrome.\n6. **One label per intent.** \"Get started\", \"Start now\", and \"Begin\" are the\n   same intent — pick one phrasing and use it everywhere it appears.\n7. **Emphasis stays in the family.** Emphasize a word with weight or italic\n   of the same typeface; injecting a serif word into a sans headline (or vice\n   versa) for visual interest is amateur.\n8. **Ship full state cycles, not the happy path.** Static-successful-state-\n   only is the default failure mode: skeletons must match the final layout's\n   shape, empty states are composed (and say how to fill them), errors are\n   inline and specific.\n9. **The slop pre-flight is mechanical.** Before any flow reaches the\n   simulator pass, count: distinct accent hues (must be 1), distinct corner\n   radii (all from the stated scale), emoji in UI chrome (0), gradients\n   without a brand reason (0), duplicate labels for one intent (0). A failed\n   count is a fix, not a judgment call.\n\n## Motion laws\n\nMotion is the highest-leverage polish surface and the easiest to overdo.\nDecide in this order:\n\n- **The frequency gate comes first.** Met 100+ times a day (tab switch,\n  keyboard, scroll, back) → the platform default and nothing else; tens a\n  day (press, row select) → near-imperceptible, under 150 ms; occasional\n  (sheets, modals, toasts) → standard motion; delight only on rare,\n  first-time moments. Tabs never slide; screen transitions stay native.\n  Passing this gate with zero lines of code is a success — when unsure,\n  the strongest move is to delete the animation.\n- **Name the purpose in one word** — feedback, spatial continuity, state\n  change, preventing a jarring cut, explanation, delight — or don't build\n  it. Data the user is reading never moves for style.\n- **If a finger was involved, it's a spring.** Start from the live value\n  (capture it on grab), hand the release velocity into the spring, pick\n  the target from projected momentum so a flick commits, rubber-band past\n  boundaries, stay grabbable mid-flight. One vocabulary per app —\n  `{ duration: 400, dampingRatio: 1 }` to settle, `{ 300, 0.8 }` for\n  sheets — and bounce only when the gesture carried momentum.\n- **Everything else is timing, under 300 ms, strong ease-out**\n  (`Easing.bezier(0.23, 1, 0.32, 1)` — built-in curves are too weak; never\n  ease-in on an entrance). Press feedback lands on press-*in*, 100–150 ms:\n  scale 0.97 on buttons and cards, a background highlight (never scale) on\n  list rows, opacity on bar buttons. Exits are faster than entrances and\n  leave the way they came in; enter from `scale(0.95)` + fade, never\n  `scale(0)`; menus grow from their trigger (centered modals exempt).\n- **Gesture → animation never hops the JS thread.** Worklets + shared\n  values (`.get()`/`.set()`; `scheduleOnRN` — Reanimated 4's `runOnJS` —\n  only at gesture end), `transform`/`opacity` only, no `entering` on\n  recycled list rows, never animate a header's height (translate inside a\n  fixed clip), keyboard-tracking UI via `react-native-keyboard-controller`\n  — never a keyboard listener plus a guessed duration.\n- **Respect Reduce Motion**: your spatial motion collapses to cross-fades;\n  native transitions stay the system's.\n- The bar: 60 fps through the hero flow, measured on a **release build on\n  the slowest device you support** — Expo Go and dev builds hide exactly\n  the jank you're hunting\n  ([references/performance.md](references/performance.md)). Watch the\n  recording once for feel, once frame by frame, and again next day with\n  fresh eyes.\n\n## State architecture\n\nScreens that feel great are screens whose state is boring:\n\n- **Server state** in TanStack Query (or the project's equivalent): caching,\n  retries, optimistic updates. Never `useEffect`+`fetch`.\n- **Client state** in a small atomic store (Zustand/Jotai). Broad \"app state\"\n  contexts cause the re-render cascades that make UIs feel heavy.\n- **Ephemeral UI state** (open/closed, focus, scroll) stays local to the\n  component.\n- **Optimistic by default**: taps reflect instantly, reconcile in the\n  background, roll back loudly on failure.\n- Uncontrolled `TextInput`s for high-frequency typing surfaces; controlled\n  inputs are a top-3 cause of typing jank.\n- Persist tiny client state in MMKV, not AsyncStorage, when latency shows.\n\n## Perceived performance\n\n- Skeletons only for content whose shape you know; otherwise progressive\n  reveal. Never a full-screen spinner for a partial update.\n- FlashList for every list; give stable keys.\n- Preload the next screen's data on press-in, not on navigation-complete.\n- Images: right-size sources, `expo-image` with `recyclingKey` in lists,\n  thumbhash/blurhash placeholders.\n- Cold-start TTI and bundle discipline live in\n  [references/performance.md](references/performance.md) — apply the\n  measure → optimize → re-measure loop, never blind memoization.\n\n## Image & illustration assets\n\nWhen a screen calls for illustration, empty-state art, hero imagery, or icons\nbeyond the symbol set:\n\n- Generate assets with the **best image model available to you** (e.g. an\n  imagegen tool or the Higgsfield MCP/CLI if connected) at the **highest\n  quality settings**, then downscale to @1x/@2x/@3x. Never upscale.\n- One visual language per app: pick a style (gradient-mesh, flat-duotone,\n  3D-clay, hand-drawn, mascot style) and generate ALL assets in that same style, same\n  palette, same lighting. A mixed-style asset set reads as template slop.\n- Prompt for **transparent or solid-flat backgrounds** matched to your surface\n  color; composite artifacts (white halos, wrong-color mattes) are an\n  automatic redo.\n- Full asset pipeline and prompt patterns:\n  [references/image-assets.md](references/image-assets.md).\n\n## The simulator loop (non-negotiable)\n\nA screen does not exist until you have seen it running. The loop:\n\n1. Implement → launch in the iOS Simulator (or Android emulator).\n2. Screenshot and **actually look**: alignment, optical centering, spacing\n   rhythm, truncation with long content, dark mode, Dynamic Type at XL.\n3. Run the **full-motion pass** below — screenshots prove layout; they prove\n   nothing about motion.\n4. Fix, relaunch, re-verify. Repeat until you cannot find a defect — then run\n   the checklist in [references/simulator-loop.md](references/simulator-loop.md)\n   once more.\n\nDo not declare a screen finished from code review alone. Do not stop at \"looks\nfine\" — stop at \"cannot find a flaw at 100% zoom\".\n\n### The full-motion pass (mandatory, per flow)\n\nEvery flow is evaluated as **moving pictures in the simulator, never as\nstills**. Screen-record the entire flow end to end\n(`xcrun simctl io booted recordVideo flow.mov`), exercising ALL of it:\n\n- every screen transition, push/pop, tab switch\n- every back path — chevron, edge swipe, Android hardware back — and, after\n  each one-way door (sign-in, onboarding done, purchase, finished session),\n  an attempt to go back that must fail to re-enter the old state\n- every modal and sheet: present, drag, dismiss — and cancel mid-drag\n- the keyboard, both directions: appear (does the layout glide, is the\n  focused input visible?) and dismiss (does anything jump-cut?)\n- every user interaction: press states, gesture follow-through, interrupted\n  gestures, rapid taps, scroll flings at the extremes\n\nWatch the recording **twice**: once at full speed for feel, once scrubbing\nframe by frame. You are hunting:\n\n- dropped or stuttered frames — the bar is a sustained **60 fps** through\n  every transition, measured, not vibed\n- one-frame flashes: white/unstyled first paint, wrong-theme frames mid-\n  transition, color pops where a surface briefly renders the wrong token\n- layout jumps, double-render pops, springs that clip or overshoot into\n  content, elements that reflow after appearing\n\nThe whole recording must play like one native piece — smooth end to end,\nzero UX glitches. One glitchy frame means the flow is not done.\n\n## Definition of done, per screen\n\n- [ ] Studied 10+ real reference screens for this screen type (via Appllama\n      MCP when available) and can name the pattern you adopted\n- [ ] Navigation answered: what this screen *is* (push / modal / sheet /\n      overlay / replace), what back does from it on iOS and Android, and —\n      behind a one-way door — that back cannot re-enter the old state\n- [ ] Light + dark mode verified in the simulator\n- [ ] Safe areas / Dynamic Island / home indicator verified\n- [ ] Long-content, empty, loading, and error states designed — not defaulted\n- [ ] Motion: the full flow screen-recorded and scrubbed — entrances,\n      presses, transitions, modals, keyboard — native feel, zero glitch or\n      wrong-color frames; Reduce Motion respected; 60 fps measured on a\n      release build on the slowest supported device\n- [ ] Dynamic Type XL doesn't break layout; text is selectable where useful\n- [ ] All tap targets ≥ 44pt; contrast passes in both themes\n- [ ] Assets: single style family, crisp at @3x, no compositing halos\n- [ ] List surfaces virtualized; no controlled-input jank; no re-render storms\n      (profiled, not guessed)\n\n## References\n\n| File | Load when |\n|---|---|\n| [references/native-controls.md](references/native-controls.md) | Choosing/wiring iOS+Android native controls, menus, pickers, sheets |\n| [references/motion.md](references/motion.md) | Any Reanimated work: gestures, transitions, springs, layout animations |\n| [references/performance.md](references/performance.md) | Jank, slow TTI, big bundles, memory leaks, profiling method |\n| [references/image-assets.md](references/image-assets.md) | Generating illustrations/icons/hero art with image models |\n| [references/simulator-loop.md](references/simulator-loop.md) | Final verification checklist + device matrix |","schemaVersion":1},"repoUrl":"https://github.com/Appllama/appllama-skills/tree/main/skills/appllama-app-design-skill","tags":["agent-skills","ai-agents","claude","claude-code","claude-code-skill","claude-skills","codex","codex-skill","cursor","design-system","expo","mcp"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"appllama-skills","audit":{"files":[],"binaries":[],"findings":[],"packages":0,"auditedAt":"2026-09-25T11:52:12.962Z","lockfiles":[]},"forks":97,"owner":"Appllama","stars":2056,"topics":["agent-skills","ai-agents","claude","claude-code","claude-code-skill","claude-skills","codex","codex-skill","cursor","design-system","expo","mcp","mobile-app-development","mobile-design","mobile-ui","model-context-protocol","react-native","skills","ui-design"],"license":"MIT","fullName":"Appllama/appllama-skills","homepage":"https://appllama.io/mcp","language":null,"pushedAt":"2026-09-06T09:01:39Z","avatarUrl":"https://avatars.githubusercontent.com/u/313732614?v=4","crawledAt":"2026-09-25T11:52:12.306Z","openIssues":1,"manifestFile":"SKILL.md","manifestPath":"skills/appllama-app-design-skill/SKILL.md","defaultBranch":"main"},"readme":"# Appllama App Design Skill\n\nYou are building screens that will sit on a phone next to the best-designed apps\nin the world. The user will compare your output to those apps within seconds of\nlaunching it. This skill defines the bar and the method for clearing it.\n\n## The Prime Directive: study before you draw\n\nNever design a screen from imagination when you can study how top apps solved\nthe same screen. Real, shipping, revenue-ranked apps encode thousands of hours\nof design iteration and A/B testing. Your first move on any screen is research:\n\n1. If the **Appllama MCP** is connected, pull real screens for the category and\n   screen type you are building (see the `appllama-usage` skill for the exact\n   research playbooks). Study 20–30 screens before writing a line of UI code.\n2. Extract the **pattern, not the pixels**: layout skeleton, information\n   hierarchy, control choices, spacing rhythm, where the primary CTA sits, what\n   gets an illustration vs. plain text, how progress is communicated.\n   Note: every Appllama image and video carries a small Appllama watermark in\n   the top-left corner. It is provenance, not design — ignore it when reading\n   a screen (it may sit over the status bar or a back button) and never\n   reproduce it in anything you build.\n3. Then design **your** screen: same proven skeleton, your product's voice.\n   Copying a competitor's screen 1:1 is both lazy and legally risky; shipping a\n   screen that ignores every convention users already know is worse.\n\n## Platform baseline\n\nDefault stack assumptions (override only if the project already differs):\n\n- **Expo + Expo Router**, React Native, TypeScript.\n- `react-native-reanimated` for motion, `react-native-gesture-handler` for\n  gestures, `@shopify/flash-list` (or FlashList v2) for any list that can grow.\n- `expo-image` for images (and SF Symbols via `source=\"sf:name\"` on iOS),\n  `expo-video` / `expo-audio` (never the deprecated `expo-av`).\n- `react-native-safe-area-context` for insets. Never hard-code notch numbers.\n- `process.env.EXPO_OS` over `Platform.OS` for compile-time platform checks.\n\n## Native fidelity laws\n\nThese are the details that separate \"web page in a wrapper\" from \"native app\".\nViolating any of them is a finding, not a style preference.\n\n1. **Semantic colors, both themes, day one.** Use system/semantic color tokens\n   (e.g. `Color` from `expo-router` on iOS: `Color.ios.label`,\n   `Color.ios.secondarySystemBackground`; Material dynamic colors on Android).\n   Every screen must render correctly in light AND dark before it is \"done\".\n   Never pass semantic color objects into Reanimated animated styles — resolve\n   to strings first.\n2. **Native controls over rebuilt ones.** Switch, Slider, SegmentedControl,\n   context menus, date pickers: use the native control or a faithful wrapper.\n   A rebuilt toggle that animates 50 ms differently than iOS's reads as fake\n   instantly.\n3. **SF Symbols / Material Symbols for iconography.** On iOS prefer SF Symbols\n   (`expo-image` with `sf:` sources, or `expo-symbols`); they inherit weight,\n   optical size, and Dynamic Type behavior. Do not mix three icon families on\n   one screen.\n4. **Typography is hierarchy.** Use the platform type ramp (Large Title / Title\n   / Headline / Body / Footnote on iOS). One display size per screen. Tabular\n   numerals (`fontVariant: ['tabular-nums']`) for anything that counts, times,\n   or prices. `Text selectable` on data users may want to copy.\n5. **Continuous corners.** `borderCurve: 'continuous'` on every rounded\n   rectangle. Squircles are the single cheapest \"feels iOS\" win that exists.\n6. **Shadows via CSS `boxShadow`**, not legacy `shadow*`/`elevation` props.\n   Shadows are for elevation logic, not decoration — one elevation system per\n   app.\n7. **Spacing rhythm.** Pick a base unit (4 or 8) and never leave it. Prefer\n   flexbox `gap` over margin stacking. ScrollView padding goes in\n   `contentContainerStyle`, never on the ScrollView itself.\n8. **Safe areas and the Dynam","createdAt":"2026-09-25T11:52:12.981Z","updatedAt":"2026-09-25T11:52:12.981Z"},{"id":"cmugwi4zn01snqu06n0kmz3m9","slug":"appllama-appllama-skills-appllama-usage","name":"appllama-usage","description":"Use the Appllama MCP (mcp.appllama.io) well — research real top-grossing mobile apps, their screens, flows, and UI elements, then build from what you learn. Load when the Appllama MCP is connected and the task involves building a mobile app or screen, researching app design patterns, studying onboarding/paywall/feature flows, improving an existing screen, or whenever an appllama_* / search_apps / list_app_screens tool is available. Covers the tool map, pagination, expiring media, and the full build-from-research playbooks.","authorId":"gh:appllama","authorName":"Appllama","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":2056,"pricePerCall":0,"manifest":{"name":"appllama-usage","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use the Appllama MCP (mcp.appllama.io) well — research real top-grossing mobile apps, their screens, flows, and UI elements, then build from what you learn. Load when the Appllama MCP is connected and the task involves building a mobile app or screen, researching app design patterns, studying onboarding/paywall/feature flows, improving an existing screen, or whenever an appllama_* / search_apps / list_app_screens tool is available. Covers the tool map, pagination, expiring media, and the full build-from-research playbooks.","permissions":[],"systemPrompt":"# Appllama Usage Skill\n\nAppllama is the design library of top-grossing mobile apps — their real\nscreens, flows, and UI patterns, with revenue and download context. The MCP\nputs that library in an agent's hands: **not just a research tool, a builder's\ntool.** You study what already wins, then you build something better.\n\nPair this skill with **appllama-app-design-skill** for every design/implementation\nstep — this skill tells you what to study; that one tells you how to build.\n\n## Ground rules (read first)\n\n1. **Start with `get_credits` — it's free.** It tells you the balance,\n   limits, and reset date. Pro includes 1,500 credits a month (they reset in\n   full on the 1st, UTC); every other call spends 1 credit.\n2. **Go deep.** Design language lives in the whole journey, not a sample —\n   walk every screen of the apps that matter for the task, images included.\n   That is exactly what the library is for. The one thing that's against the\n   terms is harvesting: sweeping the catalog to extract the dataset itself\n   rather than to answer a real task. That isn't research, and it's detected\n   server-side.\n3. **Media URLs expire in ~1 hour.** Download/view what you study promptly.\n   If links died mid-task, re-request that page for fresh ones — screen ids\n   are durable, links are not.\n4. **Ignore the watermark.** Every Appllama image and video carries a small\n   Appllama watermark in the top-left corner. It is provenance, not part of\n   the screen — don't let it skew your read of that corner (status bar,\n   back button, title), and never reproduce it in anything you build.\n5. **Pagination is sequential.** Every list response carries `next_cursor`;\n   pass it back to continue. You cannot jump to page N — and a cursor only\n   works for the same query that minted it. If a cursor errors, drop it and\n   restart from page one.\n6. **If you hit a rate limit, wait it out.** The per-minute and per-day\n   limits sit far above real research; on the rare hit, wait the stated\n   time — don't retry-hammer.\n7. **Errors are instructions.** Tool errors are written to be acted on\n   (expired cursor → restart; out of credits → tell the user their credits\n   reset on the 1st and they can request more in Settings → Usage).\n\n## Tool map\n\n| Tool | What it gives you | Typical use |\n|---|---|---|\n| `get_credits` | Balance, limits, reset date. **Free.** | Session start |\n| `search_apps` | 10 apps/page: name, revenue, downloads, rating, launch date, screens count, **flow list with screen counts**. Natural-language `query` + filters (revenue/downloads/rating/launch date/price/onboarding steps) + `sort` + `board_id` | Find the top apps for a category or need |\n| `get_app` | One app in full: ratings breakdown, category rank, IAP pricing, top countries, flows | Decide if an app deserves a deep study |\n| `list_app_screens` | 10 screens/page **in journey order** (welcome → onboarding → paywall → product), each with media URL, flow, UI elements, colors. Filter by `flow` or `section` | Walk an app screen by screen |\n| `search_screens` | Screens across the whole library. `mode=\"keyword\"` matches screen names + filters (flow, screen_type, element, app_id); `mode=\"semantic\"` searches by meaning/visual language | Gather design references for one screen type |\n| `get_screen` | One screen in full + up to 5 visually similar screens from other apps. Accepts `screen_ref` = `app_id/screen_id` (what appllama.io's \"Copy Screen ID\" produces) | The user pasted a screen ref; or drill into one reference |\n| `list_flows` | The flow taxonomy with screen/app counts | Discover what flows exist for a category |\n| `get_flow_apps` | Apps containing a flow, top revenue first | Find the best examples of one flow |\n| `list_ui_elements` | ~38 UI-element families with counts (one call) | Vocabulary for element-level research |\n| `get_element_screens` | Screens featuring an element family | Study how winners build one component |\n| `list_my_boards` | The member's own appllama.io boards (screens / apps / flows) | Find their curation first |\n| `get_board` | A board's full contents: screens with media, app profiles, or (app, flow) pairs | When the member curated a board for the task, START from it |\n\n**The screen_ref handshake:** members can click \"Copy Screen ID\" on any\nscreen at appllama.io and paste it to you. It looks like\n`1393061654/spl_9i075` — feed it straight to\n`get_screen(screen_ref=...)` and you're looking at exactly the screen they\nmean, plus its closest siblings across the library.\n\n## The playbooks\n\n| Scenario | Reference |\n|---|---|\n| Build an app from scratch (e.g. \"build me a habit tracker\") | [references/build-from-scratch.md](references/build-from-scratch.md) |\n| Make an existing screen better | [references/improve-a-screen.md](references/improve-a-screen.md) |\n| Flow & element research; general research method | [references/research-methods.md](references/research-methods.md) |\n\nBoth build playbooks end the same way: **the simulator loop from\nappllama-app-design-skill, repeated until you cannot find a flaw.** Research\nwithout that loop is decoration.\n\n## Local reference boards\n\nWhen you pull screens for study, save them into a local working structure —\nlinks expire in about an hour, but your notes and downloads don't:\n\n```\nresearch/\n  <category>/\n    apps.md            # the shortlist: metrics, flows, verdicts\n    <app-name>/\n      screens.md       # per-screen notes: id, name, flow, elements, colors\n      img/             # downloaded screens, in journey order\n    patterns.md        # cross-app synthesis: the category's design language\n```\n\nDownload the screens as you study them — synthesis happens with the images\nside by side, not from metadata. Notes and screen IDs are durable; re-fetch\na fresh link from the ID if you ever need the pixels again.","schemaVersion":1},"repoUrl":"https://github.com/Appllama/appllama-skills/tree/main/skills/appllama-usage","tags":["agent-skills","ai-agents","claude","claude-code","claude-code-skill","claude-skills","codex","codex-skill","cursor","design-system","expo","mcp"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"appllama-skills","audit":{"files":[],"binaries":[],"findings":[],"packages":0,"auditedAt":"2026-09-25T11:52:12.962Z","lockfiles":[]},"forks":97,"owner":"Appllama","stars":2056,"topics":["agent-skills","ai-agents","claude","claude-code","claude-code-skill","claude-skills","codex","codex-skill","cursor","design-system","expo","mcp","mobile-app-development","mobile-design","mobile-ui","model-context-protocol","react-native","skills","ui-design"],"license":"MIT","fullName":"Appllama/appllama-skills","homepage":"https://appllama.io/mcp","language":null,"pushedAt":"2026-09-06T09:01:39Z","avatarUrl":"https://avatars.githubusercontent.com/u/313732614?v=4","crawledAt":"2026-09-25T11:52:12.306Z","openIssues":1,"manifestFile":"SKILL.md","manifestPath":"skills/appllama-usage/SKILL.md","defaultBranch":"main"},"readme":"# Appllama Usage Skill\n\nAppllama is the design library of top-grossing mobile apps — their real\nscreens, flows, and UI patterns, with revenue and download context. The MCP\nputs that library in an agent's hands: **not just a research tool, a builder's\ntool.** You study what already wins, then you build something better.\n\nPair this skill with **appllama-app-design-skill** for every design/implementation\nstep — this skill tells you what to study; that one tells you how to build.\n\n## Ground rules (read first)\n\n1. **Start with `get_credits` — it's free.** It tells you the balance,\n   limits, and reset date. Pro includes 1,500 credits a month (they reset in\n   full on the 1st, UTC); every other call spends 1 credit.\n2. **Go deep.** Design language lives in the whole journey, not a sample —\n   walk every screen of the apps that matter for the task, images included.\n   That is exactly what the library is for. The one thing that's against the\n   terms is harvesting: sweeping the catalog to extract the dataset itself\n   rather than to answer a real task. That isn't research, and it's detected\n   server-side.\n3. **Media URLs expire in ~1 hour.** Download/view what you study promptly.\n   If links died mid-task, re-request that page for fresh ones — screen ids\n   are durable, links are not.\n4. **Ignore the watermark.** Every Appllama image and video carries a small\n   Appllama watermark in the top-left corner. It is provenance, not part of\n   the screen — don't let it skew your read of that corner (status bar,\n   back button, title), and never reproduce it in anything you build.\n5. **Pagination is sequential.** Every list response carries `next_cursor`;\n   pass it back to continue. You cannot jump to page N — and a cursor only\n   works for the same query that minted it. If a cursor errors, drop it and\n   restart from page one.\n6. **If you hit a rate limit, wait it out.** The per-minute and per-day\n   limits sit far above real research; on the rare hit, wait the stated\n   time — don't retry-hammer.\n7. **Errors are instructions.** Tool errors are written to be acted on\n   (expired cursor → restart; out of credits → tell the user their credits\n   reset on the 1st and they can request more in Settings → Usage).\n\n## Tool map\n\n| Tool | What it gives you | Typical use |\n|---|---|---|\n| `get_credits` | Balance, limits, reset date. **Free.** | Session start |\n| `search_apps` | 10 apps/page: name, revenue, downloads, rating, launch date, screens count, **flow list with screen counts**. Natural-language `query` + filters (revenue/downloads/rating/launch date/price/onboarding steps) + `sort` + `board_id` | Find the top apps for a category or need |\n| `get_app` | One app in full: ratings breakdown, category rank, IAP pricing, top countries, flows | Decide if an app deserves a deep study |\n| `list_app_screens` | 10 screens/page **in journey order** (welcome → onboarding → paywall → product), each with media URL, flow, UI elements, colors. Filter by `flow` or `section` | Walk an app screen by screen |\n| `search_screens` | Screens across the whole library. `mode=\"keyword\"` matches screen names + filters (flow, screen_type, element, app_id); `mode=\"semantic\"` searches by meaning/visual language | Gather design references for one screen type |\n| `get_screen` | One screen in full + up to 5 visually similar screens from other apps. Accepts `screen_ref` = `app_id/screen_id` (what appllama.io's \"Copy Screen ID\" produces) | The user pasted a screen ref; or drill into one reference |\n| `list_flows` | The flow taxonomy with screen/app counts | Discover what flows exist for a category |\n| `get_flow_apps` | Apps containing a flow, top revenue first | Find the best examples of one flow |\n| `list_ui_elements` | ~38 UI-element families with counts (one call) | Vocabulary for element-level research |\n| `get_element_screens` | Screens featuring an element family | Study how winners build one component |\n| `list_my_boards` | The member's own appllama.io boards (screens / ","createdAt":"2026-09-25T11:52:12.995Z","updatedAt":"2026-09-25T11:52:12.995Z"}],"total":11,"limit":24,"offset":0}