Repository navigation
Add Clerk integration and update environment configuration. #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| { | ||
| "skill_name": "clerk-backend-api", | ||
| "evals": [ | ||
| { | ||
| "id": 1, | ||
| "prompt": "i need to get all users who signed up in the last 7 days from clerk's backend API. show me how", | ||
| "expected_output": "REST API call to GET /v1/users with date filter, Bearer token auth using CLERK_SECRET_KEY, response parsing and pagination handling", | ||
| "scaffold": "nextjs-basic-auth", | ||
| "files": [], | ||
| "expectations": [ | ||
| "Uses GET /v1/users endpoint", | ||
| "Includes a created_at or date-based filter query parameter to narrow to last 7 days", | ||
| "Sets CLERK_SECRET_KEY as Bearer token in the Authorization header", | ||
| "Parses the JSON response array of user objects", | ||
| "Handles or mentions pagination via limit and offset parameters" | ||
| ] | ||
| }, | ||
| { | ||
| "id": 2, | ||
| "prompt": "use the clerk backend api to create a new organization called 'Acme Corp' and invite user@example.com as an admin", | ||
| "expected_output": "Two API calls: POST /v1/organizations to create org, then POST /v1/organizations/{id}/invitations to invite with admin role", | ||
| "scaffold": "nextjs-basic-auth", | ||
| "files": [], | ||
| "expectations": [ | ||
| "Uses POST /v1/organizations to create the organization with name 'Acme Corp'", | ||
| "Uses POST /v1/organizations/{id}/invitations to send the invitation", | ||
| "Sets the role to admin (or org:admin) in the invitation payload", | ||
| "Authenticates both API calls using CLERK_SECRET_KEY in the Authorization header", | ||
| "Extracts the organization ID from the first response and uses it in the invitation request" | ||
| ] | ||
| }, | ||
| { | ||
| "id": 3, | ||
| "prompt": "i need to set custom metadata on a user via clerk's backend api. set their plan to 'pro' and onboarded to true", | ||
| "expected_output": "PATCH /v1/users/{user_id} with public_metadata or private_metadata body, explanation of metadata types and when to use each", | ||
| "scaffold": "nextjs-basic-auth", | ||
| "files": [], | ||
| "expectations": [ | ||
| "Uses PATCH /v1/users/{user_id} endpoint", | ||
| "Sets the metadata in the correct field: public_metadata, private_metadata, or unsafe_metadata", | ||
| "Explains the difference between metadata types (client-readable vs server-only vs client-writable)", | ||
| "Includes a properly formed JSON body with the plan and onboarded fields", | ||
| "Mentions checking write permissions before executing the update" | ||
| ] | ||
| }, | ||
| { | ||
| "id": 4, | ||
| "prompt": "remove a user with id user_abc123 from our clerk instance using the backend api", | ||
| "expected_output": "DELETE /v1/users/{user_id} call with warning about irreversibility, CLERK_BAPI_SCOPES check, and CLERK_SECRET_KEY auth", | ||
| "scaffold": "nextjs-basic-auth", | ||
| "files": [], | ||
| "expectations": [ | ||
| "Uses DELETE /v1/users/{user_id} endpoint with the correct user ID", | ||
| "Warns that the action is destructive and irreversible", | ||
| "Requires or recommends explicit confirmation before executing the delete", | ||
| "Includes CLERK_SECRET_KEY as Bearer token in the Authorization header", | ||
| "Mentions implications such as loss of user data, sessions, and associated records" | ||
| ] | ||
| }, | ||
| { | ||
| "id": 5, | ||
| "prompt": "i'm updating user metadata to add a 'plan' field but it keeps deleting the existing 'role' field. what am i doing wrong?", | ||
| "expected_output": "Metadata updates overwrite not merge. Read existing metadata first, spread it, then write back.", | ||
| "scaffold": "nextjs-basic-auth", | ||
| "expectations": [ | ||
| "Explains that updateUser publicMetadata replaces all existing metadata, not merges", | ||
| "Shows reading the existing user first with getUser(userId)", | ||
| "Spreads existing metadata before adding new fields ({ ...user.publicMetadata, plan: 'pro' })", | ||
| "Calls updateUser with the merged metadata object", | ||
| "Warns that this applies to publicMetadata, privateMetadata, and unsafeMetadata equally" | ||
| ] | ||
| }, | ||
| { | ||
| "id": 6, | ||
| "prompt": "my clerk backend api calls are failing with 429 errors in development. what are the rate limits?", | ||
| "expected_output": "Development has 100 req/10s limit (vs 1000/10s in production). Batch operations or reduce call frequency.", | ||
| "scaffold": "nextjs-basic-auth", | ||
| "expectations": [ | ||
| "States the development rate limit (100 requests per 10 seconds)", | ||
| "States the production rate limit (1000 requests per 10 seconds)", | ||
| "Suggests batching operations or reducing call frequency", | ||
| "Mentions that currentUser() counts as an API call against the limit", | ||
| "Recommends using auth() instead of currentUser() when only session claims are needed" | ||
| ] | ||
| } | ||
| ] | ||
| } |
30 changes: 30 additions & 0 deletions
30
.agents/skills/clerk-backend-api/scripts/api-specs-context.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| # Fetches all available BAPI spec versions, determines the latest, | ||
| # and extracts tags from it. Output is used as skill context. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| API_URL="https://api.github.com/repos/clerk/openapi-specs/contents/bapi" | ||
| RAW_BASE="https://raw.githubusercontent.com/clerk/openapi-specs/main/bapi" | ||
|
|
||
| # Fetch version list, parse dates, sort, pick latest | ||
| versions=$(curl -s "$API_URL" | node -e " | ||
| let d=''; | ||
| process.stdin.on('data',c=>d+=c); | ||
| process.stdin.on('end',()=>{ | ||
| const items = JSON.parse(d) | ||
| .map(i=>i.name) | ||
| .filter(n=>/^\d{4}-\d{2}-\d{2}\.yml$/.test(n)) | ||
| .sort(); | ||
| items.forEach(n=>console.log(n)); | ||
| }); | ||
| ") | ||
|
|
||
| latest=$(echo "$versions" | tail -1) | ||
|
|
||
| echo "AVAILABLE VERSIONS: $(echo "$versions" | tr '\n' ' ')" | ||
| echo "LATEST VERSION: $latest" | ||
| echo "" | ||
| echo "TAGS:" | ||
| curl -s "${RAW_BASE}/${latest}" | node "$(dirname "$0")/extract-tags.js" |
88 changes: 88 additions & 0 deletions
88
.agents/skills/clerk-backend-api/scripts/execute-request.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| # Execute a Clerk Backend API request with scope enforcement. | ||
| # | ||
| # Usage: bash execute-request.sh [--admin] <METHOD> <PATH> [BODY] | ||
| # | ||
| # Scope enforcement: | ||
| # GET — always allowed | ||
| # POST, PUT, PATCH — requires CLERK_BAPI_SCOPES="write" or --admin flag | ||
| # DELETE — requires CLERK_BAPI_SCOPES="write,delete" or --admin flag | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| # Walk up from $PWD to find .env/.env.local (mirrors Clerk CLI behavior). | ||
| # Stops at the first directory that provides CLERK_SECRET_KEY. | ||
| _dir="$PWD" | ||
| while true; do | ||
| for _envfile in "$_dir/.env" "$_dir/.env.local"; do | ||
| if [[ -f "$_envfile" ]]; then | ||
| set -a | ||
| source "$_envfile" | ||
| set +a | ||
| fi | ||
| done | ||
| [[ -n "${CLERK_SECRET_KEY:-}" ]] && break | ||
| _parent="$(dirname "$_dir")" | ||
| [[ "$_parent" == "$_dir" ]] && break | ||
| _dir="$_parent" | ||
| done | ||
| unset _dir _parent _envfile | ||
|
|
||
| # Parse --admin flag | ||
| ADMIN=false | ||
| if [[ "${1:-}" == "--admin" ]]; then | ||
| ADMIN=true | ||
| shift | ||
| fi | ||
|
|
||
| METHOD="${1:?Usage: execute-request.sh [--admin] <METHOD> <PATH> [BODY]}" | ||
| PATH_ARG="${2:?Usage: execute-request.sh [--admin] <METHOD> <PATH> [BODY]}" | ||
| BODY="${3:-}" | ||
|
|
||
| METHOD_UPPER=$(echo "$METHOD" | tr '[:lower:]' '[:upper:]') | ||
| SCOPES="${CLERK_BAPI_SCOPES:-}" | ||
|
|
||
| # Scope check | ||
| if [[ "$ADMIN" == false ]]; then | ||
| case "$METHOD_UPPER" in | ||
| GET) | ||
| ;; # always allowed | ||
| POST|PUT|PATCH) | ||
| if [[ "$SCOPES" != *"write"* ]]; then | ||
| echo "ERROR: $METHOD_UPPER requests require CLERK_BAPI_SCOPES=\"write\" or --admin flag." >&2 | ||
| echo "Current CLERK_BAPI_SCOPES: \"$SCOPES\"" >&2 | ||
| exit 1 | ||
| fi | ||
| ;; | ||
| DELETE) | ||
| if [[ "$SCOPES" != *"write"* ]] || [[ "$SCOPES" != *"delete"* ]]; then | ||
| echo "ERROR: DELETE requests require CLERK_BAPI_SCOPES=\"write,delete\" or --admin flag." >&2 | ||
| echo "Current CLERK_BAPI_SCOPES: \"$SCOPES\"" >&2 | ||
| exit 1 | ||
| fi | ||
| ;; | ||
| *) | ||
| echo "ERROR: Unknown HTTP method: $METHOD_UPPER" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| fi | ||
|
|
||
| # Base URL: use CLERK_REST_API_URL if set, otherwise default to production | ||
| BASE_URL="${CLERK_REST_API_URL:-https://api.clerk.com}" | ||
|
|
||
| # Build curl command | ||
| CURL_ARGS=( | ||
| -s | ||
| -X "$METHOD_UPPER" | ||
| "${BASE_URL}/v1${PATH_ARG}" | ||
| -H "Authorization: Bearer ${CLERK_SECRET_KEY:?CLERK_SECRET_KEY is not set}" | ||
| -H "Content-Type: application/json" | ||
| ) | ||
|
|
||
| if [[ -n "$BODY" ]]; then | ||
| CURL_ARGS+=(-d "$BODY") | ||
| fi | ||
|
|
||
| curl "${CURL_ARGS[@]}" | ||
165 changes: 165 additions & 0 deletions
165
.agents/skills/clerk-backend-api/scripts/extract-endpoint-detail.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| #!/usr/bin/env bash | ||
| # extract-endpoint-detail.sh | ||
| # | ||
| # Extracts full details for a specific endpoint from an OpenAPI YAML spec (stdin), | ||
| # including parameters, request body, responses, and all referenced component schemas. | ||
| # | ||
| # Usage: | ||
| # curl -s <spec-url> | bash extract-endpoint-detail.sh "/users/{user_id}/billing/subscription" "get" | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| ENDPOINT="${1:?Usage: extract-endpoint-detail.sh <path> <method>}" | ||
| METHOD="${2:?Usage: extract-endpoint-detail.sh <path> <method>}" | ||
| TMPDIR_WORK=$(mktemp -d) | ||
| trap 'rm -rf "$TMPDIR_WORK"' EXIT | ||
|
|
||
| SPEC="$TMPDIR_WORK/spec.yml" | ||
| cat > "$SPEC" | ||
|
|
||
| node - "$ENDPOINT" "$METHOD" "$SPEC" <<'SCRIPT' | ||
| const fs = require("fs"); | ||
| const endpoint = process.argv[2]; | ||
| const method = process.argv[3].toLowerCase(); | ||
| const specFile = process.argv[4]; | ||
| const lines = fs.readFileSync(specFile, "utf8").split("\n"); | ||
|
|
||
| const httpMethods = ["get", "post", "put", "patch", "delete", "options", "head"]; | ||
|
|
||
| // Locate paths: and components: sections | ||
| let pathsStart = -1, pathsEnd = -1, componentsStart = -1; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| if (/^paths:\s*$/.test(lines[i])) pathsStart = i; | ||
| else if (pathsStart >= 0 && pathsEnd < 0 && /^\S/.test(lines[i]) && i > pathsStart) pathsEnd = i; | ||
| if (/^components:\s*$/.test(lines[i])) componentsStart = i; | ||
| } | ||
| if (pathsEnd < 0) pathsEnd = lines.length; | ||
|
|
||
| // Find the target path + method block | ||
| let targetStart = -1, targetEnd = -1; | ||
| let currentPath = null; | ||
|
|
||
| for (let i = pathsStart + 1; i < pathsEnd; i++) { | ||
| const line = lines[i]; | ||
|
|
||
| // Path line: exactly 2 spaces + / | ||
| if (/^ {2}\/\S/.test(line)) { | ||
| currentPath = line.trim().replace(/:$/, ""); | ||
| continue; | ||
| } | ||
|
|
||
| // Method line: exactly 4 spaces + method name | ||
| const methodMatch = line.match(/^ {4}(\w+):\s*$/); | ||
| if (methodMatch && httpMethods.includes(methodMatch[1])) { | ||
| if (currentPath === endpoint && methodMatch[1] === method) { | ||
| targetStart = i; | ||
| // Find end of this method block | ||
| for (let j = i + 1; j < pathsEnd; j++) { | ||
| const nextLine = lines[j]; | ||
| // New method or new path | ||
| if (/^ {2}\/\S/.test(nextLine) || (/^ {4}\w+:\s*$/.test(nextLine) && httpMethods.some(m => nextLine.trim().startsWith(m + ":")))) { | ||
| targetEnd = j; | ||
| break; | ||
| } | ||
| } | ||
| if (targetEnd < 0) targetEnd = pathsEnd; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (targetStart < 0) { | ||
| console.error(`Endpoint not found: ${method.toUpperCase()} ${endpoint}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const blockLines = lines.slice(targetStart, targetEnd); | ||
|
|
||
| // Collect all $refs from the block | ||
| const allRefs = new Set(); | ||
| for (const bl of blockLines) { | ||
| const refMatch = bl.match(/\$ref:\s*['"]?(#\/[^'"}\s]+)['"]?/); | ||
| if (refMatch) allRefs.add(refMatch[1]); | ||
| } | ||
|
|
||
| // Resolve a $ref path to the raw YAML lines for that component | ||
| function resolveRef(ref) { | ||
| const parts = ref.replace("#/", "").split("/"); | ||
| // Find the component in the file by walking indentation | ||
| let searchStart = 0; | ||
| for (let p = 0; p < parts.length; p++) { | ||
| const indent = p * 2; | ||
| const target = " ".repeat(indent) + parts[p] + ":"; | ||
| let found = false; | ||
| for (let i = searchStart; i < lines.length; i++) { | ||
| if (lines[i].startsWith(target) && (lines[i] === target || lines[i][target.length] === " ")) { | ||
| searchStart = i + 1; | ||
| found = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!found) return null; | ||
| } | ||
|
|
||
| // Collect lines for this component (until same or lower indent) | ||
| const componentStart = searchStart - 1; | ||
| const baseIndent = parts.length * 2; | ||
| const result = []; | ||
| for (let i = searchStart; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| if (line.trim() === "") { result.push(line); continue; } | ||
| const lineIndent = line.length - line.trimStart().length; | ||
| if (lineIndent < baseIndent) break; | ||
| result.push(line); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| // Recursively resolve refs from component bodies | ||
| function collectDeepRefs(refSet, visited) { | ||
| const toProcess = [...refSet].filter(r => !visited.has(r)); | ||
| for (const ref of toProcess) { | ||
| visited.add(ref); | ||
| const body = resolveRef(ref); | ||
| if (!body) continue; | ||
| for (const bl of body) { | ||
| const refMatch = bl.match(/\$ref:\s*['"]?(#\/[^'"}\s]+)['"]?/); | ||
| if (refMatch && !visited.has(refMatch[1])) { | ||
| refSet.add(refMatch[1]); | ||
| } | ||
| } | ||
| } | ||
| // Recurse if new refs were found | ||
| const newRefs = [...refSet].filter(r => !visited.has(r)); | ||
| if (newRefs.length > 0) collectDeepRefs(refSet, visited); | ||
| } | ||
|
|
||
| collectDeepRefs(allRefs, new Set()); | ||
|
|
||
| // Output | ||
| console.log(`## \`${method.toUpperCase()}\` \`${endpoint}\`\n`); | ||
| console.log("### Endpoint Definition\n"); | ||
| console.log("```yaml"); | ||
| for (const bl of blockLines) { | ||
| console.log(bl); | ||
| } | ||
| console.log("```\n"); | ||
|
|
||
| if (allRefs.size > 0) { | ||
| console.log(`### Referenced Components (${allRefs.size})\n`); | ||
| const sorted = [...allRefs].sort(); | ||
| for (const ref of sorted) { | ||
| const name = ref.split("/").pop(); | ||
| const category = ref.replace("#/", "").split("/").slice(0, -1).join("/"); | ||
| console.log(`#### \`${name}\` (${category})\n`); | ||
| const body = resolveRef(ref); | ||
| if (body) { | ||
| console.log("```yaml"); | ||
| for (const bl of body) console.log(bl); | ||
| console.log("```\n"); | ||
| } else { | ||
| console.log("_(could not resolve)_\n"); | ||
| } | ||
| } | ||
| } | ||
| SCRIPT |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.