navori 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +54 -0
- package/dist/assets/core/core-assets/lib-skills/zod-validation.md +32 -21
- package/dist/assets/core/core-assets/presets/bun-keystone/skills/keystone-graphql.md +56 -0
- package/dist/assets/core/core-assets/presets/bun-keystone/skills/prisma-keystone.md +21 -11
- package/dist/assets/core/core-assets/presets/bun-keystone.json +5 -0
- package/dist/index.js +235 -235
- package/package.json +1 -1
|
@@ -334,6 +334,60 @@ else
|
|
|
334
334
|
skeleton="${cmd}${_nl}"
|
|
335
335
|
fi
|
|
336
336
|
|
|
337
|
+
# ─── Fast path: a command no rule below can possibly match ───────────────────
|
|
338
|
+
#
|
|
339
|
+
# WHY: this guard runs on EVERY Bash call, and in auto mode the host tells the
|
|
340
|
+
# agent to work through the shell — so it is in front of reads, greps and edits
|
|
341
|
+
# alike. Measured, it costs ~46 ms against a ~2 ms process floor: the other
|
|
342
|
+
# ~44 ms is the `sed`/`grep` pipeline below, roughly forty forks. Over one
|
|
343
|
+
# audited session that is 901 invocations and ~40 s of wall clock spent proving
|
|
344
|
+
# that `cat file` is not `rm -rf /`. On 9,398 real commands from that session,
|
|
345
|
+
# 80% contain none of the tokens below.
|
|
346
|
+
#
|
|
347
|
+
# WHY IT IS SOUND, which is the only part that matters in a security control:
|
|
348
|
+
# every `block` in this file needs one of these literal substrings to survive
|
|
349
|
+
# into the string its rule reads — `git` (rules 1-2, via `$git_cp`), `rm`
|
|
350
|
+
# (rule 3), `:(` (rule 4), `/dev/` (rule 5), and `>`/`sed`/`tee` (rule 6, the
|
|
351
|
+
# three write verbs it recognizes). No rule can fire without one.
|
|
352
|
+
#
|
|
353
|
+
# The probe is the command with quotes, backslashes and newlines REMOVED, which
|
|
354
|
+
# is what makes the argument hold under the obfuscations the rules normalize
|
|
355
|
+
# away: `r'm' -rf ~` becomes `rm -rf ~`, `\git` becomes `git`, and a `g\<NL>it`
|
|
356
|
+
# split across a continuation is rejoined. Removal is the safe direction on
|
|
357
|
+
# purpose — none of the tokens contains a quote, a backslash or a newline, so
|
|
358
|
+
# stripping those can only CREATE matches, never destroy one. A false match
|
|
359
|
+
# costs the full analysis; a false miss would be a hole, and cannot happen here.
|
|
360
|
+
#
|
|
361
|
+
# PLACEMENT is load-bearing too: this sits AFTER the three fail-closed size
|
|
362
|
+
# limits (`CMD_MAX`, `LINE_MAX`, `HEREDOC_PROBE_MAX`) so a command too large to
|
|
363
|
+
# inspect is still blocked, exactly as before. It skips the rule machinery, never
|
|
364
|
+
# a refusal. And it reads `$cmd`, not `$skeleton`: the skeleton drops inert
|
|
365
|
+
# heredoc bodies, so scanning it would narrow what the probe sees.
|
|
366
|
+
#
|
|
367
|
+
# FAST_MAX exists because `${var//pat/}` is NOT linear in bash: on a string past
|
|
368
|
+
# ~64 KB with thousands of matches it goes superlinear hard — measured, 1024
|
|
369
|
+
# lines of quoted `echo` strip instantly and 2048 lines had not finished after
|
|
370
|
+
# nine minutes. An optimization that hangs the guard is a guard that hangs, so
|
|
371
|
+
# the probe is only built for a command small enough for the cost to be free.
|
|
372
|
+
# Above it, the full analysis runs exactly as it did before this block existed.
|
|
373
|
+
# 4096 covers 98.5% of 1,089 real commands from the measured session (p50 370,
|
|
374
|
+
# p90 1650) and sits sixteen times below the cliff.
|
|
375
|
+
FAST_MAX=4096
|
|
376
|
+
if [ "${#cmd}" -le "$FAST_MAX" ]; then
|
|
377
|
+
_fast=${cmd//\'/}
|
|
378
|
+
_fast=${_fast//\"/}
|
|
379
|
+
_fast=${_fast//\\/}
|
|
380
|
+
_fast=${_fast//"${_nl}"/}
|
|
381
|
+
case "$_fast" in
|
|
382
|
+
*git*|*rm*|*sed*|*tee*|*'/dev/'*|*'>'*|*':('*) ;;
|
|
383
|
+
*)
|
|
384
|
+
navori_audit_verdict="skip"
|
|
385
|
+
navori_audit_reason="no rule token in the command"
|
|
386
|
+
exit 0
|
|
387
|
+
;;
|
|
388
|
+
esac
|
|
389
|
+
fi
|
|
390
|
+
|
|
337
391
|
# A `$(…)`/backtick substitution outside single quotes RUNS: `git commit -m
|
|
338
392
|
# "$(rm -rf ~)"` is an invocation wearing a message's clothes. This check must
|
|
339
393
|
# read the skeleton BEFORE the flag values are elided below — eliding first
|
|
@@ -6,36 +6,44 @@ type: reference
|
|
|
6
6
|
|
|
7
7
|
# Zod Validation — the canonical pattern
|
|
8
8
|
|
|
9
|
-
One schema per resource (`<resource>.schema.ts`),
|
|
9
|
+
One schema per resource (`<resource>.schema.ts`), a generic validate middleware, and the DTO from `z.infer`.
|
|
10
10
|
|
|
11
11
|
## When to use this skill
|
|
12
12
|
|
|
13
|
-
When creating a schema, adding validation to an endpoint, inferring a DTO, or touching
|
|
13
|
+
When creating a schema, adding validation to an endpoint, inferring a DTO, or touching body/query/params input.
|
|
14
|
+
|
|
15
|
+
**Check the installed major** (`package.json`): snippets are **Zod 4**, with the v3 form annotated inline where they differ.
|
|
14
16
|
|
|
15
17
|
## The pattern
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
The middleware (Express here) parses `req[target]`, throws `BadRequestError` from the first issue, or reassigns `req[target] = parsed`:
|
|
18
20
|
|
|
19
21
|
```ts
|
|
20
|
-
const objectId = z.string().regex(/^[a-f\d]{24}$/i, 'Invalid ObjectId');
|
|
21
|
-
|
|
22
22
|
export const createResourceSchema = z.object({
|
|
23
|
-
owner:
|
|
24
|
-
resourceType: z.nativeEnum(ResourceTypeEnum)
|
|
23
|
+
owner: z.uuid(), // v3: z.string().uuid()
|
|
24
|
+
resourceType: z.enum(ResourceTypeEnum), // v3: z.nativeEnum(ResourceTypeEnum)
|
|
25
25
|
page: z.coerce.number().int().positive().default(1)
|
|
26
26
|
});
|
|
27
27
|
export const updateResourceSchema = createResourceSchema.partial();
|
|
28
28
|
export type CreateResourceDto = z.infer<typeof createResourceSchema>;
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Route: `router.post('/', validate(createResourceSchema, 'body'), ...)`. The controller's `req.body as CreateResourceDto` cast is safe: already parsed.
|
|
32
|
+
|
|
33
|
+
`safeParse` failure → readable 4xx (v4):
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const parsed = schema.safeParse(req.body);
|
|
37
|
+
if (!parsed.success) return res.status(400).json({ error: z.prettifyError(parsed.error) });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`z.prettifyError(e)` → readable string; `z.treeifyError(e)` → input-shaped object for per-field errors; `e.issues` → raw array (both majors). **v3 has neither:** `e.format()` / `e.flatten()`.
|
|
32
41
|
|
|
33
42
|
## Gotchas that bite
|
|
34
43
|
|
|
35
|
-
- **A bare
|
|
36
|
-
- **Query strings are always strings.** Without `z.coerce`, `z.number()` rejects them.
|
|
37
|
-
- **Unknown keys are silently dropped:** `z.object({...})` *strips*, so a typo in the body (`{ ammount }`) is lost with no error. On mutation endpoints use `z.strictObject({...})
|
|
38
|
-
- **Version:** this skill assumes Zod v3. In **v4**: `z.nativeEnum`→`z.enum`, `z.string().datetime()`→`z.iso.datetime()`, and `{ message }`→`{ error }` in the error options.
|
|
44
|
+
- **A bare id** (`z.string()`) lets `"abc"` through and the layer below breaks on it — a driver cast error becomes a 500 instead of a clean 400. Validate the id's *shape*: `z.uuid()`, `z.cuid()`, `z.coerce.number().int()` (serial) or `.regex(...)`. *Mongo:* `z.string().regex(/^[a-f\d]{24}$/i, 'Invalid ObjectId')` — see the `mongoose` skill.
|
|
45
|
+
- **Query strings are always strings.** Without `z.coerce`, `z.number()` rejects them. **Footgun:** `z.coerce.number()` uses `Number()`, so `""`/`" "`/`null` → `0` (an empty `?page=` passes as `0`); set explicit bounds or `z.string().regex(...).transform(Number)`.
|
|
46
|
+
- **Unknown keys are silently dropped:** `z.object({...})` *strips*, so a typo in the body (`{ ammount }`) is lost with no error. On mutation endpoints use `z.strictObject({...})`.
|
|
39
47
|
|
|
40
48
|
## Hard rules
|
|
41
49
|
|
|
@@ -43,26 +51,29 @@ In the route: `router.post('/', validate(createResourceSchema, 'body'), ...)`. I
|
|
|
43
51
|
2. The schema lives in `<resource>.schema.ts`, never in the routes.
|
|
44
52
|
3. DTO always with `z.infer` — don't maintain two parallel types.
|
|
45
53
|
4. No `z.any()`: it equals `any`, forbidden in new code.
|
|
46
|
-
5.
|
|
47
|
-
6.
|
|
54
|
+
5. One validator per endpoint — no Joi + Zod mix; migrate the whole endpoint.
|
|
55
|
+
6. Ids validated by shape, never a bare `z.string()`; query/params with `z.coerce`.
|
|
48
56
|
|
|
49
57
|
## Quick table
|
|
50
58
|
|
|
59
|
+
`v4 · v3` where they differ.
|
|
60
|
+
|
|
51
61
|
| Need to validate | Helper |
|
|
52
62
|
|---|---|
|
|
53
|
-
|
|
|
63
|
+
| Id | shape-specific, never `z.string()`: `z.uuid()` · `z.string().uuid()` |
|
|
54
64
|
| Non-empty string | `z.string().trim().min(1)` |
|
|
55
65
|
| Number from query | `z.coerce.number().int().positive()` |
|
|
56
|
-
| Date | `z.coerce.date()`
|
|
57
|
-
| TS enum / literal | `z.
|
|
66
|
+
| Date | `z.coerce.date()`, `z.iso.datetime()` · `z.string().datetime()` |
|
|
67
|
+
| TS enum / literal | `z.enum(MyEnum)` · `z.nativeEnum(MyEnum)`; `z.enum(['a','b'])` |
|
|
58
68
|
| Partial update | `createSchema.partial()` |
|
|
59
|
-
| Cross-field validation | `.refine((d) => ..., {
|
|
69
|
+
| Cross-field validation | `.refine((d) => ..., { error, path })` · `{ message, path }` |
|
|
70
|
+
| Error → 4xx body | `z.prettifyError(e)` · `e.format()` |
|
|
60
71
|
|
|
61
72
|
## Before declaring done
|
|
62
73
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
74
|
+
- Schema in `<resource>.schema.ts`, DTO from `z.infer`, endpoint wired with `validate(schema, target)` — no inline validation in the controller.
|
|
75
|
+
- Ids validated by shape, not a bare `z.string()`; query fields with `z.coerce`.
|
|
76
|
+
- APIs match the installed major — no `z.nativeEnum` on v4, no `z.prettifyError` on v3.
|
|
66
77
|
- `{{qualityGate.fast}}` green.
|
|
67
78
|
|
|
68
79
|
<!-- navori:user-section -->
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keystone-graphql
|
|
3
|
+
description: Custom GraphQL in Keystone 6 — extendGraphqlSchema, the project-scoped gWithContext<Context>() builder, and the access guard every custom resolver must open with. Use when adding or touching a custom mutation, query or resolver.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Keystone Custom GraphQL — extendGraphqlSchema
|
|
8
|
+
|
|
9
|
+
## When to use this skill
|
|
10
|
+
|
|
11
|
+
Before adding or changing a custom mutation/query, or when debugging a resolver that won't type-check. A custom resolver does **not** go through the list's `access`: the guard it doesn't write is a guard nobody writes.
|
|
12
|
+
|
|
13
|
+
## The builder must carry the project `Context`
|
|
14
|
+
|
|
15
|
+
Keystone 8 renamed `graphql` to `g` and changed how it's typed. Importing `g`/`graphql` straight from `@keystone-6/core` gives `GWithContext<KeystoneContext>` — bound to Keystone's **base** context, not the project's generated `Context`. Resolvers are typed against that generated `Context`, so a field built with the unparametrized builder fails to unify (`TS2322`/`TS2345`): the two contexts are structurally similar but nominally distinct types.
|
|
16
|
+
|
|
17
|
+
Keystone's documented fix: **one project module builds `gWithContext<Context>()` once**, and everything imports `g` from there. Never import `graphql`/`g` from `@keystone-6/core` in project code.
|
|
18
|
+
|
|
19
|
+
**One exception — `virtual()` fields on a list.** Keystone types their `field` as `VirtualFieldGraphQLField<BaseItem, KeystoneContext<BaseKeystoneTypeInfo>>`, against the base context, so the project-scoped `g` yields a type that does not unify with it (`TS2322`). Those import `g` from `@keystone-6/core` directly, with a comment saying why; everything under `extendGraphqlSchema` uses the shared builder.
|
|
20
|
+
|
|
21
|
+
## The pattern: pure resolver + thin field
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
/** Accepts an open appeal. Only a moderator may resolve one. */
|
|
25
|
+
export async function resolveAcceptAppeal(
|
|
26
|
+
args: { appealId: string },
|
|
27
|
+
context: Context, // the GENERATED Context, not KeystoneContext
|
|
28
|
+
): Promise<AppealResult> {
|
|
29
|
+
requireModerator(context, "…"); // guard FIRST, before any data
|
|
30
|
+
// …
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const acceptAppeal = g.field({ // g from the project module
|
|
34
|
+
type: g.object<AppealResult>()({ name: "AcceptAppealPayload", fields: { /* … */ } }),
|
|
35
|
+
args: { appealId: g.arg({ type: g.nonNull(g.ID) }) },
|
|
36
|
+
resolve: (_root, { appealId }, context: Context) => resolveAcceptAppeal({ appealId }, context),
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Wire it with `extendGraphqlSchema: g.extend((base) => ({ mutation: { … }, query: { … } }))`; `base.object("Report")` reuses a list's generated type instead of redeclaring one.
|
|
41
|
+
|
|
42
|
+
## Hard rules
|
|
43
|
+
|
|
44
|
+
1. **Guard first.** Every custom mutation/query checks session/role before touching data — the list `access` never runs here, so skipping it is an access bypass. Shared guards live in one module (see `keystone-access`).
|
|
45
|
+
2. **The resolver is a pure exported function**, separate from the `g.field` wrapping it: `(args, context) => payload`. Testable without booting GraphQL, and the unit under test (see `keystone-testing`).
|
|
46
|
+
3. **`g` comes from the project builder**, never from `@keystone-6/core` — `virtual()` fields excepted.
|
|
47
|
+
4. **Args are untrusted.** Validate them; never forward a raw `*CreateInput` into a write: the resolver runs through `context.sudo()`, which bypasses field-level access, so unfiltered `data` is mass-assignment. Whitelist what the client may supply.
|
|
48
|
+
5. **`context.sudo().db` inside the resolver** (see `keystone-models`); errors surface bounded messages, never internals.
|
|
49
|
+
|
|
50
|
+
## Before declaring the change "done"
|
|
51
|
+
|
|
52
|
+
- `{{qualityGate.fast}}` green.
|
|
53
|
+
- No `g`/`graphql` imported from `@keystone-6/core` outside the shared builder and `virtual()` fields.
|
|
54
|
+
- Every new mutation/query calls its guard before the first read or write.
|
|
55
|
+
- The resolver is exported and unit-tested with a mocked context.
|
|
56
|
+
- No raw client input reaches a `sudo()` write unfiltered.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prisma-keystone
|
|
3
|
-
description: Prisma under Keystone
|
|
3
|
+
description: Prisma under Keystone — generated schema.prisma (don't edit by hand), the Prisma 7 driver adapter and generated client, migrations via the Prisma CLI. Use when changing the data shape, wiring a DB connection or running migrations.
|
|
4
4
|
type: reference
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,27 +8,35 @@ type: reference
|
|
|
8
8
|
|
|
9
9
|
## When to use this skill
|
|
10
10
|
|
|
11
|
-
When
|
|
11
|
+
When changing the DB shape, running or reviewing migrations, opening a database connection, or writing a seed/backfill script.
|
|
12
12
|
|
|
13
13
|
## Base rule: the schema is derived, not source
|
|
14
14
|
|
|
15
15
|
- **`schema.prisma` and `schema.graphql` are autogenerated by Keystone** from the lists. The source of truth is the `models/` files. **Never edit them by hand.**
|
|
16
|
-
- To change the DB: edit the list
|
|
17
|
-
- Don't read
|
|
16
|
+
- To change the DB: edit the list, regenerate and migrate — Keystone rewrites the schema.
|
|
17
|
+
- Don't read all of `schema.prisma` to "understand the types": it's long and derived; `grep` it or read the list.
|
|
18
|
+
|
|
19
|
+
## Prisma 7: generated client, driver adapter
|
|
20
|
+
|
|
21
|
+
- **Import the client from the generator's `output`, never from `@prisma/client`.** The `generator client` block in `schema.prisma` declares the directory it writes to; that path is the module every import must use — read it, don't guess.
|
|
22
|
+
- **`new PrismaClient()` with no options throws.** Prisma 7 dropped the Rust engine: the client needs a driver adapter (`PrismaPg` from `@prisma/adapter-pg` on Postgres). Every process that talks to the DB (Keystone via `prismaClientOptions`, workers, scripts) needs one: build it once in a shared module, not per entrypoint.
|
|
23
|
+
- **Pool settings moved from the URL to the adapter.** `connection_limit`, `connect_timeout` and `pool_timeout` on `DATABASE_URL` were read by the engine; the `pg` adapter **ignores them silently**. They are pool options now (`max`, `connectionTimeoutMillis`). Set `connectionTimeoutMillis` explicitly: `pg` defaults it to 0 ("wait forever"), turning a database blip into a hung request nothing reclaims.
|
|
18
24
|
|
|
19
25
|
## Migrations (via the Prisma CLI)
|
|
20
26
|
|
|
21
27
|
```bash
|
|
22
|
-
#
|
|
28
|
+
# dev: generate + apply a migration
|
|
23
29
|
prisma migrate dev --name <short-description>
|
|
24
30
|
|
|
25
|
-
#
|
|
31
|
+
# deploy: apply generated migrations
|
|
26
32
|
prisma migrate deploy
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
Run the Prisma CLI directly
|
|
35
|
+
Run the Prisma CLI directly: the `keystone prisma ...` wrapper was **removed in `@keystone-6/core` v8**, so a leftover `keystone prisma migrate` fails with `unknown command`. It existed to pick up Keystone's database config; from Prisma 7 that config lives in `prisma.config.ts`, loaded by the bare CLI.
|
|
36
|
+
|
|
37
|
+
Read that file before assuming any CLI input: `migrations.path` says where migrations live (Prisma's `prisma/migrations` default is not a given), `datasource.shadowDatabaseUrl` replaces the `--shadow-database-url` flag `prisma migrate diff` lost, and its side-effect import of the env loader is what loads `.env` — Prisma 7 no longer does.
|
|
30
38
|
|
|
31
|
-
Review
|
|
39
|
+
Review generated SQL before committing: a destructive migration (dropping a column with data) needs a data plan.
|
|
32
40
|
|
|
33
41
|
## context.prisma — only in scripts
|
|
34
42
|
|
|
@@ -37,11 +45,13 @@ context.sudo().db.Model; // app runtime (hooks/services) — see keystone-model
|
|
|
37
45
|
context.prisma; // ONLY seed/migration/backfill scripts — never at runtime
|
|
38
46
|
```
|
|
39
47
|
|
|
40
|
-
`context.prisma`
|
|
48
|
+
`context.prisma` is the raw client (no Keystone access control or hooks): right for a seed or bulk backfill, wrong in a hook or resolver — there, always `context.sudo().db`.
|
|
41
49
|
|
|
42
50
|
## Before declaring the change "done"
|
|
43
51
|
|
|
44
52
|
- `{{qualityGate.fast}}` green.
|
|
45
|
-
- Neither `schema.prisma` nor `schema.graphql` was edited by hand
|
|
46
|
-
- Every new migration is committed
|
|
53
|
+
- Neither `schema.prisma` nor `schema.graphql` was edited by hand.
|
|
54
|
+
- Every new migration is committed with the list change that originates it.
|
|
47
55
|
- No `context.prisma` outside `scripts/`.
|
|
56
|
+
- No import from `@prisma/client`: every client import resolves to the generator's `output`.
|
|
57
|
+
- No pool setting as a `DATABASE_URL` query param — only the adapter reads those.
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"id": "keystone-rest",
|
|
37
37
|
"relPath": "presets/bun-keystone/skills/keystone-rest.md",
|
|
38
38
|
"destRelPath": ".claude/skills/keystone-rest.md"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "keystone-graphql",
|
|
42
|
+
"relPath": "presets/bun-keystone/skills/keystone-graphql.md",
|
|
43
|
+
"destRelPath": ".claude/skills/keystone-graphql.md"
|
|
39
44
|
}
|
|
40
45
|
],
|
|
41
46
|
"hooks": []
|