graphql-api — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited graphql-api (Agent Skill) and scored it 91/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} is the classic direct prompt-injection phrasing. Placed in a skill body that the agent reads as trusted instructions, it tries to make the agent abandon its prior rules and follow whatever comes next — a full system-prompt override.
ignore/disregard/forget … previous instructions sentence.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Use Case | Recommendation |
|---|---|
| TypeScript server | GraphQL Yoga + graphql |
| Python server | Strawberry (code-first) |
| React client | Apollo Client or urql |
| Next.js API | GraphQL Yoga in Route Handler |
| Schema-first TypeScript | GraphQL Code Generator |
# ✅ Good schema design
type User {
id: ID!
name: String!
email: String!
posts(first: Int = 10, after: String): PostConnection!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: DateTime
status: PostStatus!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
# Relay-style cursor pagination (preferred for large datasets)
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Input types for mutations (keep separate from output types)
input CreatePostInput {
title: String!
content: String!
}
input UpdatePostInput {
id: ID!
title: String
content: String
}
# Mutation return types - always return the mutated object
type CreatePostPayload {
post: Post
errors: [UserError!]!
}
type UserError {
field: String
message: String!
}
type Query {
me: User
user(id: ID!): User
post(id: ID!): Post
posts(first: Int, after: String, status: PostStatus): PostConnection!
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
updatePost(input: UpdatePostInput!): CreatePostPayload!
deletePost(id: ID!): Boolean!
}
type Subscription {
postCreated: Post!
postUpdated(id: ID!): Post!
}npm install graphql graphql-yoga// app/api/graphql/route.ts (Next.js App Router)
import { createYoga, createSchema } from 'graphql-yoga'
import { typeDefs } from './schema'
import { resolvers } from './resolvers'
import { createContext } from './context'
const yoga = createYoga({
schema: createSchema({ typeDefs, resolvers }),
context: createContext,
graphqlEndpoint: '/api/graphql',
fetchAPI: { Response, Request, ReadableStream },
})
export { yoga as GET, yoga as POST }
// context.ts
import { NextRequest } from 'next/server'
export async function createContext({ request }: { request: NextRequest }) {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
const user = token ? await verifyToken(token) : null
return { user, db }
}// resolvers.ts
import { Resolvers } from './generated/types'
export const resolvers: Resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' }
})
return user
},
user: async (_, { id }, { db }) => {
return db.users.findUnique({ where: { id } })
},
posts: async (_, { first = 10, after, status }, { db }) => {
const cursor = after ? Buffer.from(after, 'base64').toString() : undefined
const posts = await db.posts.findMany({
take: first + 1, // fetch one extra to determine hasNextPage
cursor: cursor ? { id: cursor } : undefined,
skip: cursor ? 1 : 0,
where: status ? { status } : undefined,
orderBy: { createdAt: 'desc' },
})
const hasNextPage = posts.length > first
const edges = posts.slice(0, first).map(post => ({
node: post,
cursor: Buffer.from(post.id).toString('base64'),
}))
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: await db.posts.count(),
}
},
},
Mutation: {
createPost: async (_, { input }, { user, db }) => {
if (!user) return { post: null, errors: [{ message: 'Not authenticated' }] }
try {
const post = await db.posts.create({
data: { ...input, authorId: user.id, status: 'DRAFT' }
})
return { post, errors: [] }
} catch (err) {
return { post: null, errors: [{ field: 'general', message: String(err) }] }
}
},
},
// Field resolvers for relationships
User: {
posts: async (user, { first = 10, after }, { loaders }) => {
// Use DataLoader to prevent N+1
return loaders.postsByUserId.load({ userId: user.id, first, after })
},
},
Post: {
author: async (post, _, { loaders }) => {
return loaders.userById.load(post.authorId)
},
},
Subscription: {
postCreated: {
subscribe: (_, __, { pubsub }) => pubsub.subscribe('POST_CREATED'),
},
},
}The N+1 problem: fetching 10 posts then making 10 separate DB calls for each author.
// loaders.ts
import DataLoader from 'dataloader'
import { db } from './database'
// Batch: one DB call fetches all requested users
export function createUserLoader() {
return new DataLoader<string, User | null>(async (userIds) => {
const users = await db.users.findMany({
where: { id: { in: [...userIds] } }
})
const byId = new Map(users.map(u => [u.id, u]))
return userIds.map(id => byId.get(id) ?? null)
})
}
// Add to context so loaders are per-request (not shared globally)
export function createLoaders() {
return {
userById: createUserLoader(),
// Add more loaders here
}
}
// context.ts
export function createContext({ request }) {
return {
user: ...,
db,
loaders: createLoaders(), // fresh per request
}
}npm install dataloader// Helper: protect resolvers
function requireAuth<T>(
resolver: (parent: unknown, args: T, context: Context) => unknown
) {
return (parent: unknown, args: T, context: Context) => {
if (!context.user) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } }
})
}
return resolver(parent, args, context)
}
}
// Usage
const resolvers = {
Mutation: {
createPost: requireAuth(async (_, { input }, { user, db }) => {
return db.posts.create({ data: { ...input, authorId: user.id } })
}),
},
}import strawberry
from strawberry.fastapi import GraphQLRouter
from typing import Optional
@strawberry.type
class User:
id: strawberry.ID
name: str
email: str
@strawberry.type
class Post:
id: strawberry.ID
title: str
author_id: strawberry.ID
@strawberry.field
async def author(self, info: strawberry.types.Info) -> User:
return await info.context.loaders.user_by_id.load(self.author_id)
@strawberry.input
class CreatePostInput:
title: str
content: str
@strawberry.type
class Query:
@strawberry.field
async def me(self, info: strawberry.types.Info) -> Optional[User]:
return info.context.user
@strawberry.field
async def post(self, id: strawberry.ID, info: strawberry.types.Info) -> Optional[Post]:
return await info.context.db.get_post(id)
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_post(self, input: CreatePostInput, info: strawberry.types.Info) -> Post:
if not info.context.user:
raise Exception("Not authenticated")
return await info.context.db.create_post(input, info.context.user.id)
schema = strawberry.Schema(query=Query, mutation=Mutation)
graphql_app = GraphQLRouter(schema)// Apollo setup
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '@apollo/client'
const client = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: { keyArgs: ['status'], merge: true } // cursor pagination merge
}
}
}
}),
headers: { Authorization: `Bearer ${token}` },
})
// Query
const GET_POSTS = gql`
query GetPosts($first: Int, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
author { id name }
}
cursor
}
pageInfo { hasNextPage endCursor }
}
}
`
function PostList() {
const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
variables: { first: 10 }
})
if (loading) return <Spinner />
if (error) return <Error message={error.message} />
return (
<>
{data.posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
{data.posts.pageInfo.hasNextPage && (
<button onClick={() => fetchMore({
variables: { after: data.posts.pageInfo.endCursor }
})}>
Load More
</button>
)}
</>
)
}UserError in payload; use exceptions only for system errorsrequireAuth at the resolver level, not just the schemaintrospection: false in production GraphQL servers to prevent schema leakage~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.