kickjs-skill-c30610 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited kickjs-skill-c30610 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
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.
KickJS has strong conventions whose violation causes silent breakage — broken HMR, env values silently undefined, decorators firing at the wrong time, plugins dropped from the DI container, lockfile drift between packages. The framework's value is the conventions; the skill's job is to keep you aligned with them.
This skill operates in two modes and the right one depends on which repository you're in. Detect mode first, load the matching reference file, then apply the shared invariants below.
Run these checks in order. The first match wins.
contributor → both pnpm-workspace.yaml AND packages/kickjs/ exist at repo root
adopter → kick.config.ts exists, OR package.json depends on @forinda/kickjs*
neither → don't trigger; this isn't a KickJS context, fall back to default behaviourThe reason contributor wins when both apply: if you're editing the framework itself, adopter conventions about "use the published @forinda/kickjs API" are misleading — you ARE the published API.
references/contributor.md. Covers monorepo layout, Turbo + Vite + tsc build pipeline, pnpm + workspace deps, package add/remove flow, lockstep release scripts, BYO recipe pattern, the "only write to @forinda/kickjs" rule.references/adopter.md. Covers defineAdapter/definePlugin, decorator usage, env wiring, module file naming for HMR, contributors vs middleware, tests via Container.create(), the named app export, BYO swaps for the 6 v5-removed wrappers.Both modes also consult the shared invariants in Step 3 below — those apply everywhere.
These are universal. Violating any of them produces silent breakage that the type system will not catch.
defineAdapter / definePlugin factories — never class-basedAdapters and plugins are factories, not classes:
// Right
export const myAdapter = defineAdapter({
name: 'my-adapter',
beforeStart: ({ container }) => { /* … */ },
shutdown: () => { /* … */ },
})
// Wrong — class-based adapter is a v3 pattern, dropped in v4
class MyAdapter implements AppAdapter { /* … */ }Why: factory shape lets the framework introspect every hook ahead of time (devtools, typegen, lifecycle ordering), narrows the dependency graph for shutdown phases, and keeps the v5 BYO recipes — and the user's adapters — using the same primitives the framework uses internally.
@Controller() takes no path argument@Controller() // Right — mount prefix comes from routes().path
@Controller('/users') // Wrong — path arg removed in v4The mount prefix comes from routes({ path: '/users' }) in the module, not the decorator. Mixing both produces double prefixes that look correct in tests and 404 in production.
// Right — adopter scope
export const USER_REPO = createToken<UserRepo>('app/users/repository')
// Right — first-party uses reserved 'kick/' prefix
export const PRISMA_CLIENT = createToken<PrismaClient>('kick/prisma/Client')
// Wrong — Symbol() doesn't survive serialization, devtools, or worker boundaries
export const USER_REPO = Symbol('UserRepo')Adopter projects must NEVER use the kick/ prefix — it's reserved for the framework. Use your own scope (typically the project name or app/). The contributor reference covers what scope to use for new first-party packages.
Container.create() for test isolation// Right — every test gets a fresh, isolated container
beforeEach(() => {
container = Container.create()
container.register(/* … */)
})
// Wrong — shared singleton bleeds state between tests; reset() is incomplete
beforeEach(() => {
Container.getInstance().reset()
})Decorators fire at class-definition time and write to the global container. Tests that share Container.getInstance() race each other; tests that reset() lose decorator-registered metadata. Container.create() gives each test its own isolated container without losing the framework's setup.
getRequestValue<K>(key) for service-level reads — never expose setRequestValue// Right — services read context values via the typed helper
import { getRequestValue } from '@forinda/kickjs'
const tenant = getRequestValue('tenant')
// Wrong — internal store API, not part of the public surface
const store = requestStore.getStore()
const tenant = store?.values.get('tenant')Writes flow either through ctx.set('key', value) inside a handler or as the return value of a defineContextDecorator({ resolve }). There is intentionally NO setRequestValue export — letting services mutate the per-request map produces ordering bugs that are expensive to debug. If you need a value to be available, contribute it via a context decorator instead.
@Middleware() for ctx-populationIf the only job of a piece of middleware is to compute a value other code reads off ctx, write it as a defineContextDecorator (or defineHttpContextDecorator when HTTP-specific), not a @Middleware():
// Right — typed pipeline with deps + dependsOn ordering
const LoadTenant = defineHttpContextDecorator({
key: 'tenant',
deps: { repo: TENANT_REPO },
resolve: (ctx, { repo }) => repo.findById(ctx.req.headers['x-tenant-id'] as string),
})
@LoadTenant
@Get('/me')
me(ctx: RequestContext) { ctx.json(ctx.get('tenant')) }Middleware still wins for short-circuiting responses, response-stream mutation, and pre-route-matching work. The split is documented in docs/guide/context-decorators.md.
@forinda/kickjs-graphql, -otel, -cron, -mailer, -multi-tenant, -notifications were removed in v5. Do NOT add them as dependencies in adopter projects, do NOT reference them as published packages in contributor docs/code. Use the BYO recipes at docs/guide/{cron,mailer,multi-tenancy,notifications,otel,graphql}.md — each shows how to wire the upstream library through defineAdapter/definePlugin directly. The kick add cron (etc.) command is wired to surface the BYO guide URL instead of erroring.
// Right — call the factory; each instance owns its own state (Redis client, etc.)
bootstrap({ adapters: [redisAdapter({ url: env.REDIS_URL })], plugins: [authPlugin()] })
// Wrong — passing the factory itself; the framework will not invoke it for you
bootstrap({ adapters: [redisAdapter], plugins: [authPlugin] })The closure-over-config pattern is why defineAdapter/definePlugin exist — every adapter instance owns isolated state, and that's how shutdown hooks know which Redis client to close.
Within a single request, framework order is:
validation (Zod from route decorators)
→ file upload (@FileUpload)
→ class-level @Middleware()
→ method-level @Middleware()
→ context contributors (sorted by dependsOn topo order)
→ handlerValidation always runs first — you cannot put auth @Middleware() "before" Zod validation by reordering decorators. If you need work to happen pre-validation, use bootstrap({ middleware: [...] }) (global) or an adapter's beforeRoutes phase.
RequestContext — 3 instances per request, one shared bagEach layer (middleware → contributors → handler) sees a different RequestContext JS object. They all read/write the same AsyncLocalStorage-backed bag, but object identity differs. Two consequences:
ctx.foo = bar (direct property assignment) does NOT survive across layers. The next layer's ctx is a fresh object. Always ctx.set('foo', bar) / ctx.get('foo').getRequestValue('foo') (the typed ALS reader). Reading ctx.foo from a service is impossible by design — services don't see ctx.Container.create() for tests as established. For runtime: scope-rule reminder.@Autowired() on properties is lazy-resolved (first access). @Inject(token) in constructors is eager (DI bootstrap). Cycle detection only catches eager cycles; a lazy cycle errors at first access in production.createToken<T>(name) returns a unique frozen object by reference. Two files calling createToken<X>('foo') produce two different tokens. Always export const X = createToken<...>('x') and import the same const everywhere.module.register(container) — @Service / @Repository auto-register only the concrete class.dependsOn typos fail at boot, not at request timedefineHttpContextDecorator({ dependsOn: ['tenent'] }) (typo) throws MissingContributorError at bootstrap(). This is intentional — bad pipelines should fail fast. Don't try to silence the error; fix the spelling.
After loading the matching reference and the invariants above, apply guidance in this order when responding:
@forinda/* imports, no kick.config.ts. Don't inject KickJS conventions into a non-KickJS project.KickJS users are technical; they're picking a decorator framework on purpose and have likely used Nest. Don't over-explain decorators or DI. Do explain the why behind any KickJS-specific divergence — most rules exist because some adopter hit silent breakage and we wrote the rule down.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.