[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6FJZFDGEM6N9BTH21BAJCS7":3,"skill-same:01M6FJZFDGEM6N9BTH21BAJCS7":50},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":23,"linkOnly":24,"markdownContent":25,"markdownBody":25,"frontMatter":26,"refName":27,"commitSha":28,"skillDirectory":29,"forksCount":30,"repositoryHtmlUrl":31,"repositoryHomepageUrl":32,"ownerHtmlUrl":33,"ownerBlogUrl":6,"ownerDescription":34,"updatedAt":35,"install":36,"resources":49},"01M6FJZFDGEM6N9BTH21BAJCS7","convex-security-audit",null,"ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fwaynesutton\u002Fconvexskills\u002Fconvex-security-audit\u002FSKILL.md",[],[18,19,20,21,22],"convex","security","audit","security audit","convex security audit","2026-11-10 10:12:30",false,"# Convex Security Audit\n\nComprehensive security review patterns for Convex applications including authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Fauth\u002Ffunctions-auth\n- Production Security: https:\u002F\u002Fdocs.convex.dev\u002Fproduction\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### Security Audit Areas\n\n1. **Authorization Logic** - Who can do what\n2. **Data Access Boundaries** - What data users can see\n3. **Action Isolation** - Protecting external API calls\n4. **Rate Limiting** - Preventing abuse\n5. **Sensitive Operations** - Protecting critical functions\n\n### Authorization Logic Audit\n\n#### Role-Based Access Control (RBAC)\n\n```typescript\n\u002F\u002F convex\u002Flib\u002Fauth.ts\nimport { QueryCtx, MutationCtx } from \".\u002F_generated\u002Fserver\";\nimport { ConvexError } from \"convex\u002Fvalues\";\nimport { Doc } from \".\u002F_generated\u002FdataModel\";\n\ntype UserRole = \"user\" | \"moderator\" | \"admin\" | \"superadmin\";\n\nconst roleHierarchy: Record\u003CUserRole, number> = {\n  user: 0,\n  moderator: 1,\n  admin: 2,\n  superadmin: 3,\n};\n\nexport async function getUser(ctx: QueryCtx | MutationCtx): Promise\u003CDoc\u003C\"users\"> | null> {\n  const identity = await ctx.auth.getUserIdentity();\n  if (!identity) return null;\n  \n  return await ctx.db\n    .query(\"users\")\n    .withIndex(\"by_tokenIdentifier\", (q) => \n      q.eq(\"tokenIdentifier\", identity.tokenIdentifier)\n    )\n    .unique();\n}\n\nexport async function requireRole(\n  ctx: QueryCtx | MutationCtx, \n  minRole: UserRole\n): Promise\u003CDoc\u003C\"users\">> {\n  const user = await getUser(ctx);\n  \n  if (!user) {\n    throw new ConvexError({\n      code: \"UNAUTHENTICATED\",\n      message: \"Authentication required\",\n    });\n  }\n  \n  const userRoleLevel = roleHierarchy[user.role as UserRole] ?? 0;\n  const requiredLevel = roleHierarchy[minRole];\n  \n  if (userRoleLevel \u003C requiredLevel) {\n    throw new ConvexError({\n      code: \"FORBIDDEN\",\n      message: `Role '${minRole}' or higher required`,\n    });\n  }\n  \n  return user;\n}\n\n\u002F\u002F Permission-based check\ntype Permission = \"read:users\" | \"write:users\" | \"delete:users\" | \"admin:system\";\n\nconst rolePermissions: Record\u003CUserRole, Permission[]> = {\n  user: [\"read:users\"],\n  moderator: [\"read:users\", \"write:users\"],\n  admin: [\"read:users\", \"write:users\", \"delete:users\"],\n  superadmin: [\"read:users\", \"write:users\", \"delete:users\", \"admin:system\"],\n};\n\nexport async function requirePermission(\n  ctx: QueryCtx | MutationCtx,\n  permission: Permission\n): Promise\u003CDoc\u003C\"users\">> {\n  const user = await getUser(ctx);\n  \n  if (!user) {\n    throw new ConvexError({ code: \"UNAUTHENTICATED\", message: \"Authentication required\" });\n  }\n  \n  const userRole = user.role as UserRole;\n  const permissions = rolePermissions[userRole] ?? [];\n  \n  if (!permissions.includes(permission)) {\n    throw new ConvexError({\n      code: \"FORBIDDEN\",\n      message: `Permission '${permission}' required`,\n    });\n  }\n  \n  return user;\n}\n```\n\n### Data Access Boundaries Audit\n\n```typescript\n\u002F\u002F convex\u002Fdata.ts\nimport { query, mutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\nimport { getUser, requireRole } from \".\u002Flib\u002Fauth\";\nimport { ConvexError } from \"convex\u002Fvalues\";\n\n\u002F\u002F Audit: Users can only see their own data\nexport const getMyData = query({\n  args: {},\n  returns: v.array(v.object({\n    _id: v.id(\"userData\"),\n    content: v.string(),\n  })),\n  handler: async (ctx) => {\n    const user = await getUser(ctx);\n    if (!user) return [];\n    \n    \u002F\u002F SECURITY: Filter by userId\n    return await ctx.db\n      .query(\"userData\")\n      .withIndex(\"by_user\", (q) => q.eq(\"userId\", user._id))\n      .collect();\n  },\n});\n\n\u002F\u002F Audit: Verify ownership before returning sensitive data\nexport const getSensitiveItem = query({\n  args: { itemId: v.id(\"sensitiveItems\") },\n  returns: v.union(v.object({\n    _id: v.id(\"sensitiveItems\"),\n    secret: v.string(),\n  }), v.null()),\n  handler: async (ctx, args) => {\n    const user = await getUser(ctx);\n    if (!user) return null;\n    \n    const item = await ctx.db.get(args.itemId);\n    \n    \u002F\u002F SECURITY: Verify ownership\n    if (!item || item.ownerId !== user._id) {\n      return null; \u002F\u002F Don't reveal if item exists\n    }\n    \n    return item;\n  },\n});\n\n\u002F\u002F Audit: Shared resources with access list\nexport const getSharedDocument = query({\n  args: { docId: v.id(\"documents\") },\n  returns: v.union(v.object({\n    _id: v.id(\"documents\"),\n    content: v.string(),\n    accessLevel: v.string(),\n  }), v.null()),\n  handler: async (ctx, args) => {\n    const user = await getUser(ctx);\n    const doc = await ctx.db.get(args.docId);\n    \n    if (!doc) return null;\n    \n    \u002F\u002F Public documents\n    if (doc.visibility === \"public\") {\n      return { ...doc, accessLevel: \"public\" };\n    }\n    \n    \u002F\u002F Must be authenticated for non-public\n    if (!user) return null;\n    \n    \u002F\u002F Owner has full access\n    if (doc.ownerId === user._id) {\n      return { ...doc, accessLevel: \"owner\" };\n    }\n    \n    \u002F\u002F Check shared access\n    const access = await ctx.db\n      .query(\"documentAccess\")\n      .withIndex(\"by_doc_and_user\", (q) => \n        q.eq(\"documentId\", args.docId).eq(\"userId\", user._id)\n      )\n      .unique();\n    \n    if (!access) return null;\n    \n    return { ...doc, accessLevel: access.level };\n  },\n});\n```\n\n### Action Isolation Audit\n\n```typescript\n\u002F\u002F convex\u002Factions.ts\n\"use node\";\n\nimport { action, internalAction } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\nimport { api, internal } from \".\u002F_generated\u002Fapi\";\nimport { ConvexError } from \"convex\u002Fvalues\";\n\n\u002F\u002F SECURITY: Never expose API keys in responses\nexport const callExternalAPI = action({\n  args: { query: v.string() },\n  returns: v.object({ result: v.string() }),\n  handler: async (ctx, args) => {\n    \u002F\u002F Verify user is authenticated\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) {\n      throw new ConvexError(\"Authentication required\");\n    }\n    \n    \u002F\u002F Get API key from environment (not hardcoded)\n    const apiKey = process.env.EXTERNAL_API_KEY;\n    if (!apiKey) {\n      throw new Error(\"API key not configured\");\n    }\n    \n    \u002F\u002F Log usage for audit trail\n    await ctx.runMutation(internal.audit.logAPICall, {\n      userId: identity.tokenIdentifier,\n      endpoint: \"external-api\",\n      timestamp: Date.now(),\n    });\n    \n    const response = await fetch(\"https:\u002F\u002Fapi.example.com\u002Fquery\", {\n      method: \"POST\",\n      headers: {\n        \"Authorization\": `Bearer ${apiKey}`,\n        \"Content-Type\": \"application\u002Fjson\",\n      },\n      body: JSON.stringify({ query: args.query }),\n    });\n    \n    if (!response.ok) {\n      \u002F\u002F Don't expose external API error details\n      throw new ConvexError(\"External service unavailable\");\n    }\n    \n    const data = await response.json();\n    \n    \u002F\u002F Sanitize response before returning\n    return { result: sanitizeResponse(data) };\n  },\n});\n\n\u002F\u002F Internal action - not exposed to clients\nexport const _processPayment = internalAction({\n  args: {\n    userId: v.id(\"users\"),\n    amount: v.number(),\n    paymentMethodId: v.string(),\n  },\n  returns: v.object({ success: v.boolean(), transactionId: v.optional(v.string()) }),\n  handler: async (ctx, args) => {\n    const stripeKey = process.env.STRIPE_SECRET_KEY;\n    \n    \u002F\u002F Process payment with Stripe\n    \u002F\u002F This should NEVER be exposed as a public action\n    \n    return { success: true, transactionId: \"txn_xxx\" };\n  },\n});\n```\n\n### Rate Limiting Audit\n\n```typescript\n\u002F\u002F convex\u002FrateLimit.ts\nimport { mutation, query } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\nimport { ConvexError } from \"convex\u002Fvalues\";\n\nconst RATE_LIMITS = {\n  message: { requests: 10, windowMs: 60000 }, \u002F\u002F 10 per minute\n  upload: { requests: 5, windowMs: 300000 },  \u002F\u002F 5 per 5 minutes\n  api: { requests: 100, windowMs: 3600000 },  \u002F\u002F 100 per hour\n};\n\nexport const checkRateLimit = mutation({\n  args: {\n    userId: v.string(),\n    action: v.union(v.literal(\"message\"), v.literal(\"upload\"), v.literal(\"api\")),\n  },\n  returns: v.object({ allowed: v.boolean(), retryAfter: v.optional(v.number()) }),\n  handler: async (ctx, args) => {\n    const limit = RATE_LIMITS[args.action];\n    const now = Date.now();\n    const windowStart = now - limit.windowMs;\n    \n    \u002F\u002F Count requests in window\n    const requests = await ctx.db\n      .query(\"rateLimits\")\n      .withIndex(\"by_user_and_action\", (q) => \n        q.eq(\"userId\", args.userId).eq(\"action\", args.action)\n      )\n      .filter((q) => q.gt(q.field(\"timestamp\"), windowStart))\n      .collect();\n    \n    if (requests.length >= limit.requests) {\n      const oldestRequest = requests[0];\n      const retryAfter = oldestRequest.timestamp + limit.windowMs - now;\n      \n      return { allowed: false, retryAfter };\n    }\n    \n    \u002F\u002F Record this request\n    await ctx.db.insert(\"rateLimits\", {\n      userId: args.userId,\n      action: args.action,\n      timestamp: now,\n    });\n    \n    return { allowed: true };\n  },\n});\n\n\u002F\u002F Use in mutations\nexport const sendMessage = mutation({\n  args: { content: v.string() },\n  returns: v.id(\"messages\"),\n  handler: async (ctx, args) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) throw new ConvexError(\"Authentication required\");\n    \n    \u002F\u002F Check rate limit\n    const rateCheck = await checkRateLimit(ctx, {\n      userId: identity.tokenIdentifier,\n      action: \"message\",\n    });\n    \n    if (!rateCheck.allowed) {\n      throw new ConvexError({\n        code: \"RATE_LIMITED\",\n        message: `Too many requests. Try again in ${Math.ceil(rateCheck.retryAfter! \u002F 1000)} seconds`,\n      });\n    }\n    \n    return await ctx.db.insert(\"messages\", {\n      content: args.content,\n      authorId: identity.tokenIdentifier,\n      createdAt: Date.now(),\n    });\n  },\n});\n```\n\n### Sensitive Operations Protection\n\n```typescript\n\u002F\u002F convex\u002Fadmin.ts\nimport { mutation, internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\nimport { requireRole, requirePermission } from \".\u002Flib\u002Fauth\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\n\u002F\u002F Two-factor confirmation for dangerous operations\nexport const deleteAllUserData = mutation({\n  args: {\n    userId: v.id(\"users\"),\n    confirmationCode: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    \u002F\u002F Require superadmin\n    const admin = await requireRole(ctx, \"superadmin\");\n    \n    \u002F\u002F Verify confirmation code\n    const confirmation = await ctx.db\n      .query(\"confirmations\")\n      .withIndex(\"by_admin_and_code\", (q) => \n        q.eq(\"adminId\", admin._id).eq(\"code\", args.confirmationCode)\n      )\n      .filter((q) => q.gt(q.field(\"expiresAt\"), Date.now()))\n      .unique();\n    \n    if (!confirmation || confirmation.action !== \"delete_user_data\") {\n      throw new ConvexError(\"Invalid or expired confirmation code\");\n    }\n    \n    \u002F\u002F Delete confirmation to prevent reuse\n    await ctx.db.delete(confirmation._id);\n    \n    \u002F\u002F Schedule deletion (don't do it inline)\n    await ctx.scheduler.runAfter(0, internal.admin._performDeletion, {\n      userId: args.userId,\n      requestedBy: admin._id,\n    });\n    \n    \u002F\u002F Audit log\n    await ctx.db.insert(\"auditLogs\", {\n      action: \"delete_user_data\",\n      targetUserId: args.userId,\n      performedBy: admin._id,\n      timestamp: Date.now(),\n    });\n    \n    return null;\n  },\n});\n\n\u002F\u002F Generate confirmation code for sensitive action\nexport const requestDeletionConfirmation = mutation({\n  args: { userId: v.id(\"users\") },\n  returns: v.string(),\n  handler: async (ctx, args) => {\n    const admin = await requireRole(ctx, \"superadmin\");\n    \n    const code = generateSecureCode();\n    \n    await ctx.db.insert(\"confirmations\", {\n      adminId: admin._id,\n      code,\n      action: \"delete_user_data\",\n      targetUserId: args.userId,\n      expiresAt: Date.now() + 5 * 60 * 1000, \u002F\u002F 5 minutes\n    });\n    \n    \u002F\u002F In production, send code via secure channel (email, SMS)\n    return code;\n  },\n});\n```\n\n## Examples\n\n### Complete Audit Trail System\n\n```typescript\n\u002F\u002F convex\u002Faudit.ts\nimport { mutation, query, internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\nimport { getUser, requireRole } from \".\u002Flib\u002Fauth\";\n\nconst auditEventValidator = v.object({\n  _id: v.id(\"auditLogs\"),\n  _creationTime: v.number(),\n  action: v.string(),\n  userId: v.optional(v.string()),\n  resourceType: v.string(),\n  resourceId: v.string(),\n  details: v.optional(v.any()),\n  ipAddress: v.optional(v.string()),\n  timestamp: v.number(),\n});\n\n\u002F\u002F Internal: Log audit event\nexport const logEvent = internalMutation({\n  args: {\n    action: v.string(),\n    userId: v.optional(v.string()),\n    resourceType: v.string(),\n    resourceId: v.string(),\n    details: v.optional(v.any()),\n  },\n  returns: v.id(\"auditLogs\"),\n  handler: async (ctx, args) => {\n    return await ctx.db.insert(\"auditLogs\", {\n      ...args,\n      timestamp: Date.now(),\n    });\n  },\n});\n\n\u002F\u002F Admin: View audit logs\nexport const getAuditLogs = query({\n  args: {\n    resourceType: v.optional(v.string()),\n    userId: v.optional(v.string()),\n    limit: v.optional(v.number()),\n  },\n  returns: v.array(auditEventValidator),\n  handler: async (ctx, args) => {\n    await requireRole(ctx, \"admin\");\n    \n    let query = ctx.db.query(\"auditLogs\");\n    \n    if (args.resourceType) {\n      query = query.withIndex(\"by_resource_type\", (q) => \n        q.eq(\"resourceType\", args.resourceType)\n      );\n    }\n    \n    return await query\n      .order(\"desc\")\n      .take(args.limit ?? 100);\n  },\n});\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Implement defense in depth (multiple security layers)\n- Log all sensitive operations for audit trails\n- Use confirmation codes for destructive actions\n- Rate limit all user-facing endpoints\n- Never expose internal API keys or errors\n- Review access patterns regularly\n\n## Common Pitfalls\n\n1. **Single point of failure** - Implement multiple auth checks\n2. **Missing audit logs** - Log all sensitive operations\n3. **Trusting client data** - Always validate server-side\n4. **Exposing error details** - Sanitize error messages\n5. **No rate limiting** - Always implement rate limits\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- Functions Auth: https:\u002F\u002Fdocs.convex.dev\u002Fauth\u002Ffunctions-auth\n- Production Security: https:\u002F\u002Fdocs.convex.dev\u002Fproduction\n",{},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Fwaynesutton\u002Fconvexskills\u002Fconvex-security-audit",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":7,"skillName":5,"commands":37,"prompt":47,"zipAvailable":48},[38,41,44],{"tool":39,"command":40},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill convex-security-audit",{"tool":42,"command":43},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill convex-security-audit",{"tool":45,"command":46},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill convex-security-audit","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fwaynesutton\u002Fconvexskills\u002Fconvex-security-audit\u002FSKILL.md",true,[],[51,65,75,84,90,97],{"skillNo":52,"name":53,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":54,"categories":55,"keywords":61,"publishedAt":23},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[56],{"categoryNo":57,"categoryName":58,"taxonomyType":59,"taxonomyTypeDesc":60},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[62,63,64],"model","model hierarchy","model hierarchy skill",{"skillNo":66,"name":67,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":71,"publishedAt":23},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[70],{"categoryNo":57,"categoryName":58,"taxonomyType":59,"taxonomyTypeDesc":60},[72,73,74],"MCP","zoom","zoom mcp",{"skillNo":76,"name":77,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":23},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[81,82,83,73],"sdk","zoom apps sdk","apps",{"skillNo":85,"name":86,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":23},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[86],{"skillNo":91,"name":92,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":93,"categories":94,"keywords":96,"publishedAt":23},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[95],{"categoryNo":57,"categoryName":58,"taxonomyType":59,"taxonomyTypeDesc":60},[92],{"skillNo":98,"name":99,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":100,"categories":101,"keywords":102,"publishedAt":23},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[99]]