enigma-cli 1.32.3 → 1.32.5
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/assets/commands/gate.md +40 -7
- package/assets/memory/AGENTS.md +5 -3
- package/assets/memory/CLAUDE.md +5 -3
- package/assets/skills/anti-overengineering-policy/skill.json +1 -1
- package/assets/skills/anti-overengineering-review/skill.json +1 -1
- package/assets/skills/backend-policy/SKILL.md +2 -0
- package/assets/skills/backend-policy/skill.json +4 -4
- package/assets/skills/ciphera-style-policy/skill.json +1 -1
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/skill.json +1 -1
- package/assets/skills/database-expert/SKILL.md +13 -1
- package/assets/skills/database-expert/skill.json +4 -4
- package/assets/skills/debugging-policy/skill.json +1 -1
- package/assets/skills/dependency-policy/skill.json +1 -1
- package/assets/skills/email-policy/skill.json +1 -1
- package/assets/skills/frontend-design/skill.json +1 -1
- package/assets/skills/frontend-policy/SKILL.md +67 -1
- package/assets/skills/frontend-policy/skill.json +5 -5
- package/assets/skills/git-policy/skill.json +1 -1
- package/assets/skills/logo-sourcing-policy/skill.json +1 -1
- package/assets/skills/security-policy/SKILL.md +37 -1
- package/assets/skills/security-policy/skill.json +4 -4
- package/assets/skills/skill-creator/skill.json +1 -1
- package/assets/skills/task-completion-policy/skill.json +1 -1
- package/assets/skills/technical-writing-policy/skill.json +1 -1
- package/assets/skills/testing-policy/skill.json +1 -1
- package/assets/skills/validation-policy/SKILL.md +33 -2
- package/assets/skills/validation-policy/skill.json +4 -4
- package/bin/checksums.json +4 -4
- package/dist/guardrails.js +381 -4
- package/package.json +1 -1
package/assets/commands/gate.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Validate code changes through the enigma gate pipeline - automated review, tests, lint, docs, push, PR, and CI - before they reach the configured push target. Use when the user asks to run the gate, gate/ship/validate their changes, push safely, do a task and then validate it, or invokes /gate.
|
|
3
|
-
argument-hint: [task] | (bare to gate already-committed work) | "skip the lint step"-style requests
|
|
2
|
+
description: Validate code changes through the enigma gate pipeline - automated review, tests, lint, docs, push, PR, and CI - before they reach the configured push target. Use when the user asks to run the gate, gate/ship/validate their changes, push safely, do a task and then validate it, or invokes /gate. Also the way to turn the gate on or off (/gate on, /gate off, /gate status) when the user asks to enable, disable, or stop it running automatically.
|
|
3
|
+
argument-hint: [task] | (bare to gate already-committed work) | on | off | status | "skip the lint step"-style requests
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /gate
|
|
@@ -18,6 +18,33 @@ for something specific, translate it into the matching `axi run` flags yourself
|
|
|
18
18
|
for example "skip the lint step" becomes `--skip=lint`. Run
|
|
19
19
|
`enigma gate axi run --help` to see the flags.
|
|
20
20
|
|
|
21
|
+
## Turning the gate on and off
|
|
22
|
+
|
|
23
|
+
`/gate on`, `/gate off` and `/gate status` are settings, not runs - handle them
|
|
24
|
+
first and stop there, without touching the pipeline.
|
|
25
|
+
|
|
26
|
+
| Invocation | Command to run |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `/gate status` | `enigma config gate` (prints the value in force here) |
|
|
29
|
+
| `/gate off` | `enigma config gate off -l` |
|
|
30
|
+
| `/gate on` | `enigma config gate on -l` |
|
|
31
|
+
| `/gate off --global` (or "everywhere", "in every project") | `enigma config gate off -g` |
|
|
32
|
+
| `/gate on --global` | `enigma config gate on -g` |
|
|
33
|
+
|
|
34
|
+
Scope matters, so be explicit about it. Plain `/gate off` switches the gate off
|
|
35
|
+
for **this project only** by writing `gate: false` into its `.enigma.json` -
|
|
36
|
+
narrow and reversible, and it leaves every other repo alone. Only go global when
|
|
37
|
+
the user actually says so. Report which of the two you applied.
|
|
38
|
+
|
|
39
|
+
Toggling rewrites the agent memory file, which is read at startup: tell the user
|
|
40
|
+
the change takes effect in their **next** session. In this one, keep honouring
|
|
41
|
+
what they just asked for - if they turned the gate off, do not drive it again.
|
|
42
|
+
|
|
43
|
+
One asymmetry worth stating when it applies: a **global** off also removes this
|
|
44
|
+
command from the agent, so tell the user that turning it back on is
|
|
45
|
+
`enigma config gate on -g` from a terminal. A project-scoped off leaves `/gate`
|
|
46
|
+
in place, so `/gate on` works there.
|
|
47
|
+
|
|
21
48
|
## Two ways to invoke
|
|
22
49
|
|
|
23
50
|
- **Validate-only** - bare `/gate` (optionally with flag-style requests like
|
|
@@ -27,15 +54,19 @@ for example "skip the lint step" becomes `--skip=lint`. Run
|
|
|
27
54
|
carry out the task yourself, then validate:
|
|
28
55
|
1. **Check scope.** Inspect `git status` before changing or committing anything.
|
|
29
56
|
Preserve unrelated uncommitted changes; commit only what belongs to the task.
|
|
30
|
-
2. **Do the work**, then **commit it
|
|
31
|
-
the
|
|
32
|
-
|
|
57
|
+
2. **Do the work**, then **commit it**. The gate validates committed history on
|
|
58
|
+
whatever branch the user is on - never switch or create a branch just to run
|
|
59
|
+
it.
|
|
33
60
|
3. **Then validate**, passing the user's task as your `--intent` (the goal in
|
|
34
61
|
their words), enriched with the decisions and tradeoffs you made.
|
|
35
62
|
|
|
36
63
|
## Before you start
|
|
37
64
|
|
|
38
|
-
- The work must be **committed
|
|
65
|
+
- The work must be **committed**. Any branch qualifies, the default branch
|
|
66
|
+
included - there the pipeline opens no PR and its push lands directly on that
|
|
67
|
+
branch, which is the intended behavior. The only exception is a branch the user
|
|
68
|
+
listed in `gate-protected-branches`: `axi run` refuses it, and the answer is to
|
|
69
|
+
tell the user, not to move their work to another branch.
|
|
39
70
|
- The repository must be initialized with `enigma gate init`.
|
|
40
71
|
|
|
41
72
|
If any precondition fails, `axi run` returns an `error:` with the exact fix -
|
|
@@ -92,7 +123,9 @@ uses `--intent` to tell a deliberate choice from a mistake.
|
|
|
92
123
|
driving. Tell the user the PR is ready and ask them to review and merge it
|
|
93
124
|
(link in the `help` line). The gate keeps monitoring the PR in the
|
|
94
125
|
background; do not poll for the merge.
|
|
95
|
-
- `passed` - cleared the gate and the PR was merged or closed.
|
|
126
|
+
- `passed` - cleared the gate and the PR was merged or closed. On the default
|
|
127
|
+
branch this is the normal ending: no PR is opened and CI monitoring is
|
|
128
|
+
skipped, so the run finishes here once the push lands.
|
|
96
129
|
- `failed` / `cancelled` - read the output, fix what it points at, commit on
|
|
97
130
|
the same branch, and drive again (`axi run` for a fresh run, or
|
|
98
131
|
`enigma gate rerun`). Do not leave the user at a failed outcome without
|
package/assets/memory/AGENTS.md
CHANGED
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
Non-negotiable, language-agnostic defaults - apply them by default without being asked, using the stack's idiomatic tool. They restate the cores of validation-policy, backend-policy and frontend-policy so they hold even when a skill does not load.
|
|
38
38
|
|
|
39
39
|
- Validate EVERY external input (request body, query, params, event payload, form field, CLI arg, webhook/message) against an explicit schema before use - Zod (TS/JS), Pydantic (Python), the language's equivalent elsewhere. Never consume an unvalidated shape or leave it open-ended. When the input is a tagged/event union, validate the discriminant AND that specific variant's body, with the expected fields typed.
|
|
40
|
-
-
|
|
40
|
+
- Normalize before validating, on the client AND the server, from one shared normalizer: trim every string, lowercase the email, capitalize each word of a person's name, canonicalize a link or handle to one stored form. A check that cannot fail is not validation - never patch the value into validity and then check the patched value.
|
|
41
|
+
- Frontend forms: validate in real time against the same schema, on EVERY field that has a rule and not only the ones with a famous format, and use optimistic UI with rollback on failure for user-facing mutations.
|
|
41
42
|
- Cache reads on the client (localStorage/sessionStorage, or the data layer's cache) with a short TTL (~30s or more) to avoid redundant queries and survive rate limits; invalidate on write.
|
|
42
43
|
- Build reusable, composable components instead of duplicating UI - e.g. a single Input that renders a show/hide toggle when the type is password. Reuse before writing new.
|
|
43
44
|
- Never use the browser's native `alert`/`confirm`/`prompt` - use a dialog/modal component that matches the page design.
|
|
@@ -83,8 +84,9 @@ Non-negotiable, language-agnostic defaults - apply them by default without being
|
|
|
83
84
|
<!-- enigma:gate:start -->
|
|
84
85
|
### AI Quality Gate (Automatic)
|
|
85
86
|
|
|
86
|
-
- The AI quality gate is active. When you finish a code task
|
|
87
|
-
-
|
|
87
|
+
- The AI quality gate is active. When you finish a code task and the changes are committed, drive the gate yourself before reporting the work done - on WHATEVER branch the work is on, the default branch included. The user should not have to ask for it or run any setup command.
|
|
88
|
+
- Skip automatic driving only when: the user told you to skip it, the repo's `.enigma.json` sets `gate: false`, there is nothing committed to validate, or `axi run` refuses the branch as protected (`gate-protected-branches`) - then say so and leave the work unvalidated rather than switching branches on your own. To stop it for good the user runs `/gate off` (this project) or `enigma config gate off -g` (everywhere); offer that instead of quietly not running it.
|
|
89
|
+
- On the default branch the pipeline opens no PR and its push lands directly on that branch. That is the intended behavior there; report it as the outcome instead of waiting for a PR link.
|
|
88
90
|
- If the repo is not initialized yet (`enigma gate axi` says so), run `enigma gate init` once yourself, then proceed. If `enigma gate` itself misbehaves, `enigma gate doctor` reports why.
|
|
89
91
|
- Drive it through the `/gate` workflow (`enigma gate axi run --intent "<what the user set out to accomplish>"`): authorize `auto-fix` and `no-op` findings on your own judgment, but STOP and escalate every `ask-user` finding verbatim. Do NOT pass `--yes` and do NOT merge the PR yourself - on `checks-passed`, leave the PR ready and ask the user to review and merge it.
|
|
90
92
|
- While a run is active never edit code to fix a finding; the pipeline owns the fixes (respond with `--action fix`). The full command reference lives in the gate skill / `/gate` command.
|
package/assets/memory/CLAUDE.md
CHANGED
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
Non-negotiable, language-agnostic defaults - apply them by default without being asked, using the stack's idiomatic tool. They restate the cores of validation-policy, backend-policy and frontend-policy so they hold even when a skill does not load.
|
|
38
38
|
|
|
39
39
|
- Validate EVERY external input (request body, query, params, event payload, form field, CLI arg, webhook/message) against an explicit schema before use - Zod (TS/JS), Pydantic (Python), the language's equivalent elsewhere. Never consume an unvalidated shape or leave it open-ended. When the input is a tagged/event union, validate the discriminant AND that specific variant's body, with the expected fields typed.
|
|
40
|
-
-
|
|
40
|
+
- Normalize before validating, on the client AND the server, from one shared normalizer: trim every string, lowercase the email, capitalize each word of a person's name, canonicalize a link or handle to one stored form. A check that cannot fail is not validation - never patch the value into validity and then check the patched value.
|
|
41
|
+
- Frontend forms: validate in real time against the same schema, on EVERY field that has a rule and not only the ones with a famous format, and use optimistic UI with rollback on failure for user-facing mutations.
|
|
41
42
|
- Cache reads on the client (localStorage/sessionStorage, or the data layer's cache) with a short TTL (~30s or more) to avoid redundant queries and survive rate limits; invalidate on write.
|
|
42
43
|
- Build reusable, composable components instead of duplicating UI - e.g. a single Input that renders a show/hide toggle when the type is password. Reuse before writing new.
|
|
43
44
|
- Never use the browser's native `alert`/`confirm`/`prompt` - use a dialog/modal component that matches the page design.
|
|
@@ -83,8 +84,9 @@ Non-negotiable, language-agnostic defaults - apply them by default without being
|
|
|
83
84
|
<!-- enigma:gate:start -->
|
|
84
85
|
### AI Quality Gate (Automatic)
|
|
85
86
|
|
|
86
|
-
- The AI quality gate is active. When you finish a code task
|
|
87
|
-
-
|
|
87
|
+
- The AI quality gate is active. When you finish a code task and the changes are committed, drive the gate yourself before reporting the work done - on WHATEVER branch the work is on, the default branch included. The user should not have to ask for it or run any setup command.
|
|
88
|
+
- Skip automatic driving only when: the user told you to skip it, the repo's `.enigma.json` sets `gate: false`, there is nothing committed to validate, or `axi run` refuses the branch as protected (`gate-protected-branches`) - then say so and leave the work unvalidated rather than switching branches on your own. To stop it for good the user runs `/gate off` (this project) or `enigma config gate off -g` (everywhere); offer that instead of quietly not running it.
|
|
89
|
+
- On the default branch the pipeline opens no PR and its push lands directly on that branch. That is the intended behavior there; report it as the outcome instead of waiting for a PR link.
|
|
88
90
|
- If the repo is not initialized yet (`enigma gate axi` says so), run `enigma gate init` once yourself, then proceed. If `enigma gate` itself misbehaves, `enigma gate doctor` reports why.
|
|
89
91
|
- Drive it through the `/gate` workflow (`enigma gate axi run --intent "<what the user set out to accomplish>"`): authorize `auto-fix` and `no-op` findings on your own judgment, but STOP and escalate every `ask-user` finding verbatim. Do NOT pass `--yes` and do NOT merge the PR yourself - on `checks-passed`, leave the PR ready and ask the user to review and merge it.
|
|
90
92
|
- While a run is active never edit code to fix a finding; the pipeline owns the fixes (respond with `--action fix`). The full command reference lives in the gate skill / `/gate` command.
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "On-demand over-engineering review - diff review, whole-repo audit, and enigma: debt-marker ledger (tags delete/stdlib/native/yagni/shrink, line/dep scoring); lists cuts, applies nothing.",
|
|
6
6
|
"updated": "2026-06-16T11:24:30+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "f742a2be3f328b9ea1ff9a35a449177c2cbec35ad16e46f7054b7a873a2ab017"
|
|
9
9
|
}
|
|
@@ -52,6 +52,8 @@ Cache expensive or hot reads on the server to reduce database load, complementin
|
|
|
52
52
|
### When to cache
|
|
53
53
|
|
|
54
54
|
- Cache read-heavy, expensive-to-compute, or frequently requested data.
|
|
55
|
+
- Reach for the client cache first (frontend-policy): it removes the request instead of serving it faster, so it costs this service nothing. A server cache is for what the client cannot hold - an expensive computation shared across users, a payload too large or too sensitive to sit on a device, or a rate-limited upstream you are shielding.
|
|
56
|
+
- Support the client's revalidation instead of making it re-download: answer with an `ETag`/`Last-Modified` and honour `If-None-Match`/`If-Modified-Since` with a `304`, so an unchanged resource costs a header exchange and no body.
|
|
55
57
|
- Do not cache data that must always be strongly consistent unless invalidation is immediate and reliable.
|
|
56
58
|
- Never cache secrets or sensitive data without encryption and strict access control.
|
|
57
59
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backend-policy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Backend/API architecture: controller-service-repository layering, API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
|
|
6
|
-
"updated": "2026-07-
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
6
|
+
"updated": "2026-07-31T16:34:48+02:00",
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
|
+
"sha": "768c2e088948ff97d6b897ce43854e53dfd156738e6a19548679b5327278433d"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Ciphera code style conventions (formatting, naming, imports incl. namespace imports for wide module surfaces, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
|
|
6
6
|
"updated": "2026-07-30T15:48:04+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "05dc812da459071110d96ee41c5201b1ee230471d635352415e10ef8b220c267"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "3d3bbe0602d5bbb4afe37648fe3c2fa39376b1bcbac5d8c441f01fad1e866ed0"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Core engineering execution policy and harness orchestration (highest-authority rules).",
|
|
6
6
|
"updated": "2026-07-30T19:29:19+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "6a881d8589926fa7f48058314fd26d7042fd2ac82f1c87a6c11ffb54d1fda22b"
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: database-expert
|
|
3
|
-
description: Senior database architecture - schema design, normalization and anti-duplication, query/index optimization, scalability (partitioning, sharding, replication), and RGPD/GDPR encryption of sensitive data. Use when designing, modifying, migrating, querying, or reviewing any database, schema, SQL, ORM model, or persistence layer.
|
|
3
|
+
description: Senior database architecture - engine selection (PostgreSQL is the default relational engine for anything deployed or multi-writer; SQLite only for local-first, embedded, single-writer stores), schema design, normalization and anti-duplication, query/index optimization, scalability (partitioning, sharding, replication), and RGPD/GDPR encryption of sensitive data. Use when designing, modifying, migrating, querying, or reviewing any database, schema, SQL, ORM model, or persistence layer, and when choosing the datastore for a new project's stack.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Database Expert Policy (Senior Data Architecture Standards)
|
|
@@ -26,6 +26,18 @@ description: Senior database architecture - schema design, normalization and ant
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## Engine Selection (Default: PostgreSQL)
|
|
30
|
+
|
|
31
|
+
- For anything that will be deployed, grow, or be written to by more than one process, the default relational engine is **PostgreSQL**. Pick it without being asked; when something else is chosen, say in one line what constraint forced it.
|
|
32
|
+
- What makes it the default and not a preference: real write concurrency (MVCC, no database-wide writer lock), the types the rest of this policy assumes (native `uuid`, `jsonb`, arrays, enums, ranges, `timestamptz`), partial, expression and GIN indexes, generated columns, materialized views, declarative partitioning, logical replication and read replicas, and extensions that each remove a service from the stack (`pgvector` for embeddings, `pg_trgm` for fuzzy search, PostGIS for geo, `pg_cron` for schedules).
|
|
33
|
+
- SQLite is one file with one writer. It is the right default for a local-first or embedded store - a CLI's own state, a desktop or mobile app, an agent's local cache or index, a test fixture, an offline replica - and the wrong one for a web or API backend, anything running more than one instance, anything a background worker writes to, or anything with a managed-hosting story. Starting there and growing out of it is a migration with downtime, not a config change.
|
|
34
|
+
- MySQL/MariaDB only when the platform, the host or the team requires it. SQL Server or Oracle only where it is already the environment.
|
|
35
|
+
- Do not add a second datastore before PostgreSQL runs out. It handles queues (`SELECT ... FOR UPDATE SKIP LOCKED`), full-text search, vectors, JSON documents and counters well past early scale. Add Redis, a search engine or a vector database when a measured limit demands it, not as part of the initial stack.
|
|
36
|
+
- Serverless and edge runtimes still get PostgreSQL: the problem there is connection count, not the engine, so put a pooler in front (PgBouncer, Prisma Accelerate, the provider's pooled endpoint) instead of switching to a file database.
|
|
37
|
+
- Wire it the same way every time: Prisma as the ORM for TypeScript, migrations committed to version control, the connection string from the environment and never in the repo, and pooling configured before the first load test rather than after the first outage.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
29
41
|
## Identifier Policy (Non-Negotiable)
|
|
30
42
|
|
|
31
43
|
- Every primary key and every externally exposed identifier MUST be a UUID. NEVER use auto-increment, SERIAL, BIGSERIAL, IDENTITY, AUTO_INCREMENT, or any incrementing integer/sequence as an entity ID - no exceptions, in any datastore.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "database-expert",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
|
-
"description": "Senior database architecture policy: query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
|
|
5
|
+
"description": "Senior database architecture policy: engine selection (PostgreSQL by default, SQLite only for local-first/embedded stores), query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
|
|
6
6
|
"updated": "2026-06-03T14:19:50+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
|
+
"sha": "b21ae85a015305106f787be2eb0a022661b2e984bc42c6bc66703954256e3c53"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "14b0064c8b33a0dc85e51464b05005cf5801c756b1101789a6924b9548420f6b"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Transactional email: React Email templates instead of hand-written HTML tables, server-side rendering, one send module behind the provider SDK, plain-text alternatives, idempotent background sending, link safety, and deliverability (SPF/DKIM/DMARC, bounce suppression, unsubscribe).",
|
|
6
6
|
"updated": "2026-07-30T19:29:19+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "c9724fdbcdbeab99573be3fd44d4cdd97c2a394d99f3c4395f118f17356b00ed"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one.",
|
|
6
6
|
"updated": "2026-07-29T01:18:36+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontend-policy
|
|
3
|
-
description: Frontend architecture - reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals the current state - form saves, toggles, filters, reorders - not just saves; dirty means the values DIFFER from the loaded snapshot, not that the user touched the field, so a value edited and put back leaves Save disabled), client-side caching (localStorage/sessionStorage to avoid redundant server calls and survive rate limits), instant first paint (render the shell immediately, load data async via the API, show skeletons - never block render on data), perceived performance and responsiveness (instant interaction feedback, prefetch on intent, debounce/throttle, cancel stale requests, avoid request waterfalls, lazy-load heavy widgets), large-list rendering (virtualized infinite scroll vs pagination, skeletons, progressive/parallel loading, short-TTL caching), optimistic UI with rollback, visual restraint (never a card inside a card, borders only where they carry information, spacing and background tone before chrome), icon actions (repeated row/card actions like copy, edit, rename, remove, download, refresh are icon-only buttons carrying aria-label plus title, never a text label), responsive/adaptive layout (fluid units, breakpoints, no overlap or horizontal overflow, viewport meta, touch targets), AI chat/assistant/agent interfaces (use Vercel's AI Elements registry for message threads, streaming, reasoning and tool-call panels, prompt inputs - never hand-roll chat UI in React), and periodic React code-health audits (react-doctor). Use when building or changing UI components, client state, forms/save flows, data fetching/caching, lists that show lots of data, loading states, dashboards/panels, layout/responsiveness, making the UI feel fast, building a chat/AI/agent/LLM interface, or any frontend structure.
|
|
3
|
+
description: Frontend architecture - reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals the current state - form saves, toggles, filters, reorders - not just saves; dirty means the values DIFFER from the loaded snapshot, not that the user touched the field, so a value edited and put back leaves Save disabled), client-side caching (localStorage/sessionStorage to avoid redundant server calls and survive rate limits), instant first paint (render the shell immediately, load data async via the API, show skeletons - never block render on data), perceived performance and responsiveness (instant interaction feedback, prefetch on intent, debounce/throttle, cancel stale requests, avoid request waterfalls, lazy-load heavy widgets), large-list rendering (virtualized infinite scroll vs pagination, skeletons, progressive/parallel loading, short-TTL caching), optimistic UI with rollback, visual restraint (never a card inside a card, borders only where they carry information, spacing and background tone before chrome), icon actions (repeated row/card actions like copy, edit, rename, remove, download, refresh are icon-only buttons carrying aria-label plus title, never a text label), responsive/adaptive layout (fluid units, breakpoints, no overlap or horizontal overflow, viewport meta, touch targets), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode/spellcheck per field kind, set once in the shared Input, normalized on blur rather than on every keystroke, with an inline error on every field that has a rule), AI chat/assistant/agent interfaces (use Vercel's AI Elements registry for message threads, streaming, reasoning and tool-call panels, prompt inputs - never hand-roll chat UI in React), and periodic React code-health audits (react-doctor). Use when building or changing UI components, client state, forms/save flows, data fetching/caching, lists that show lots of data, loading states, dashboards/panels, layout/responsiveness, making the UI feel fast, building a chat/AI/agent/LLM interface, or any frontend structure.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Frontend Architecture Policy
|
|
@@ -134,6 +134,16 @@ Navigation and any other persistent chrome must stay where the user left it whil
|
|
|
134
134
|
- The same rule covers a sticky header, toolbar, filter rail or side panel: pinned, bounded, and independently scrollable. Keep pinned chrome shallow - it eats vertical space on small screens, so collapse it into a drawer or a top bar there rather than pinning it over half the viewport.
|
|
135
135
|
- Verify by scrolling to the BOTTOM of a long page and confirming the sidebar is still on screen with its last entry reachable. Mechanically: the sidebar's `getBoundingClientRect().top` stays at its offset as the page scrolls, and `scrollHeight <= clientHeight` holds for it, or it can scroll itself.
|
|
136
136
|
|
|
137
|
+
### On a phone the sidebar takes the whole screen
|
|
138
|
+
|
|
139
|
+
A sidebar keeps its desktop width only while there is a desktop to put it in. On a phone it becomes a full-screen surface, unless the design or the user says otherwise. Apply this by default to any off-canvas chrome: the nav sidebar, a filter rail, a details drawer, a settings panel.
|
|
140
|
+
|
|
141
|
+
- Full width AND full height: `w-full` (`100%`, `100vw`) with the desktop width added at a breakpoint (`w-full md:w-80`) or capped by `max-w-*`. Prefer `100dvh` over `100vh` for the height so the mobile browser's collapsing toolbar does not cut the panel off.
|
|
142
|
+
- A fixed width on a phone is the defect either way it lands: 320px on a 360px screen leaves a useless sliver of dead content, and anything wider than the viewport is simply cut off.
|
|
143
|
+
- The stacked variant counts as full width: when the layout collapses to one column and the sidebar returns to normal flow, it already spans the screen. Nothing more to do there.
|
|
144
|
+
- Being full screen makes it modal, so treat it as one: a visible close control, dismissal by backdrop tap and Escape, focus moved into the panel and trapped while it is open, focus restored to the trigger on close, and the page behind it locked from scrolling.
|
|
145
|
+
- The exceptions are real but explicit: a design that deliberately keeps a peek of the content behind, and a panel that is hidden on phones entirely because a different component serves that size. Say so in the code rather than leaving it to be read as an oversight.
|
|
146
|
+
|
|
137
147
|
---
|
|
138
148
|
|
|
139
149
|
## Links In Copy Are Links
|
|
@@ -222,6 +232,42 @@ When the user edits a value that must be unique within a set the client already
|
|
|
222
232
|
|
|
223
233
|
---
|
|
224
234
|
|
|
235
|
+
## Every Field Declares Its Keyboard And Its Casing
|
|
236
|
+
|
|
237
|
+
A text input is not generic. What it holds decides the keyboard a phone opens, whether the first letter arrives capitalized, and whether autocorrect rewrites it. Left unset, the mobile defaults produce "juan perez" in a name field and a spell-checked handle in the next one.
|
|
238
|
+
|
|
239
|
+
- Person name (full name, first, last): `autocapitalize="words"`, the matching `autocomplete` token (`name`, `given-name`, `family-name`), `spellcheck="false"`, `autocorrect="off"`. Phone keyboards capitalize SENTENCES by default, which capitalizes only the first word of the name.
|
|
240
|
+
- Email: `type="email"`, `autocomplete="email"`, `inputmode="email"`, `autocapitalize="none"`, `spellcheck="false"`.
|
|
241
|
+
- Username, handle, slug, coupon, licence key: `autocapitalize="none"`, `autocorrect="off"`, `spellcheck="false"`.
|
|
242
|
+
- URL or profile link: `inputmode="url"`, `autocapitalize="none"`, `autocorrect="off"`.
|
|
243
|
+
- Phone: `type="tel"`, `inputmode="tel"`, `autocomplete="tel"`. Numeric code: `inputmode="numeric"`, `autocomplete="one-time-code"`.
|
|
244
|
+
- Free prose (bio, message, description): `autocapitalize="sentences"` with spellcheck left on.
|
|
245
|
+
- Set `enterkeyhint` where the key does something specific (`search`, `send`, `done`), and mark the search field `type="search"`.
|
|
246
|
+
|
|
247
|
+
Where this lives and when it runs:
|
|
248
|
+
|
|
249
|
+
- Put the whole matrix in the shared Input/TextField component, chosen by one prop (`kind="name" | "email" | "handle" | ...`), so a new form gets it by construction. A form that spells out attributes per field will forget them; that is how the defect ships.
|
|
250
|
+
- The attribute only shapes what the keyboard offers. Normalize the value as well - trim, collapse inner spaces, capitalize each word of a name, lowercase an email - using the shared normalizer from validation-policy, and let the server apply the same one.
|
|
251
|
+
- Normalize on BLUR and before save, never on every keystroke: rewriting the value under the caret moves the cursor and breaks IME composition mid-word. While the field has focus, show what the user typed; on blur, show what will be stored.
|
|
252
|
+
- Validate the normalized value, so a trailing space the user has not finished typing never renders as an error.
|
|
253
|
+
- Show the error inline as the user types, from the first blur onward, on EVERY field that has a rule, and keep Save blocked while any field is invalid. Validating the email and leaving the link, the phone and the handle next to it silent is the usual half-built form.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Sign-In, Sign-Up and Recovery Screens
|
|
258
|
+
|
|
259
|
+
Auth is the first screen a user meets and the one most often shipped half-built. Treat the four screens as one flow: sign in, sign up, forgot password, set a new password. The server-side rules (token lifetime, rate limits, what an answer may reveal) are owned by security-policy; what follows is the UI half.
|
|
260
|
+
|
|
261
|
+
- Every sign-in form with a password field carries a visible "Forgot your password?" link next to that field, leading to a real reset flow. Building the login screen without it is shipping a dead end.
|
|
262
|
+
- The reset request screen confirms in the same words whether or not the address is registered ("If that address has an account, we have sent a link"). Never render "no account with that email" - the screen would be an account-existence oracle.
|
|
263
|
+
- The new-password screen validates in real time against the same schema the server uses, uses the shared Input (which brings the show/hide toggle), and compares the confirmation field as the user types. Keep Submit disabled until both are valid, with the reason visible.
|
|
264
|
+
- After sign-up the user lands inside the app, already signed in. If the account still needs email verification, say so in the app with a way to resend, and block only the actions that need it.
|
|
265
|
+
- Surface throttling honestly. On a `429`, show how long the wait is (from `Retry-After`), keep the button disabled with a countdown, and never swallow the response into a generic "something went wrong".
|
|
266
|
+
- A one-time-code field is one input with `autocomplete="one-time-code"`, `inputmode="numeric"`, paste of the whole code, and no clearing of what the user typed on a wrong attempt. Say how many attempts are left only if the server chose to reveal it.
|
|
267
|
+
- Never keep a password, token, or code in `localStorage`, a query string, or an analytics payload. A reset token in the URL stays out of logs and out of any third-party script on the page.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
225
271
|
## Client-Side Caching (Reduce Server Load)
|
|
226
272
|
|
|
227
273
|
Cache on the client to avoid redundant server round-trips and to keep the app usable under rate limits. The goal is to reach the backend (and therefore Redis/DB) as rarely as correctness allows.
|
|
@@ -254,6 +300,26 @@ Cache on the client to avoid redundant server round-trips and to keep the app us
|
|
|
254
300
|
- Prefer stale-while-revalidate for non-critical data: serve cached, refresh in the background.
|
|
255
301
|
- Never serve stale data for security-, money-, or correctness-critical reads.
|
|
256
302
|
|
|
303
|
+
### Show the last snapshot, then patch only what changed
|
|
304
|
+
|
|
305
|
+
The strongest version of stale-while-revalidate: persist the last response and render it on entry, so a returning user sees the screen already populated while the real request is still in flight. It is what makes a view backed by something slow (a NAS, a device on the LAN, a third-party API, anything you do not control) feel loaded instantly, because between two visits almost nothing usually changed.
|
|
306
|
+
|
|
307
|
+
- On mount, render from the persisted snapshot and fire the request at the same time. No spinner over content you can already show; a small "refreshing" marker is enough. Skeletons are for the first ever load, when there is no snapshot.
|
|
308
|
+
- When the response lands, RECONCILE, do not replace. Diff against what is on screen and apply only the differences: rows added, rows removed, fields whose value actually changed. Wholesale replacement is what produces the flash, the scroll jump, and the lost selection, and it is exactly what the snapshot was meant to avoid.
|
|
309
|
+
- Keep identity stable. Key rows by their real id (never the array index), and reuse the existing object for an unchanged row instead of a fresh one, so the framework re-renders the rows that changed and nothing else. Preserve scroll position, selection, expanded rows, in-progress edits, and focus across the refresh.
|
|
310
|
+
- If the whole response is equal to the snapshot, do nothing at all: no state write, no re-render. This is No-Op Detection applied to reads, and it is the common case.
|
|
311
|
+
- Detect "nothing changed" as cheaply as the backend allows: an `ETag` with `If-None-Match` (a `304` costs you a header exchange and no body), a `Last-Modified`/`updatedAt` cursor, or a content hash of the payload. Falling back to comparing the parsed objects is fine for small payloads, and hashing is fine for large ones - just do not deep-compare a huge tree on every poll.
|
|
312
|
+
- Stamp the snapshot with the time it was taken and show it ("updated 2 minutes ago"). A stale number no one can date is worse than a spinner.
|
|
313
|
+
- Never let the snapshot outlive its usefulness: version the stored shape (drop it when the app's schema changes), give it a TTL, cap what you store, and clear it on sign-out. localStorage is small, synchronous, and shared with every script on the page.
|
|
314
|
+
- Sensitive or fast-moving data does not get this treatment: money, permissions, live status, anything that would mislead if it were a minute old. Showing an old value is a correctness decision, not just a UX one.
|
|
315
|
+
|
|
316
|
+
### Caching is not free
|
|
317
|
+
|
|
318
|
+
- Every cache is a second copy of the truth, and the cost is invalidation, staleness bugs, and the memory or storage it occupies. Add one when there is a measured round-trip to save, not by default.
|
|
319
|
+
- Prefer the client cache: it removes the request entirely, so it costs the server nothing and scales with the number of users rather than against it. A server cache (Redis, see backend-policy) is for what the client cannot hold - expensive shared computations, data too large or too sensitive to sit on a device - and it is one more thing to size, evict, and invalidate.
|
|
320
|
+
- Cache the response, not the render. Storing derived UI state means re-deriving it on every schema change and getting it wrong when the derivation does.
|
|
321
|
+
- Coalesce and throttle rather than cache harder: one in-flight request per resource, no refetch on every focus event, and no polling loop that runs while the tab is hidden.
|
|
322
|
+
|
|
257
323
|
---
|
|
258
324
|
|
|
259
325
|
## Instant First Paint (Shell First, Data Async)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontend-policy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
|
-
"description": "Frontend architecture: reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals current state, not just form saves; dirty means different from the loaded snapshot, not touched), instant first paint (render the shell, load data async, skeletons), perceived performance (prefetch on intent, debounce/throttle, cancel stale requests, avoid waterfalls, lazy widgets), large-list rendering (infinite scroll/pagination, virtualization, skeletons, progressive loading), optimistic UI with rollback, visual restraint (one card level, spacing before borders, one elevation scale), icon actions (repeated row/card actions are icon-only buttons with aria-label plus title, not text labels), responsive/adaptive layout (fluid units, breakpoints, no overlap/overflow, viewport meta, touch targets), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
|
|
6
|
-
"updated": "2026-07-
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
5
|
+
"description": "Frontend architecture: reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals current state, not just form saves; dirty means different from the loaded snapshot, not touched), instant first paint (render the shell, load data async, skeletons), perceived performance (prefetch on intent, debounce/throttle, cancel stale requests, avoid waterfalls, lazy widgets), large-list rendering (infinite scroll/pagination, virtualization, skeletons, progressive loading), optimistic UI with rollback, visual restraint (one card level, spacing before borders, one elevation scale), icon actions (repeated row/card actions are icon-only buttons with aria-label plus title, not text labels), responsive/adaptive layout (fluid units, breakpoints, no overlap/overflow, viewport meta, touch targets), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode per field kind, set once in the shared Input, normalized on blur), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
|
|
6
|
+
"updated": "2026-07-31T16:34:48+02:00",
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
|
+
"sha": "79816cdc63a7fd64336b93296aada3520bab26b38a7f65600f5ca27745de3a65"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Git & contribution policy (senior engineering standards).",
|
|
6
6
|
"updated": "2026-07-16T22:44:02+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "e6dfbc33884000d9d25841bd9c5a84d6558ffd374882cb7b34451eb2cebc2161"
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: security-policy
|
|
3
|
-
description: Application and AI-agent security - secrets management, authentication and authorization (least privilege), OWASP Top 10 mitigations, transport and crypto baseline, secure logging, and agent/MCP/tool-use safety (prompt injection, untrusted tool output, permission boundaries). Use when handling secrets, auth, permissions, untrusted data or tool output, or any security-sensitive code, config, or infrastructure.
|
|
3
|
+
description: Application and AI-agent security - secrets management, authentication and authorization (least privilege), credential flows (sign-in, sign-up that establishes the session, password reset, 2FA, and rate limiting per IP and per account), OWASP Top 10 mitigations, transport and crypto baseline, secure logging, and agent/MCP/tool-use safety (prompt injection, untrusted tool output, permission boundaries). Use when handling secrets, auth, login or registration screens, permissions, untrusted data or tool output, or any security-sensitive code, config, or infrastructure.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Security Policy
|
|
@@ -37,6 +37,42 @@ description: Application and AI-agent security - secrets management, authenticat
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
+
## Credential Flows (Sign-In, Sign-Up, Reset, 2FA)
|
|
41
|
+
|
|
42
|
+
These four screens are one system: an attacker who cannot guess a password will try to register, reset, or brute-force a second factor instead. Build them together.
|
|
43
|
+
|
|
44
|
+
### Sign-up establishes the session
|
|
45
|
+
|
|
46
|
+
- A successful registration already proves the credentials: create the session there and land the user in the app. Never redirect them to the sign-in form to retype what they just typed.
|
|
47
|
+
- Keep email verification asynchronous. Let them in unverified, ask for confirmation, and gate only the actions that genuinely need a verified address (billing, invites, outbound mail).
|
|
48
|
+
- The exception is an account created by someone else (admin provisioning, an approval queue). Then say so in the code, because the redirect looks like the defect.
|
|
49
|
+
|
|
50
|
+
### Password recovery is part of every password login
|
|
51
|
+
|
|
52
|
+
- Every sign-in form with a password field needs a visible recovery entry point. A login screen with no way out of a forgotten password is an incomplete flow, not a simpler one.
|
|
53
|
+
- The request step answers identically whether or not the account exists, and takes the same time. "If that address has an account, we sent a link" is the whole response; never confirm or deny.
|
|
54
|
+
- The token is high-entropy, single-use, stored hashed, and expires in 15 to 60 minutes. Issuing a new one invalidates the previous one.
|
|
55
|
+
- On a successful reset: consume the token, rotate the session, and invalidate every other active session and refresh token for that account. Notify the account by email that the password changed.
|
|
56
|
+
- Never send the new password by email, and never embed credentials in the link. The link proves control of the address, nothing more.
|
|
57
|
+
|
|
58
|
+
### Rate-limit by IP AND by account
|
|
59
|
+
|
|
60
|
+
- Limit both dimensions on every credential endpoint: sign-in, sign-up, password reset request, reset confirmation, email verification, 2FA/OTP verification, and any "does this identifier exist" helper the sign-in page calls.
|
|
61
|
+
- By IP stops the broad sweep. By account or identifier stops the slow distributed attack that spreads one attempt per IP across a botnet, which the IP limit never sees. One without the other is not rate limiting.
|
|
62
|
+
- Count FAILURES, not requests, so a person typing their password wrong twice is not treated like an attack while a scripted run is stopped early.
|
|
63
|
+
- Back off exponentially and answer `429` with `Retry-After`. Keep every counter server-side; a client-held attempt count is decoration.
|
|
64
|
+
- Cap second factors hard: a handful of attempts per code, then invalidate the code and require a new one. OTP codes are single-use with a short TTL, and backup codes are single-use and stored hashed.
|
|
65
|
+
- After a threshold of failures, lock the account temporarily and tell the owner by email. An unbounded lock is a denial-of-service someone else can trigger, so prefer a timed lock with a clear unlock path.
|
|
66
|
+
- Derive the client IP from the trusted proxy chain, never from a raw client-supplied header. `X-Forwarded-For` is attacker-controlled unless your edge rewrites it.
|
|
67
|
+
- The limiter must not become an oracle: an unknown account and a known one get the same response shape, status, and timing. Always run the password hash, even when the user does not exist, so the timing does not answer the question the error message refused to.
|
|
68
|
+
- Registration and any endpoint that sends mail also carry a cost and abuse surface. Limit them per IP, per address, and globally.
|
|
69
|
+
|
|
70
|
+
### Uniform answers
|
|
71
|
+
|
|
72
|
+
- Wrong password, unknown account, locked account, and unverified account all return the same generic failure to the client. The specifics belong in the server log, not the response.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
40
76
|
## OWASP Top 10 Baseline
|
|
41
77
|
|
|
42
78
|
- Injection (SQL/NoSQL/command/LDAP): use parameterized queries and safe APIs; never build queries or shell commands by string concatenation (query specifics in database-expert).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "security-policy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Application and AI-agent security: secrets, authn/authz (least privilege), OWASP Top 10, transport/crypto baseline, secure logging, and agent/MCP/tool-use safety.",
|
|
6
|
-
"updated": "2026-
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
6
|
+
"updated": "2026-07-31T16:26:45+02:00",
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
|
+
"sha": "e5e7ac2791919e4b8483115ee8b83800c5ebdf4eebde019650d974b400417c55"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Create new skills, modify and improve existing skills, and measure skill performance with evals and benchmarks.",
|
|
6
6
|
"updated": "2026-07-29T01:18:36+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "13d24c217bdb6fe83fe16835d8f5c3d397a9f3338f16876c61ef96e97f34c90a"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Exhaustive completion discipline for long/multi-item tasks - inventory, coverage ledger, verified done.",
|
|
6
6
|
"updated": "2026-07-30T20:10:24+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "2f82288d83dca3676d6bc49955c89477cc298f77fa56b4e414cd95ff858fa261"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Concise, realistic technical copy - UI microcopy, descriptions, hints, empty/error states, and README/doc prose that informs without over-explaining or restating the obvious, and never uses a typographic dash.",
|
|
6
6
|
"updated": "2026-07-28T20:30:23+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "a4b792103eb1f9dad93b9d70ea79dc18fe9cbbc318facf5adb47ae5907d842f9"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Test strategy, coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (layout by type/domain, mirrored paths, file naming, fixture/helper placement).",
|
|
6
6
|
"updated": "2026-06-16T17:11:49+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
8
|
"sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: validation-policy
|
|
3
|
-
description: Strict frontend + backend schema validation (Zod or equivalent), schema consistency between client and server, and safe client-facing error handling. Use when handling any external input - forms, API request bodies, query params, CLI args, file parsing, or third-party payloads.
|
|
3
|
+
description: Strict frontend + backend schema validation (Zod or equivalent), normalization before validation (one shared normalizer on both sides - trim, lowercase the email, capitalize each word of a name, canonicalize a profile link or handle) and the rule that a check which cannot fail is not validation, schema consistency between client and server, and safe client-facing error handling. Use when handling any external input - forms, API request bodies, query params, CLI args, file parsing, or third-party payloads.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Validation & Error Handling Policy
|
|
@@ -33,6 +33,7 @@ description: Strict frontend + backend schema validation (Zod or equivalent), sc
|
|
|
33
33
|
- Use schema-driven validation (e.g. Zod or equivalent).
|
|
34
34
|
- Validation must prevent invalid state before submission.
|
|
35
35
|
- UI must reflect validation state immediately and clearly.
|
|
36
|
+
- **Every field, not only the famous formats.** Email and password get validated because their rules are obvious; the fields next to them (a profile link, a phone, a job title, a website) are the ones shipped open. Give each field a rule and show its error inline: a format check where there is a format, a max length everywhere, and an "empty after trimming" check where the value is required. A field whose only rule is `z.string()` is an unvalidated field.
|
|
36
37
|
- Validate cross-record constraints (uniqueness, availability, "already in use") in real time too, not just per-field type/format. When the client already holds the relevant set (the list of accounts, profiles, names, slugs it just rendered), check the input against that loaded data on every change and block submission on a conflict - do not defer the duplicate check to the server round-trip (this gives instant feedback and spares a redundant request and its DB query). The server still re-validates as the authority (client checks can be stale), but the user must see the conflict as they type. Mirror the server's exact rule (same pattern, case-folding, reserved values, and scope - e.g. unique per parent vs. globally) so the two never disagree; exclude the record's own current value when editing so renaming to the same name is not flagged.
|
|
37
38
|
|
|
38
39
|
### Backend / API Validation (Mandatory)
|
|
@@ -54,10 +55,40 @@ description: Strict frontend + backend schema validation (Zod or equivalent), sc
|
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
|
58
|
+
## Normalize Before You Validate (Client And Server)
|
|
59
|
+
|
|
60
|
+
Input arrives shaped by whoever typed it: a leading space from a paste, a name in lowercase from a phone keyboard, an address in mixed case, a profile link carrying tracking parameters. Normalize first, validate the normalized value, store that same value. Both sides do it - the client so the user sees what will be stored, the server because a request does not have to come from your form.
|
|
61
|
+
|
|
62
|
+
- Keep the normalizers in ONE module that the client and the server both import, next to the schema they belong to. Two copies drift, and the day they disagree the server rejects what the form accepted.
|
|
63
|
+
- Normalize INSIDE the schema wherever the validator supports it, so no caller can forget. Zod: `z.string().trim().toLowerCase().pipe(z.email())`. Order matters: `z.email().trim()` validates before trimming and rejects a pasted `" a@b.com"`. Yup: `.trim().lowercase().email()`. Pydantic: a `field_validator(mode="before")`.
|
|
64
|
+
- Default for every string field: trim both ends, collapse runs of inner whitespace, strip control and zero-width characters, and normalize Unicode to NFC so a composed and a decomposed accent are the same value.
|
|
65
|
+
- The server normalizes again, always. A client that skipped it, an API client, a script, and a replayed request all reach the same handler.
|
|
66
|
+
|
|
67
|
+
Per field kind (defaults - override only with a reason):
|
|
68
|
+
|
|
69
|
+
| Field | Normalizes to |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| Email | trim, lowercase, then validate. Store it lowercased so lookups and uniqueness never miss. |
|
|
72
|
+
| Person name (full name, first, last) | trim, collapse inner spaces, uppercase the first letter of every word - and ONLY that letter, so `McDonald`, `O'Brien`, `van der Berg` and `Jean-Luc` survive. Split on spaces, hyphens and apostrophes. |
|
|
73
|
+
| Username, handle, slug | trim, drop a leading `@`, lowercase when the identifier is case-insensitive, then check the allowed character set. |
|
|
74
|
+
| Profile link (LinkedIn, GitHub, X, Instagram) | accept BOTH a full URL and a bare handle, canonicalize to one stored form, check the host is the expected domain, and drop query and tracking parameters. |
|
|
75
|
+
| URL | trim, add the scheme when missing, lowercase the host, drop a trailing slash. |
|
|
76
|
+
| Phone | strip spaces, dots, dashes and parentheses, keep the leading `+`, store E.164. |
|
|
77
|
+
| Number, date, money | parse into the canonical type at the boundary; never store the localized string. |
|
|
78
|
+
|
|
79
|
+
### A check that cannot fail is not validation
|
|
80
|
+
|
|
81
|
+
- Never patch the value into validity and then check the patched value. `z.url().safeParse(v.startsWith("http") ? v : "https://" + v)` accepts `asdf`, `pepe`, and every other single token, because `https://asdf` is a syntactically valid URL. The field looks validated, has an error slot, and rejects nothing.
|
|
82
|
+
- Canonicalizing and validating are two steps, in that order. Canonicalize (add the scheme, strip the `@`), then apply a check the canonical value can still fail: the host contains a dot, the host is the expected domain, the path has the expected shape.
|
|
83
|
+
- Before calling a field done, type three wrong values into it and confirm each is rejected. A validator nobody has watched fail is unverified.
|
|
84
|
+
- The same applies to a permissive fallback: an `.optional()` that swallows `""`, a `catch()` that returns a default, or a `refine` that returns `true` on anything it cannot parse.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
57
88
|
## Validation Standards
|
|
58
89
|
|
|
59
90
|
- Validate type, shape, range, format, and required/optional status.
|
|
60
|
-
- Normalize input (trim, case-fold, canonicalize) before validating equality or storing.
|
|
91
|
+
- Normalize input (trim, case-fold, canonicalize) before validating equality or storing, per the section above.
|
|
61
92
|
- Enforce explicit allowlists over denylists for constrained values.
|
|
62
93
|
- Set explicit limits on size, length, and array cardinality to prevent abuse.
|
|
63
94
|
- Fail closed: unknown or unexpected fields are rejected, not silently ignored, on sensitive endpoints.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "validation-policy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
|
-
"description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
|
|
5
|
+
"description": "Strict frontend + backend schema validation, normalization before validation (shared normalizers: trim, lowercase email, capitalize names, canonicalize links and handles), schema consistency, and safe client-facing error handling.",
|
|
6
6
|
"updated": "2026-07-22T01:41:06+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
7
|
+
"cliVersion": "1.32.5",
|
|
8
|
+
"sha": "84bb43cc570aa927ab5c0311c17242b4158287961aa04af111cf83eb4eec6ce4"
|
|
9
9
|
}
|
package/bin/checksums.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"enigma-darwin-arm64": "
|
|
3
|
-
"enigma-linux-arm64": "
|
|
4
|
-
"enigma-linux-x64": "
|
|
5
|
-
"enigma-win32-x64.exe": "
|
|
2
|
+
"enigma-darwin-arm64": "17d7ee22f86d00a6dc03f48ecb7cd64400f941a54008ee6a058807f70b43bbe1",
|
|
3
|
+
"enigma-linux-arm64": "a6de59589f327143d9180eabf76320685e127092fbc60a89f37d08ee880fe44e",
|
|
4
|
+
"enigma-linux-x64": "bb23f85be6f2567ffaae0ebd852472519313fc607fe4ed7b3a10b23497759a08",
|
|
5
|
+
"enigma-win32-x64.exe": "e43f5cafec4ab27c16e6be74e5f802437809042077d94debfd32d371a5202c57"
|
|
6
6
|
}
|
package/dist/guardrails.js
CHANGED
|
@@ -34,6 +34,28 @@ var BUILTIN_RULES = [
|
|
|
34
34
|
severity: "warn",
|
|
35
35
|
skill: "database-expert"
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
id: "db-sqlite-app-datastore",
|
|
39
|
+
label: "PostgreSQL as the default relational engine",
|
|
40
|
+
// Basename glob, so it covers schema.prisma at any depth (a `**/*.prisma` glob would
|
|
41
|
+
// miss a root-level one - the documented gotcha).
|
|
42
|
+
files: ["*.prisma"],
|
|
43
|
+
scope: "file",
|
|
44
|
+
// A Prisma DATASOURCE on sqlite: the app's own database, declared by the ORM this policy
|
|
45
|
+
// already defaults to. `provider` also appears in generator blocks, but only a datasource
|
|
46
|
+
// ever names sqlite, so the value alone is the discriminator.
|
|
47
|
+
// WHY THIS SHAPE AND NOT A DEPENDENCY CHECK: a Prisma project declares NO sqlite driver in
|
|
48
|
+
// package.json (Prisma bundles its own), so the package.json signature that would look
|
|
49
|
+
// natural here misses the exact stack an agent scaffolds. Measured over the corpus: 14
|
|
50
|
+
// prisma schemas, every datasource already postgresql, and 0 package.json files declaring
|
|
51
|
+
// a sqlite driver at all - so this rule is a scaffolding guard with no legacy backlog to
|
|
52
|
+
// flag, which is also why it is the only slice of the convention worth gating.
|
|
53
|
+
pattern: `provider\\s*=\\s*["']sqlite["']`,
|
|
54
|
+
absent: "enigma:allow-sqlite",
|
|
55
|
+
message: "SQLite as the application datastore. SQLite is one file with one writer: it is right for a local-first or embedded store (a CLI's own state, a desktop or mobile app, a local cache or index, a test fixture) and wrong for anything deployed, replicated, or written to by a background worker - and moving off it later is a migration with downtime, not a config change. Default to PostgreSQL: real write concurrency, native uuid/jsonb/arrays/enums/timestamptz, partial and GIN indexes, partitioning and read replicas, plus pgvector, pg_trgm and PostGIS instead of a second service. On serverless put a pooler in front (PgBouncer, Prisma Accelerate, the provider's pooled endpoint); the constraint there is connection count, not the engine. If this datastore is deliberately local-first or embedded, mark it with an `enigma:allow-sqlite` note (database-expert).",
|
|
56
|
+
severity: "block",
|
|
57
|
+
skill: "database-expert"
|
|
58
|
+
},
|
|
37
59
|
{
|
|
38
60
|
id: "be-validate-input-ts",
|
|
39
61
|
label: "Validate request input (TypeScript)",
|
|
@@ -66,6 +88,53 @@ var BUILTIN_RULES = [
|
|
|
66
88
|
severity: "warn",
|
|
67
89
|
skill: "validation-policy"
|
|
68
90
|
},
|
|
91
|
+
{
|
|
92
|
+
id: "val-email-normalize",
|
|
93
|
+
label: "Email is normalized before it is validated",
|
|
94
|
+
files: ["*.ts", "*.tsx", "*.js", "*.jsx", "*.mts", "*.cts", "*.mjs", "*.vue", "*.svelte"],
|
|
95
|
+
excludeFiles: [
|
|
96
|
+
"*.test.*",
|
|
97
|
+
"*.spec.*",
|
|
98
|
+
"**/tests/**",
|
|
99
|
+
"**/__tests__/**",
|
|
100
|
+
"*.d.ts",
|
|
101
|
+
"*.min.js",
|
|
102
|
+
"**/dist/**",
|
|
103
|
+
"**/build/**",
|
|
104
|
+
"**/_build/**",
|
|
105
|
+
"**/node_modules/**",
|
|
106
|
+
"**/vendor/**",
|
|
107
|
+
"dist/**",
|
|
108
|
+
"build/**",
|
|
109
|
+
"_build/**",
|
|
110
|
+
"node_modules/**",
|
|
111
|
+
"vendor/**"
|
|
112
|
+
],
|
|
113
|
+
scope: "file",
|
|
114
|
+
// An email schema declared with no normalization anywhere in the file. The three forms
|
|
115
|
+
// cover the ecosystem: `.string()...email(` (zod 3, yup, joi), `z.email(` (zod 4) and
|
|
116
|
+
// `v.email(` (valibot). Measured over ~2500 files of real product repos: 10 files declare
|
|
117
|
+
// an email schema, 7 normalize nothing - all 7 genuine (invitation forms, backend request
|
|
118
|
+
// schemas, an auth route), 0 false positives. `absent` keys on CASE-FOLDING only, not on
|
|
119
|
+
// trimming: an email schema that trims but keeps the case still lets "A@x.com" and
|
|
120
|
+
// "a@x.com" become two accounts, which is the defect. It stays file-scoped (the engine has
|
|
121
|
+
// no line-scoped absent), so a file that lowercases anything at all clears - a deliberate
|
|
122
|
+
// false negative, precision over recall.
|
|
123
|
+
pattern: "\\.string\\(\\)[^\\n]*\\.email\\(|\\bz\\.email\\(|\\bv\\.email\\(",
|
|
124
|
+
absent: "toLowerCase|lowercase\\(|normalizeEmail|enigma:allow-raw-email",
|
|
125
|
+
message: 'Email schema with no normalization. An address pasted with a leading space or typed in mixed case must reduce to ONE stored value, or the lookup misses, the uniqueness check passes, and the user ends up with a second account. Normalize inside the schema so no caller can forget it: Zod `z.string().trim().toLowerCase().pipe(z.email())` - the order matters, `z.email().trim()` validates first and rejects a pasted " a@b.com" - Yup `.trim().lowercase().email()`, Pydantic a `field_validator(mode="before")`. Use the same schema on the client and the server. If this address must keep its case, mark it with an `enigma:allow-raw-email` note (validation-policy).',
|
|
126
|
+
severity: "block",
|
|
127
|
+
skill: "validation-policy"
|
|
128
|
+
},
|
|
129
|
+
// NOTE: there is deliberately no "URL check that patches the value first" rule, though that
|
|
130
|
+
// exact shape is what makes a link field accept anything: `z.url().safeParse(v.startsWith("http")
|
|
131
|
+
// ? v : "https://" + v)` passes for `asdf`, because `https://asdf` IS a syntactically valid URL.
|
|
132
|
+
// The signature (a scheme interpolated into the string being parsed) does not survive
|
|
133
|
+
// measurement: 7 hits across the corpus and 5 are legitimate canonicalization for DISPLAY or
|
|
134
|
+
// parsing (build a URL to read its hostname), which is the same code shape with none of the
|
|
135
|
+
// defect. Telling them apart needs to know whether the result is a VERDICT or a value, which
|
|
136
|
+
// is not in the line. It stays in validation-policy ("A check that cannot fail is not
|
|
137
|
+
// validation") together with the URL-or-handle canonicalization rule.
|
|
69
138
|
// NOTE: no Go/Rust input-validation rule. Go's manual validation (`if in.X == ""`) is
|
|
70
139
|
// idiomatic and has no detectable signature, and Rust's serde typed deserialization already
|
|
71
140
|
// enforces shape - a rule for either would false-positive. The generic "validate every input"
|
|
@@ -85,6 +154,47 @@ var BUILTIN_RULES = [
|
|
|
85
154
|
severity: "warn",
|
|
86
155
|
skill: "frontend-policy"
|
|
87
156
|
},
|
|
157
|
+
{
|
|
158
|
+
id: "fe-name-input-capitalize",
|
|
159
|
+
label: "A person-name field capitalizes its words",
|
|
160
|
+
files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.html", "*.htm"],
|
|
161
|
+
excludeFiles: [
|
|
162
|
+
"*.test.*",
|
|
163
|
+
"*.spec.*",
|
|
164
|
+
"**/tests/**",
|
|
165
|
+
"**/__tests__/**",
|
|
166
|
+
"**/stories/**",
|
|
167
|
+
"*.stories.*",
|
|
168
|
+
"*.min.js",
|
|
169
|
+
"**/dist/**",
|
|
170
|
+
"**/build/**",
|
|
171
|
+
"**/_build/**",
|
|
172
|
+
"**/node_modules/**",
|
|
173
|
+
"**/vendor/**",
|
|
174
|
+
"dist/**",
|
|
175
|
+
"build/**",
|
|
176
|
+
"_build/**",
|
|
177
|
+
"node_modules/**",
|
|
178
|
+
"vendor/**"
|
|
179
|
+
],
|
|
180
|
+
scope: "file",
|
|
181
|
+
// A field that holds a PERSON's name, with no autocapitalize anywhere in the file. On a
|
|
182
|
+
// phone the keyboard defaults to sentence capitalization, so the user types "juan perez"
|
|
183
|
+
// and that is what gets stored; `autocapitalize="words"` is the one attribute that fixes
|
|
184
|
+
// it, and it is inert on a desktop keyboard.
|
|
185
|
+
// PRECISION comes from the token set, measured over ~2500 files of real product repos.
|
|
186
|
+
// Only names that can ONLY be a person's are matched: the HTML autofill tokens (which the
|
|
187
|
+
// spec defines as the person's name) and first/last/surname/apellido field names. `name`,
|
|
188
|
+
// `nombre` and `fullname` are deliberately EXCLUDED - every one of their hits in the
|
|
189
|
+
// corpus was an entity name (a project, a team, a token, a webhook), which must not be
|
|
190
|
+
// title-cased. With that set: 8 findings, all genuine person-name inputs, 0 false
|
|
191
|
+
// positives; the one file that already sets autocapitalize is correctly cleared.
|
|
192
|
+
pattern: `autocomplete=\\{?["'](?:name|given-name|family-name|additional-name|honorific-prefix)["']|(?:\\bname|\\bid|\\bfor|formControlName)=\\{?["'](?:first[-_]?name|last[-_]?name|given[-_]?name|family[-_]?name|surname|apellidos?)["']`,
|
|
193
|
+
absent: "autocapitalize|enigma:allow-no-capitalize",
|
|
194
|
+
message: 'Person-name field with no capitalization rule. Phone keyboards capitalize SENTENCES, so a name typed on mobile is stored as "juan perez": add `autocapitalize="words"` (plus `spellcheck="false"` and `autocorrect="off"`, and the matching `autocomplete` token). The attribute only covers typing, so normalize the value too - trim, collapse inner spaces, and uppercase the first letter of every word - on blur and again on the server, uppercasing ONLY that letter so `McDonald`, `O\'Brien` and `van der Berg` survive. Best placed once in the shared Input/TextField component, selected by a prop. For a field that must keep what was typed, add an `enigma:allow-no-capitalize` note (frontend-policy, validation-policy).',
|
|
195
|
+
severity: "block",
|
|
196
|
+
skill: "frontend-policy"
|
|
197
|
+
},
|
|
88
198
|
{
|
|
89
199
|
id: "fe-no-native-dialog",
|
|
90
200
|
label: "No native browser dialogs",
|
|
@@ -463,6 +573,273 @@ var BUILTIN_RULES = [
|
|
|
463
573
|
fileCheck: "proc-windows-hide",
|
|
464
574
|
message: "Process spawned without windowsHide. On Windows a console child started by a process that has no console of its own - a daemon, an editor hook, a detached background task - pops a real console window on screen and closes it again, which reads as something crashing. Add `windowsHide: true` to the options object; it is inert on macOS and Linux, and inert on Windows when the parent already has a console, so it is safe on every call that is not deliberately opening a terminal for the user. For one that IS (a login flow that must show a terminal), mark the call with an `enigma:` note.",
|
|
465
575
|
severity: "block"
|
|
576
|
+
},
|
|
577
|
+
// NOTE: no rule for "render the cached snapshot, then patch only what changed". Whether a
|
|
578
|
+
// fresh response REPLACES the rendered list or is reconciled into it is a property of the
|
|
579
|
+
// state update, which normally lives in a store, a query library's cache or a parent - and
|
|
580
|
+
// `setRows(data)` is correct code in the many views that have no snapshot to reconcile
|
|
581
|
+
// against. There is no file-local evidence of the defect, so it stays in frontend-policy's
|
|
582
|
+
// Client-Side Caching section.
|
|
583
|
+
{
|
|
584
|
+
id: "fe-mobile-drawer-full-width",
|
|
585
|
+
label: "An off-canvas panel fills the phone screen",
|
|
586
|
+
files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.html", "*.htm"],
|
|
587
|
+
excludeFiles: [
|
|
588
|
+
"*.test.*",
|
|
589
|
+
"*.spec.*",
|
|
590
|
+
"**/tests/**",
|
|
591
|
+
"**/__tests__/**",
|
|
592
|
+
"**/stories/**",
|
|
593
|
+
"*.stories.*",
|
|
594
|
+
"*.min.js",
|
|
595
|
+
"**/dist/**",
|
|
596
|
+
"**/build/**",
|
|
597
|
+
"**/node_modules/**",
|
|
598
|
+
"**/vendor/**",
|
|
599
|
+
"dist/**",
|
|
600
|
+
"build/**",
|
|
601
|
+
"node_modules/**",
|
|
602
|
+
"vendor/**"
|
|
603
|
+
],
|
|
604
|
+
scope: "file",
|
|
605
|
+
// An off-canvas panel (fixed/absolute, pinned for the full height) whose width is a fixed
|
|
606
|
+
// desktop size with no full-width base. Utility-class frameworks only: the same defect in
|
|
607
|
+
// a stylesheet lives inside a @media block, which a line scanner cannot see.
|
|
608
|
+
// PRECISION, measured over the reference corpus: the width must be a PANEL width - 100px
|
|
609
|
+
// or more, 10rem or more, a Tailwind step of 40 (10rem) or more, or a fraction of the
|
|
610
|
+
// viewport. Every false positive found was a hairline (`w-px` chart cursors, a `w-px`
|
|
611
|
+
// resize handle, a `before:w-0.5` guide line), and that one bound removed all of them.
|
|
612
|
+
// The negative lookahead carries the "unless the design says otherwise" cases: an
|
|
613
|
+
// already-full-width base, a width capped by `max-w-full`, and a panel that is hidden on
|
|
614
|
+
// phones anyway (the desktop half of a hidden/drawer pair).
|
|
615
|
+
pattern: "^(?!.*enigma:)(?!.*(?:\\bw-full\\b|\\bmax-w-full\\b|\\bw-screen\\b|\\bhidden\\s+(?:sm|md|lg|xl):|\\bmd:hidden\\b))(?:.*\\b(?:fixed|absolute)\\b[^\"'`]*\\b(?:inset-y-0|h-full|h-screen)\\b[^\"'`]*\\bw-(?:\\[(?:\\d{3,}px|\\d{2,}rem)\\]|[4-9]\\d\\b|\\d{3}\\b|\\d/\\d\\b)|.*\\bw-(?:\\[(?:\\d{3,}px|\\d{2,}rem)\\]|[4-9]\\d\\b|\\d{3}\\b|\\d/\\d\\b)[^\"'`]*\\b(?:fixed|absolute)\\b[^\"'`]*\\b(?:inset-y-0|h-full|h-screen)\\b)",
|
|
616
|
+
absent: "enigma:allow-partial-drawer",
|
|
617
|
+
message: "Off-canvas panel with a desktop width. On a phone a sidebar, drawer or nav panel takes the WHOLE screen - `w-full` as the base, with the desktop width added at a breakpoint (`w-full md:w-80`) or capped by `max-w-*`. A 320px panel on a 360px screen leaves a sliver of dead content behind it, and a panel wider than the viewport is simply cut off. Keep it dismissible: a close control, the backdrop, and Escape. If the design deliberately wants a partial-width panel on mobile, put an `enigma:` note on the line or `enigma:allow-partial-drawer` in the file (frontend-policy).",
|
|
618
|
+
severity: "block",
|
|
619
|
+
skill: "frontend-policy"
|
|
620
|
+
},
|
|
621
|
+
// AUTH. Three defects an agent building a sign-in flow reproduces constantly, each with a
|
|
622
|
+
// file-local signature. They fire only in files NAMED for the flow they belong to
|
|
623
|
+
// (*login*, *register*, *2fa*, ...), which is what keeps them off the rest of a codebase:
|
|
624
|
+
// "password" and "redirect to /login" appear everywhere, but not in a file called login.tsx
|
|
625
|
+
// that is not the sign-in surface. The semantic half of each - token lifetime, lockout
|
|
626
|
+
// policy, what a reset email may reveal - has no signature and lives in security-policy.
|
|
627
|
+
{
|
|
628
|
+
id: "auth-password-reset-entry",
|
|
629
|
+
label: "Login form offers a way to recover the password",
|
|
630
|
+
ignoreFileCase: true,
|
|
631
|
+
// Named for the flow, either way a project spells it: in the FILE name (login-form.tsx)
|
|
632
|
+
// or in the DIRECTORY (app/(auth)/login/page.tsx, the Next App Router shape - a basename
|
|
633
|
+
// glob would miss every one of those). Dir globs are listed twice because `**/x/**` does
|
|
634
|
+
// not match a root-level `x/`, the same gotcha the excludes above carry.
|
|
635
|
+
files: [
|
|
636
|
+
"*login*.tsx",
|
|
637
|
+
"*login*.jsx",
|
|
638
|
+
"*login*.vue",
|
|
639
|
+
"*login*.svelte",
|
|
640
|
+
"*login*.astro",
|
|
641
|
+
"*login*.html",
|
|
642
|
+
"*signin*.tsx",
|
|
643
|
+
"*signin*.jsx",
|
|
644
|
+
"*signin*.vue",
|
|
645
|
+
"*signin*.svelte",
|
|
646
|
+
"*signin*.astro",
|
|
647
|
+
"*signin*.html",
|
|
648
|
+
"*sign-in*.tsx",
|
|
649
|
+
"*sign-in*.jsx",
|
|
650
|
+
"*sign-in*.vue",
|
|
651
|
+
"*sign-in*.svelte",
|
|
652
|
+
"*sign-in*.astro",
|
|
653
|
+
"*sign-in*.html",
|
|
654
|
+
"**/login/**",
|
|
655
|
+
"login/**",
|
|
656
|
+
"**/signin/**",
|
|
657
|
+
"signin/**",
|
|
658
|
+
"**/sign-in/**",
|
|
659
|
+
"sign-in/**"
|
|
660
|
+
],
|
|
661
|
+
excludeFiles: [
|
|
662
|
+
"*.test.*",
|
|
663
|
+
"*.spec.*",
|
|
664
|
+
"**/tests/**",
|
|
665
|
+
"**/__tests__/**",
|
|
666
|
+
"**/stories/**",
|
|
667
|
+
"*.stories.*",
|
|
668
|
+
"**/dist/**",
|
|
669
|
+
"**/build/**",
|
|
670
|
+
"**/node_modules/**",
|
|
671
|
+
"**/vendor/**",
|
|
672
|
+
"dist/**",
|
|
673
|
+
"build/**",
|
|
674
|
+
"node_modules/**",
|
|
675
|
+
"vendor/**"
|
|
676
|
+
],
|
|
677
|
+
scope: "file",
|
|
678
|
+
// The password field is what makes this THE sign-in surface rather than a wrapper or a
|
|
679
|
+
// route file; a login page that only renders <LoginForm/> has no password field and is
|
|
680
|
+
// correctly left alone (the form itself is the file that must carry the link).
|
|
681
|
+
pattern: `type=["']password["']|type=\\{["']password["']\\}`,
|
|
682
|
+
// Any recovery affordance clears the file: the link, the route, or a handler named for it.
|
|
683
|
+
absent: "forgot|reset[-_ ]?password|password[-_ ]?reset|recover|olvid|recuperar|magic[-_ ]?link|enigma:allow-no-reset",
|
|
684
|
+
message: 'Sign-in form with no way out of a forgotten password. Every login form needs a visible "Forgot your password?" entry point next to the password field, leading to a real reset flow: ask for the identifier, always answer the same way (never reveal whether the account exists), email a single-use token that expires in ~15-60 minutes, and on success invalidate that token plus every other active session. If this screen is deliberately reset-less (an internal tool, SSO-only, a passwordless magic-link form), mark the file with an `enigma:allow-no-reset` note (frontend-policy, security-policy).',
|
|
685
|
+
severity: "block",
|
|
686
|
+
skill: "security-policy"
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
id: "auth-signup-auto-login",
|
|
690
|
+
label: "Registration signs the user in",
|
|
691
|
+
ignoreFileCase: true,
|
|
692
|
+
files: [
|
|
693
|
+
"*register*.tsx",
|
|
694
|
+
"*register*.jsx",
|
|
695
|
+
"*register*.ts",
|
|
696
|
+
"*register*.js",
|
|
697
|
+
"*register*.vue",
|
|
698
|
+
"*register*.svelte",
|
|
699
|
+
"*register*.astro",
|
|
700
|
+
"*signup*.tsx",
|
|
701
|
+
"*signup*.jsx",
|
|
702
|
+
"*signup*.ts",
|
|
703
|
+
"*signup*.js",
|
|
704
|
+
"*signup*.vue",
|
|
705
|
+
"*signup*.svelte",
|
|
706
|
+
"*signup*.astro",
|
|
707
|
+
"*sign-up*.tsx",
|
|
708
|
+
"*sign-up*.jsx",
|
|
709
|
+
"*sign-up*.ts",
|
|
710
|
+
"*sign-up*.js",
|
|
711
|
+
"*sign-up*.vue",
|
|
712
|
+
"*sign-up*.svelte",
|
|
713
|
+
"*sign-up*.astro",
|
|
714
|
+
"**/register/**",
|
|
715
|
+
"register/**",
|
|
716
|
+
"**/signup/**",
|
|
717
|
+
"signup/**",
|
|
718
|
+
"**/sign-up/**",
|
|
719
|
+
"sign-up/**"
|
|
720
|
+
],
|
|
721
|
+
excludeFiles: [
|
|
722
|
+
"*.test.*",
|
|
723
|
+
"*.spec.*",
|
|
724
|
+
"**/tests/**",
|
|
725
|
+
"**/__tests__/**",
|
|
726
|
+
"**/stories/**",
|
|
727
|
+
"*.stories.*",
|
|
728
|
+
"*.d.ts",
|
|
729
|
+
"**/dist/**",
|
|
730
|
+
"**/build/**",
|
|
731
|
+
"**/node_modules/**",
|
|
732
|
+
"**/vendor/**",
|
|
733
|
+
"dist/**",
|
|
734
|
+
"build/**",
|
|
735
|
+
"node_modules/**",
|
|
736
|
+
"vendor/**"
|
|
737
|
+
],
|
|
738
|
+
scope: "file",
|
|
739
|
+
// Only a PROGRAMMATIC redirect to the sign-in route counts. An `href="/login"` is the
|
|
740
|
+
// "already have an account?" link every sign-up form carries and is not the defect, so
|
|
741
|
+
// the pattern requires a navigation CALL - which is the code path that runs after the
|
|
742
|
+
// account is created.
|
|
743
|
+
pattern: "(?:push|replace|redirect|navigate|goto|assign)\\(\\s*[\"'`][^\"'`]*/(?:login|signin|sign-in)\\b|location(?:\\.href)?\\s*=\\s*[\"'`][^\"'`]*/(?:login|signin|sign-in)\\b|(?:push|replace|navigate)\\(\\s*\\{[^}]*name:\\s*[\"'`](?:login|signin|sign-in)[\"'`]",
|
|
744
|
+
// Any session-establishing call in the file means the redirect is some other path
|
|
745
|
+
// (an already-registered branch, an error case), so the file is cleared.
|
|
746
|
+
absent: "signIn\\(|signInWith|createSession|setSession|startSession|newSession|setAuthCookie|setAuthToken|setToken\\(|setAuth\\(|login\\(|logIn\\(|authenticate\\(|session\\.save|sessionStorage\\.setItem\\([\"'`](?:token|session)|cookies\\(\\)\\.set|setUser\\(|enigma:allow-login-redirect",
|
|
747
|
+
message: "Registration sends the user to the sign-in screen instead of signing them in. A successful sign-up already proves the credentials: establish the session right there and land the user in the app. Keep email verification asynchronous (let them in, ask them to confirm, and gate only the actions that need a verified address) rather than parking them on a login form to type what they just typed. If this redirect is deliberate (an admin creating someone else's account, an approval queue), mark the file with an `enigma:allow-login-redirect` note (backend-policy, security-policy).",
|
|
748
|
+
severity: "block",
|
|
749
|
+
skill: "security-policy"
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
id: "auth-rate-limit",
|
|
753
|
+
label: "Rate-limit the credential endpoints",
|
|
754
|
+
ignoreFileCase: true,
|
|
755
|
+
files: [
|
|
756
|
+
"*login*.ts",
|
|
757
|
+
"*login*.js",
|
|
758
|
+
"*login*.mts",
|
|
759
|
+
"*login*.cts",
|
|
760
|
+
"*login*.py",
|
|
761
|
+
"*signin*.ts",
|
|
762
|
+
"*signin*.js",
|
|
763
|
+
"*sign-in*.ts",
|
|
764
|
+
"*sign-in*.js",
|
|
765
|
+
"*signin*.py",
|
|
766
|
+
"*sign-in*.py",
|
|
767
|
+
"*register*.ts",
|
|
768
|
+
"*register*.js",
|
|
769
|
+
"*signup*.ts",
|
|
770
|
+
"*signup*.js",
|
|
771
|
+
"*sign-up*.ts",
|
|
772
|
+
"*sign-up*.js",
|
|
773
|
+
"*register*.py",
|
|
774
|
+
"*signup*.py",
|
|
775
|
+
"*sign-up*.py",
|
|
776
|
+
"*2fa*.ts",
|
|
777
|
+
"*2fa*.js",
|
|
778
|
+
"*2fa*.py",
|
|
779
|
+
"*mfa*.ts",
|
|
780
|
+
"*mfa*.js",
|
|
781
|
+
"*mfa*.py",
|
|
782
|
+
"*otp*.ts",
|
|
783
|
+
"*otp*.js",
|
|
784
|
+
"*otp*.py",
|
|
785
|
+
"*forgot-password*.ts",
|
|
786
|
+
"*forgot-password*.js",
|
|
787
|
+
"*forgot-password*.py",
|
|
788
|
+
"*reset-password*.ts",
|
|
789
|
+
"*reset-password*.js",
|
|
790
|
+
"*reset-password*.py",
|
|
791
|
+
"**/login/**",
|
|
792
|
+
"login/**",
|
|
793
|
+
"**/signin/**",
|
|
794
|
+
"signin/**",
|
|
795
|
+
"**/sign-in/**",
|
|
796
|
+
"sign-in/**",
|
|
797
|
+
"**/register/**",
|
|
798
|
+
"register/**",
|
|
799
|
+
"**/signup/**",
|
|
800
|
+
"signup/**",
|
|
801
|
+
"**/sign-up/**",
|
|
802
|
+
"sign-up/**",
|
|
803
|
+
"**/2fa/**",
|
|
804
|
+
"2fa/**",
|
|
805
|
+
"**/mfa/**",
|
|
806
|
+
"mfa/**",
|
|
807
|
+
"**/otp/**",
|
|
808
|
+
"otp/**",
|
|
809
|
+
"**/forgot-password/**",
|
|
810
|
+
"forgot-password/**",
|
|
811
|
+
"**/reset-password/**",
|
|
812
|
+
"reset-password/**"
|
|
813
|
+
],
|
|
814
|
+
excludeFiles: [
|
|
815
|
+
"*.test.*",
|
|
816
|
+
"*.spec.*",
|
|
817
|
+
"**/tests/**",
|
|
818
|
+
"**/__tests__/**",
|
|
819
|
+
"test_*.py",
|
|
820
|
+
"*_test.py",
|
|
821
|
+
"*.d.ts",
|
|
822
|
+
"**/dist/**",
|
|
823
|
+
"**/build/**",
|
|
824
|
+
"**/node_modules/**",
|
|
825
|
+
"**/vendor/**",
|
|
826
|
+
"dist/**",
|
|
827
|
+
"build/**",
|
|
828
|
+
"node_modules/**",
|
|
829
|
+
"vendor/**"
|
|
830
|
+
],
|
|
831
|
+
scope: "file",
|
|
832
|
+
// A server-side handler for the flow: the route export/registration, a framework
|
|
833
|
+
// decorator, or a "use server" module (a Next server action reachable from the sign-in
|
|
834
|
+
// page is an unauthenticated endpoint like any other, and it is how App Router projects
|
|
835
|
+
// write this). A client component calling fetch() is deliberately not matched - it
|
|
836
|
+
// cannot enforce a limit, and the file that must is the one defining the endpoint.
|
|
837
|
+
pattern: `export\\s+(?:async\\s+)?function\\s+(?:POST|PUT|PATCH)\\b|export\\s+const\\s+(?:POST|PUT|PATCH)\\s*[:=]|\\b(?:router|app|api|server|fastify)\\.(?:post|put|patch)\\s*\\(|@(?:app|router|bp|blueprint)\\.(?:post|route)\\s*\\(|@Post\\s*\\(|^["']use server["']`,
|
|
838
|
+
// Cleared by any limiter in the file, whatever the library or the wrapper name.
|
|
839
|
+
absent: "rate[-_]?limit|ratelimit|Ratelimit|RateLimiter|limiter|throttle|slowDown|slow_down|bottleneck|arcjet|leaky|token[-_]?bucket|attempts?[-_]?(?:left|remaining|count)|lockout|too[-_ ]?many[-_ ]?requests|429|enigma:allow-unlimited-auth",
|
|
840
|
+
message: "Credential endpoint with no rate limiting. Login, registration, password reset and every 2FA/OTP verification are guessing surfaces: limit them BY IP (blunt, stops the broad sweep) AND BY ACCOUNT or identifier (stops the slow distributed attack the IP limit misses), count failures rather than requests, back off exponentially, and answer 429 with Retry-After. Keep the accounting server-side and identical for an unknown account, so the limiter itself does not become an account-existence oracle. If the limit is enforced upstream (gateway, middleware, WAF), note it in the file with an `enigma:allow-unlimited-auth` marker (security-policy, backend-policy).",
|
|
841
|
+
severity: "block",
|
|
842
|
+
skill: "security-policy"
|
|
466
843
|
}
|
|
467
844
|
];
|
|
468
845
|
var PROJECT_CHECKS = {
|
|
@@ -533,10 +910,10 @@ function readPkgDeps(root) {
|
|
|
533
910
|
return null;
|
|
534
911
|
}
|
|
535
912
|
}
|
|
536
|
-
function globToRegExp(glob) {
|
|
913
|
+
function globToRegExp(glob, ignoreCase = false) {
|
|
537
914
|
const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
538
915
|
const body = esc.replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/ /g, ".*").replace(/\?/g, "[^/]");
|
|
539
|
-
return new RegExp(glob.includes("/") ? `^${body}$` : `(^|/)${body}
|
|
916
|
+
return new RegExp(glob.includes("/") ? `^${body}$` : `(^|/)${body}$`, ignoreCase ? "i" : "");
|
|
540
917
|
}
|
|
541
918
|
function guardrailsConfigPath() {
|
|
542
919
|
return process.env.ENIGMA_GUARDRAILS_CONFIG || join(homedir(), ".enigma-guardrails.json");
|
|
@@ -581,8 +958,8 @@ function checkFile(file, content, projectRoot) {
|
|
|
581
958
|
const norm = file.replace(/\\/g, "/");
|
|
582
959
|
const out = [];
|
|
583
960
|
for (const rule of loadRules()) {
|
|
584
|
-
if (!rule.files.some((g) => globToRegExp(g).test(norm))) continue;
|
|
585
|
-
if (rule.excludeFiles?.some((g) => globToRegExp(g).test(norm))) continue;
|
|
961
|
+
if (!rule.files.some((g) => globToRegExp(g, rule.ignoreFileCase).test(norm))) continue;
|
|
962
|
+
if (rule.excludeFiles?.some((g) => globToRegExp(g, rule.ignoreFileCase).test(norm))) continue;
|
|
586
963
|
const base = { ruleId: rule.id, severity: rule.severity, file: norm, message: rule.message, skill: rule.skill };
|
|
587
964
|
if (rule.scope === "file" && rule.maxBytes) {
|
|
588
965
|
const bytes = Buffer.byteLength(content, "utf8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enigma-cli",
|
|
3
|
-
"version": "1.32.
|
|
3
|
+
"version": "1.32.5",
|
|
4
4
|
"description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|