enigma-cli 1.42.0 → 1.43.0

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.
@@ -7,6 +7,6 @@
7
7
  "minimalCode"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.42.0",
10
+ "cliVersion": "1.43.0",
11
11
  "sha": "3f0dcc28341bb0407860534f7ce9314cfc91b5e673f8f3d13b89d61851ed75f6"
12
12
  }
@@ -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.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "f742a2be3f328b9ea1ff9a35a449177c2cbec35ad16e46f7054b7a873a2ab017"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Backend/API architecture: controller-service-repository layering, modern TypeScript project configuration (module resolution, strict flags, @/* path alias), API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "652637f818b4ce4f4d5d83fa68fa57fe7842e32fb179523958c5496af54bb938"
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, path-alias specifiers and no file extensions, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "3beb0383a9cb0331ef8a579308a2517d24d57a8ba33751ad17d16d0a4f66e344"
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-08-19T12:37:59+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "6b8a924cc11b61d4b45b73384aeb095ce622229359b7cc2c6ce4bb0cf3015ed7"
9
9
  }
@@ -7,6 +7,6 @@
7
7
  "codeGraph"
8
8
  ],
9
9
  "updated": "2026-08-15T20:49:24+02:00",
10
- "cliVersion": "1.42.0",
10
+ "cliVersion": "1.43.0",
11
11
  "sha": "95b7202e5659f8d9d6f11ce29e7dc5b01575ba86c78ef948bec4753ccf430155"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Senior database architecture policy: engine selection (PostgreSQL by default, SQLite only for local-first/embedded stores), ORM selection (Prisma in TypeScript/JavaScript), query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
6
6
  "updated": "2026-08-04T18:33:27+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "26fddc14caf697e830448a2cc453ddc470fe83d098d50bd67b278effba63274e"
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-08-02T04:15:47+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "968876bcac9ce6e5a20be05c89ac97191b112c9e08679a8dc506d150c375fec4"
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.42.0",
7
+ "cliVersion": "1.43.0",
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.42.0",
7
+ "cliVersion": "1.43.0",
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.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
9
9
  }
@@ -26,7 +26,15 @@ description: Frontend architecture - reusable components, abstraction thresholds
26
26
  - Interaction logic - looping/draggable rows, momentum, focus traps, virtualized lists, dismiss timers, read caches - is BEHAVIOR, and behavior that has already been measured and tested is a dependency, not a snippet to rewrite. Check the catalogue before writing it: `enigma add` lists it and `enigma add <name>` adds it as a DEPENDENCY, which is how you use these - the package keeps getting fixes and a dependency is how they reach the project. `--copy` vendors the source and freezes it at that version; reach for it to EDIT a component, never as the normal way to add one.
27
27
  - These primitives ship NO visual styles. They apply only what the behavior requires (overflow, touch-action, user-select, will-change, transform) and publish their state as `data-*` attributes, so the look stays entirely yours.
28
28
  - Do not hand-roll one of them "just for this page". The hand-rolled version is where the measured bugs come back: a duration-driven marquee that accelerates as items are added, a `setPointerCapture` drag that silently stops every link in the row from opening, a hover slowdown that sticks forever after the first tap because touch never fires `pointerleave`.
29
- - Two of them are worth naming because their hand-rolled versions are near-misses of behavior everyone already knows, and the `fe-context-menu-hand-rolled` and `fe-selection-hand-rolled` guardrails block both. A RIGHT-CLICK MENU (`enigma add context-menu`) is never the panel: it is the release of the opening press choosing whatever row it landed on, a submenu that closes while the pointer crosses a sibling to reach it (the way out goes over them, so closing on the first crossing makes a nested menu unreachable), a panel that opens off the edge of the window instead of flipping, no Escape, no arrows, no Shift+F10, and a menu left behind when the page scrolls - plus shortcuts printed for the reader's platform and a menu that refuses to open when it has nothing to show. A LIST SELECTION (`enigma add selection`) is Explorer's model: Shift+click measures from the ANCHOR every time (measuring from the last click leaves a trail of rows as it is dragged), Ctrl toggles instead of replacing, Ctrl+Shift adds the range, a range steps over a row it may not select, and Ctrl+A, Escape and the arrow keys exist at all - with every shortcut rebindable or removable, and delete/rename/open reported as commands rather than performed.
29
+ - THE CATALOGUE, so you never have to guess whether one exists. `enigma add <name>` adds it as a dependency:
30
+ - `input` - one field for every `type`: password (reveal, generator, strength meter, breach check), search (debounce, ranking, clear), color (swatch, saturation square, hue and alpha rails, presets, eyedropper), and everything else as a plain field with native props.
31
+ - `select` - a listbox that replaces the native one: typeahead, disabled rows the arrows walk past, one highlight shared by pointer and keyboard, a panel that measures before it opens, multi-select tags, a hidden input so the form can submit it.
32
+ - `palette` - the Ctrl/Cmd+K panel: one flat keyboard sequence across groups, a highlight that wraps and that a shorter list pulls back, `aria-activedescendant` so the caret never leaves the field, recents guarded against a storage that throws.
33
+ - `toast` - notifications whose timer pauses on hover, on focus and on a hidden tab, with promise toasts and actions.
34
+ - `context-menu` - the right-click menu with the desktop's behaviour: submenus on a beat, a panel that flips at the window edge, Escape, arrows, Shift+F10, shortcuts printed for the reader's platform.
35
+ - `selection` - Explorer's selection model: anchor and cursor, Shift, Ctrl, Ctrl+Shift, a rubber band, arrow keys, and delete/rename/open reported as commands.
36
+ - `button` - press states, async pending, cooldowns, and a shortcut badge; `marquee` and `logo-marquee` - a looping row that takes px/s and measures its own lap; `search` - the engine behind a field or a page; `flags` - a country flag as an image, never an emoji; `relative-time` - "3 minutes ago" that updates itself; `network` - online/offline and connection quality; `notifications` - the queue under the toast, for a renderer of your own.
37
+ - Five of them are already deterministic: `fe-select-hand-rolled`, `fe-toast-hand-rolled`, `fe-palette-hand-rolled`, `fe-context-menu-hand-rolled` and `fe-selection-hand-rolled` block the hand-rolled version at the moment it is written, and each message names the defects the primitive fixed. Reading the message is faster than rediscovering them.
30
38
  - If the catalogue has no primitive for what you need, write it as one (core + adapter + registry entry + test) rather than inline in a page, so the next screen reuses it instead of rewriting it.
31
39
 
32
40
  ## Component Reuse (Mandatory)
@@ -3,7 +3,7 @@
3
3
  "version": "1.31.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Frontend architecture: reusable components, abstraction thresholds, state management, real-time form validation where an emptied or not-yet-filled field is incomplete rather than invalid (a `*` on the label and an aria-disabled submit, never \"email is not valid\" over an empty input), 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 is the preferred default, pagination the deliberate exception when the design or the user calls for it, virtualization, skeletons, progressive loading), data views that ship their own affordances by default (a log, expense, transaction or history table is not done when the rows render: fuse.js search, the filters its column kinds imply, a date range, sort, filter state in the URL, and an export of the filtered set), every reference to an entity being a way into it (a name, id, project or path in a row links to that record, reveals it in a hover card, or at minimum copies and filters by it, never inert text; machine codes get human labels and raw payloads are never dumped into a cell), 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), auth screens (breached-password feedback, strength meter, cookie consent before login/register), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
6
- "updated": "2026-09-01T21:30:38+02:00",
7
- "cliVersion": "1.42.0",
8
- "sha": "c18c746f8c29c591685b81cabbaada43996640bc830181c28b8560e904934bc2"
6
+ "updated": "2026-09-05T02:46:46+02:00",
7
+ "cliVersion": "1.43.0",
8
+ "sha": "d0085206565523d2a54d3c74ccb670fe5c9c43a64cef2b31c15b22156d1b8131"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
6
  "updated": "2026-08-22T09:25:34+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "cb320a6a5fd3279604ad7920b877c1a36e48f4f39aeead1a4c4daa89901d9ce0"
9
9
  }
@@ -7,6 +7,6 @@
7
7
  "logoColorPolicy"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.42.0",
10
+ "cliVersion": "1.43.0",
11
11
  "sha": "09cdbefd98625b02a7d03685e5deed128238ff8454a83fe22279610fe3ef8ddf"
12
12
  }
@@ -54,6 +54,15 @@ These four screens are one system: an attacker who cannot guess a password will
54
54
  - 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).
55
55
  - 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.
56
56
 
57
+ ### A credential just proven is not asked for again
58
+
59
+ - Enrolling the first second factor right after sign-up or sign-in must NOT ask for the password. It was verified seconds ago and it established the session that is rendering the page: asking verifies nothing, and it teaches the visitor that a password box can appear at any moment for no stated reason, which is the habit every phishing page depends on. `sec-2fa-reauth-prompt` blocks it.
60
+ - What proves the enrollment is the CODE from the authenticator app, not the password. Show the secret and its QR once, require one valid code before the factor counts as active, then issue single-use recovery codes and show them once.
61
+ - Re-authentication is a real control, and it is a question about FRESHNESS rather than about the action. Record `authenticatedAt` when the session is created, treat it as fresh for 5 to 15 minutes, and ask again only past that window - GitHub's sudo mode, and Google's reauth, are exactly this.
62
+ - Guard these with a fresh session, and only these: changing the password or the email, disabling 2FA or removing a factor, viewing or regenerating recovery codes, issuing an API token, changing payout or billing details, and deleting the account.
63
+ - When the window has passed, prefer stepping up with the SECOND factor or a passkey over the password: it is the stronger proof, and it is the one an attacker holding a stolen password does not have.
64
+ - The same rule applies to every other credential in a flow. Do not ask for the email again on the screen that just verified it, and do not ask a user to retype a password they entered on the previous step of the same wizard. A form that re-asks for what the flow already holds is a bug in the flow, not a security control.
65
+
57
66
  ### Password recovery is part of every password login
58
67
 
59
68
  - 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.
@@ -3,7 +3,7 @@
3
3
  "version": "1.4.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), credential flows with breach-checked passwords (Have I Been Pwned), OWASP Top 10, transport/crypto baseline, cookies and consent, secure logging, and agent/MCP/tool-use safety.",
6
- "updated": "2026-08-19T12:37:59+02:00",
7
- "cliVersion": "1.42.0",
8
- "sha": "0270c33d2b4559e831aec794c188d293d26fc17e2a8064a967d716ceb3a7bab6"
6
+ "updated": "2026-09-05T02:46:46+02:00",
7
+ "cliVersion": "1.43.0",
8
+ "sha": "d64fe3a4a54f27a71e3066875ee949500f170d04da5c548ef605268a12102afb"
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-08-03T00:16:54+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "a49f6166f57d734e0f13487332da37903e925f32257447856af0fbd6a5f1dc84"
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-08-02T04:15:47+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "4c2197954135dd3375f32d839238b82a95c0e93e976ddf5a9cbc4a0cf0146e97"
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-08-19T12:37:59+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "a5a228a302f50d975a947b9d84676b4c2a4780f2b222f819388cc98d6a12f207"
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.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Strict frontend + backend schema validation, normalization before validation (shared normalizers: trim, lowercase email, capitalize names, canonicalize links and handles), real-time error timing (an emptied or not-yet-filled field is incomplete, not invalid: a `*` and an aria-disabled submit instead of \"not valid\" over an empty input), schema consistency, and safe client-facing error handling.",
6
6
  "updated": "2026-08-11T17:10:56+02:00",
7
- "cliVersion": "1.42.0",
7
+ "cliVersion": "1.43.0",
8
8
  "sha": "24928a4c9eed28ebede9f23f746b09a9d3d8176f8fcca61a7780a853fa5bed4e"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "enigma-darwin-arm64": "5bc00861d203812701691f36281f83f8de66fa4cb67380d5aca1302b35c8d787",
3
- "enigma-linux-arm64": "b3d46e8c74883b698af6d1859657cd56a515620ae4b580aaf2fe467f34d7480e",
4
- "enigma-linux-x64": "61f748d1288b28df6e21df65c40cdf2de2f13b0573ab010d87e96f9231df1fb7",
5
- "enigma-win32-x64.exe": "f232b982a7f552b09487aa386931f2f76693599c83d5d2017f9f6283faf66d37"
2
+ "enigma-darwin-arm64": "d73a3b756085651051cdc4837fe8dc84534c5cbba183d0ad0287fe83ef6f719d",
3
+ "enigma-linux-arm64": "48fe283cb9b9f97d0324779d144229594e47f760cc44311fcc67c3f40651a089",
4
+ "enigma-linux-x64": "9069846ed7b4293a1fb3c559cff2d08509efd79fa45d27f280175626b1d3497e",
5
+ "enigma-win32-x64.exe": "211b6228e930ab760b231badc6d9042a04fece9a731802a569403729dcf10d76"
6
6
  }
@@ -1063,6 +1063,114 @@ var BUILTIN_RULES = [
1063
1063
  severity: "block",
1064
1064
  skill: "frontend-policy"
1065
1065
  },
1066
+ {
1067
+ id: "sec-2fa-reauth-prompt",
1068
+ label: "A credential just proven is not asked for again",
1069
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js", "*.py", "*.rb", "*.php"],
1070
+ excludeFiles: [
1071
+ "*.test.*",
1072
+ "*.spec.*",
1073
+ "**/tests/**",
1074
+ "**/__tests__/**",
1075
+ "**/fixtures/**",
1076
+ "*.min.js",
1077
+ "**/dist/**",
1078
+ "**/build/**",
1079
+ "**/node_modules/**",
1080
+ "**/vendor/**",
1081
+ "dist/**",
1082
+ "build/**",
1083
+ "node_modules/**",
1084
+ "vendor/**"
1085
+ ],
1086
+ scope: "file",
1087
+ stage: "diff",
1088
+ fileCheck: "sec-2fa-reauth-prompt",
1089
+ message: "Turning 2FA on asks for the password the visitor typed a moment ago. Nothing is verified by asking: the session was established by that exact credential during sign-up or sign-in, so the answer is already known - and what the prompt teaches is that a password box can appear at any moment for no stated reason, which is the habit every phishing page relies on. The step that proves the factor works is the CODE from the authenticator, not the password. Re-authentication is a real control and this is not it: it belongs to a sensitive change on a session that is no longer FRESH, so record `authenticatedAt` when the session is created and treat it as fresh for 5-15 minutes; ask again only past that window, and prefer the second factor or a passkey over the password when you do. The actions worth guarding that way are changing the password or the email, disabling 2FA or removing a factor, regenerating recovery codes, issuing an API token, changing payout details and deleting the account. Mark the line `enigma:allow-2fa-password-prompt` where a policy genuinely requires the prompt (security-policy).",
1090
+ severity: "block",
1091
+ skill: "security-policy"
1092
+ },
1093
+ {
1094
+ id: "fe-select-hand-rolled",
1095
+ label: "Custom selects come from the primitive",
1096
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
1097
+ excludeFiles: [
1098
+ "*.test.*",
1099
+ "*.spec.*",
1100
+ "**/tests/**",
1101
+ "**/__tests__/**",
1102
+ "**/fixtures/**",
1103
+ "*.min.js",
1104
+ "**/dist/**",
1105
+ "**/build/**",
1106
+ "**/node_modules/**",
1107
+ "**/vendor/**",
1108
+ "dist/**",
1109
+ "build/**",
1110
+ "node_modules/**",
1111
+ "vendor/**"
1112
+ ],
1113
+ scope: "file",
1114
+ stage: "diff",
1115
+ fileCheck: "fe-select-hand-rolled",
1116
+ message: "A select written by hand. Replacing the native one is often right - it cannot hold an icon, a second line or a tag, and its popup is drawn by the OS - but what replaces it has to do everything the platform was doing for free, and the panel is the part that gets written. Missing every time: a typeahead (one letter walks the rows that start with it, a longer buffer refines), a disabled row that is listed and announced but that the arrow keys walk PAST rather than onto, a highlight that the pointer and the keyboard share (two highlights on screen is what makes Enter open the row the mouse is not on), a panel that measures before it opens instead of hanging off the bottom of the window, a hidden input so the form it sits in can actually submit the value, and a long list rendered a window at a time instead of 250 rows for the seven anybody sees. `enigma add select` (@enigmax/primitives) is all of it with a test each, plus a filter that turns itself on from the option count, multi-select with tags, and a baseline theme because an unstyled popup is transparent text lying on the page. Mark the line `enigma:allow-hand-rolled-select` when the listbox genuinely has to be your own (frontend-policy).",
1117
+ severity: "block",
1118
+ skill: "frontend-policy"
1119
+ },
1120
+ {
1121
+ id: "fe-toast-hand-rolled",
1122
+ label: "Toasts come from the primitive",
1123
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
1124
+ excludeFiles: [
1125
+ "*.test.*",
1126
+ "*.spec.*",
1127
+ "**/tests/**",
1128
+ "**/__tests__/**",
1129
+ "**/fixtures/**",
1130
+ "*.min.js",
1131
+ "**/dist/**",
1132
+ "**/build/**",
1133
+ "**/node_modules/**",
1134
+ "**/vendor/**",
1135
+ "dist/**",
1136
+ "build/**",
1137
+ "node_modules/**",
1138
+ "vendor/**"
1139
+ ],
1140
+ scope: "file",
1141
+ stage: "diff",
1142
+ fileCheck: "fe-toast-hand-rolled",
1143
+ message: "A toast stack written by hand: an array of messages and a `setTimeout` that removes them. The queue is the easy half; what is missing is the timing. A timer that does not PAUSE while the pointer is over the toast dismisses the one thing somebody stopped to read, and one that does not pause on a hidden tab expires every toast the moment the tab is left. A timer restarted by a re-render never fires at all, and one keyed on the array index dismisses the wrong toast as soon as two overlap. Then the stack pushes the page around as it grows, and none of it is announced, because a toast needs a live region rather than a div that appeared. `enigma add toast` (@enigmax/primitives) is the real thing - hover and focus pause it, the tab pauses it, promise toasts, actions, and one styled surface you can restyle. Mark the line `enigma:allow-hand-rolled-toast` when the stack genuinely has to be your own (frontend-policy).",
1144
+ severity: "block",
1145
+ skill: "frontend-policy"
1146
+ },
1147
+ {
1148
+ id: "fe-palette-hand-rolled",
1149
+ label: "The Ctrl+K palette comes from the primitive",
1150
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
1151
+ excludeFiles: [
1152
+ "*.test.*",
1153
+ "*.spec.*",
1154
+ "**/tests/**",
1155
+ "**/__tests__/**",
1156
+ "**/fixtures/**",
1157
+ "*.min.js",
1158
+ "**/dist/**",
1159
+ "**/build/**",
1160
+ "**/node_modules/**",
1161
+ "**/vendor/**",
1162
+ "dist/**",
1163
+ "build/**",
1164
+ "node_modules/**",
1165
+ "vendor/**"
1166
+ ],
1167
+ scope: "file",
1168
+ stage: "diff",
1169
+ fileCheck: "fe-palette-hand-rolled",
1170
+ message: "A command palette written by hand. The shortcut and the panel are an afternoon; the list underneath is where every palette breaks. A shorter result list leaves the highlight past the end, so Enter opens nothing - the single most common palette bug. The arrows stop at the last row instead of wrapping, which reads as a frozen panel. Focus moves to the row, so the caret leaves the field and a screen reader hears nothing (a row is announced through `aria-activedescendant`, with focus never leaving the input). The pointer gets its own highlight beside the keyboard's, so Enter opens the row the mouse is not on. It reopens holding the last query, which hides what an empty query is for - the recents. And dismissing hands focus to the top of the document instead of back to the trigger. `enigma add palette` (@enigmax/primitives) has each of those with a test, plus grouping whose keyboard sequence stays flat across groups, recents guarded against a storage that throws in a private window, and a scroll lock that compensates for the scrollbar. Mark the line `enigma:allow-hand-rolled-palette` when the palette genuinely has to be your own (frontend-policy).",
1171
+ severity: "block",
1172
+ skill: "frontend-policy"
1173
+ },
1066
1174
  {
1067
1175
  id: "fe-icon-action-button",
1068
1176
  label: "Repeated actions are icon buttons, not text labels",
@@ -1862,7 +1970,11 @@ var FILE_CHECKS = {
1862
1970
  "fe-new-password-affordance-on-signin": (content) => newPasswordAffordanceOnSignIn(content),
1863
1971
  "fe-context-menu-hand-rolled": (content) => handRolledContextMenu(content),
1864
1972
  "fe-selection-hand-rolled": (content) => handRolledSelection(content),
1865
- "fe-color-picker-hand-rolled": (content) => handRolledColorPicker(content)
1973
+ "fe-color-picker-hand-rolled": (content) => handRolledColorPicker(content),
1974
+ "sec-2fa-reauth-prompt": (content) => twoFactorPasswordPrompt(content),
1975
+ "fe-select-hand-rolled": (content) => handRolledSelect(content),
1976
+ "fe-toast-hand-rolled": (content) => handRolledToast(content),
1977
+ "fe-palette-hand-rolled": (content) => handRolledPalette(content)
1866
1978
  };
1867
1979
  var FIXERS = {
1868
1980
  "fe-name-input-capitalize": (line, file) => {
@@ -2168,6 +2280,77 @@ function handRolledColorPicker(content) {
2168
2280
  }
2169
2281
  return [];
2170
2282
  }
2283
+ var TWOFA_SUBJECT = /2fa|two[-_ ]?factor|totp|mfa|authenticator|otpauth/i;
2284
+ var TWOFA_ENROLL = /enroll?(?!ed)|set[-_ ]?up|activate(?!d)|enable(?!d)|provision(?!ed)/i;
2285
+ var TWOFA_PASSWORD = /type=["']password["']|\bpassword\s*[:=]|\bpassword\b\s*[,)]|currentPassword|current_password/i;
2286
+ var TWOFA_MITIGATED = /authenticatedAt|authenticated_at|reauthenticat|sudo[-_ ]?mode|sudoMode|freshSession|fresh_session|requireFresh|lastAuthAt|last_auth_at|recentlyAuthenticated|stepUp|step_up|enigma:allow-2fa-password-prompt/i;
2287
+ var TWOFA_WINDOW = 25;
2288
+ function twoFactorPasswordPrompt(content) {
2289
+ if (TWOFA_MITIGATED.test(content)) return [];
2290
+ if (!TWOFA_SUBJECT.test(content) || !TWOFA_ENROLL.test(content)) return [];
2291
+ const lines = content.split("\n");
2292
+ for (let i = 0; i < lines.length; i++) {
2293
+ const line = lines[i];
2294
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
2295
+ if (!TWOFA_SUBJECT.test(line) || !TWOFA_ENROLL.test(line)) continue;
2296
+ for (let j = Math.max(0, i - TWOFA_WINDOW); j < Math.min(lines.length, i + TWOFA_WINDOW); j++) {
2297
+ const near = lines[j];
2298
+ if (COMMENT_LINE.test(near) || /enigma:/.test(near)) continue;
2299
+ if (!TWOFA_PASSWORD.test(near)) continue;
2300
+ return [{ line: j + 1, detail: "a password field inside a first-time 2FA enrollment" }];
2301
+ }
2302
+ }
2303
+ return [];
2304
+ }
2305
+ var LISTBOX_ROLE = /role=["'{\s]*["']?(?:listbox|option)\b|aria-activedescendant/;
2306
+ var LISTBOX_OPEN = /\bset(?:Open|IsOpen|DropdownOpen|MenuOpen|Expanded)\b|\bisOpen\b|\baria-expanded\b/;
2307
+ var LISTBOX_OPTIONS = /\b(?:options|choices)\b/;
2308
+ var LISTBOX_MITIGATED = /@enigmax\/primitives|createSelect|useSelectContext|<Select|SelectRoot|radix-ui|@headlessui|downshift|react-select|@mui\/material|antd|@mantine|@chakra-ui|primereact|enigma:allow-hand-rolled-select/;
2309
+ function handRolledSelect(content) {
2310
+ if (LISTBOX_MITIGATED.test(content)) return [];
2311
+ if (!LISTBOX_ROLE.test(content) || !LISTBOX_OPEN.test(content) || !LISTBOX_OPTIONS.test(content)) return [];
2312
+ const lines = content.split("\n");
2313
+ for (let i = 0; i < lines.length; i++) {
2314
+ const line = lines[i];
2315
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
2316
+ if (!LISTBOX_ROLE.test(line)) continue;
2317
+ return [{ line: i + 1, detail: "a listbox role over an options list with its own open state" }];
2318
+ }
2319
+ return [];
2320
+ }
2321
+ var TOAST_QUEUE = /\bset(?:Toasts?|Snackbars?)\b|\btoasts\.(?:map|filter|slice)\b|\btoastQueue\b/;
2322
+ var TOAST_TIMER = /setTimeout\(/;
2323
+ var TOAST_MITIGATED = /@enigmax\/primitives|createNotifications|useNotifications|<Toaster|ToastT\b|ToasterProps\b|["']sonner["']|react-hot-toast|react-toastify|notistack|@radix-ui|@mui\/material|@chakra-ui|@mantine|antd|enigma:allow-hand-rolled-toast/;
2324
+ function handRolledToast(content) {
2325
+ if (TOAST_MITIGATED.test(content)) return [];
2326
+ if (!TOAST_QUEUE.test(content) || !TOAST_TIMER.test(content)) return [];
2327
+ const lines = content.split("\n");
2328
+ for (let i = 0; i < lines.length; i++) {
2329
+ const line = lines[i];
2330
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
2331
+ if (!TOAST_QUEUE.test(line)) continue;
2332
+ return [{ line: i + 1, detail: "a queue of toasts dismissed by a timer" }];
2333
+ }
2334
+ return [];
2335
+ }
2336
+ var PALETTE_SHORTCUT = /key\s*===?\s*["'`][kK]["'`]/;
2337
+ var PALETTE_MODIFIER = /metaKey|ctrlKey/;
2338
+ var PALETTE_OPEN = /\bset(?:Open|IsOpen|PaletteOpen|CommandOpen|Palette)\b/;
2339
+ var PALETTE_FILTER = /\bquery\b|\bfilter\(|\bsearch\b/i;
2340
+ var PALETTE_MITIGATED = /@enigmax\/primitives|isPaletteShortcut|usePaletteContext|<SearchPalette|PaletteRoot|["']cmdk["']|\bkbar\b|enigma:allow-hand-rolled-palette/;
2341
+ function handRolledPalette(content) {
2342
+ if (PALETTE_MITIGATED.test(content)) return [];
2343
+ if (!PALETTE_SHORTCUT.test(content) || !PALETTE_MODIFIER.test(content)) return [];
2344
+ if (!PALETTE_OPEN.test(content) || !PALETTE_FILTER.test(content)) return [];
2345
+ const lines = content.split("\n");
2346
+ for (let i = 0; i < lines.length; i++) {
2347
+ const line = lines[i];
2348
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
2349
+ if (!PALETTE_SHORTCUT.test(line)) continue;
2350
+ return [{ line: i + 1, detail: "Ctrl/Cmd+K opening a filtered panel" }];
2351
+ }
2352
+ return [];
2353
+ }
2171
2354
  var CLIP_ONE_LINE = /\btruncate\b|\btext-ellipsis\b|text-overflow\s*:\s*ellipsis/;
2172
2355
  var DYNAMIC_CHILD = /(?<![=!<>-])>\s*\{(?![#/:@])/;
2173
2356
  var VALUE_REACHABLE = /\btitle\s*=|aria-label\s*=|<\s*Tooltip|TooltipTrigger|data-tooltip|hoverCard|HoverCard/i;
@@ -2747,7 +2930,10 @@ export {
2747
2930
  formatFindings,
2748
2931
  handRolledColorPicker,
2749
2932
  handRolledContextMenu,
2933
+ handRolledPalette,
2934
+ handRolledSelect,
2750
2935
  handRolledSelection,
2936
+ handRolledToast,
2751
2937
  loadRules,
2752
2938
  missingPathAlias,
2753
2939
  missingWindowsHide,
@@ -2765,6 +2951,7 @@ export {
2765
2951
  summarizeLedger,
2766
2952
  textareaSizeBounds,
2767
2953
  truncatedValueUnreachable,
2954
+ twoFactorPasswordPrompt,
2768
2955
  unboundedRemoteList,
2769
2956
  viewBlankedWhileLoading,
2770
2957
  wideNamedImports
package/dist/post-edit.js CHANGED
@@ -569,7 +569,10 @@ __export(guardrails_exports, {
569
569
  formatFindings: () => formatFindings,
570
570
  handRolledColorPicker: () => handRolledColorPicker,
571
571
  handRolledContextMenu: () => handRolledContextMenu,
572
+ handRolledPalette: () => handRolledPalette,
573
+ handRolledSelect: () => handRolledSelect,
572
574
  handRolledSelection: () => handRolledSelection,
575
+ handRolledToast: () => handRolledToast,
573
576
  loadRules: () => loadRules,
574
577
  missingPathAlias: () => missingPathAlias,
575
578
  missingWindowsHide: () => missingWindowsHide,
@@ -587,6 +590,7 @@ __export(guardrails_exports, {
587
590
  summarizeLedger: () => summarizeLedger,
588
591
  textareaSizeBounds: () => textareaSizeBounds,
589
592
  truncatedValueUnreachable: () => truncatedValueUnreachable,
593
+ twoFactorPasswordPrompt: () => twoFactorPasswordPrompt,
590
594
  unboundedRemoteList: () => unboundedRemoteList,
591
595
  viewBlankedWhileLoading: () => viewBlankedWhileLoading,
592
596
  wideNamedImports: () => wideNamedImports
@@ -841,6 +845,60 @@ function handRolledColorPicker(content) {
841
845
  }
842
846
  return [];
843
847
  }
848
+ function twoFactorPasswordPrompt(content) {
849
+ if (TWOFA_MITIGATED.test(content)) return [];
850
+ if (!TWOFA_SUBJECT.test(content) || !TWOFA_ENROLL.test(content)) return [];
851
+ const lines = content.split("\n");
852
+ for (let i = 0; i < lines.length; i++) {
853
+ const line = lines[i];
854
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
855
+ if (!TWOFA_SUBJECT.test(line) || !TWOFA_ENROLL.test(line)) continue;
856
+ for (let j = Math.max(0, i - TWOFA_WINDOW); j < Math.min(lines.length, i + TWOFA_WINDOW); j++) {
857
+ const near = lines[j];
858
+ if (COMMENT_LINE.test(near) || /enigma:/.test(near)) continue;
859
+ if (!TWOFA_PASSWORD.test(near)) continue;
860
+ return [{ line: j + 1, detail: "a password field inside a first-time 2FA enrollment" }];
861
+ }
862
+ }
863
+ return [];
864
+ }
865
+ function handRolledSelect(content) {
866
+ if (LISTBOX_MITIGATED.test(content)) return [];
867
+ if (!LISTBOX_ROLE.test(content) || !LISTBOX_OPEN.test(content) || !LISTBOX_OPTIONS.test(content)) return [];
868
+ const lines = content.split("\n");
869
+ for (let i = 0; i < lines.length; i++) {
870
+ const line = lines[i];
871
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
872
+ if (!LISTBOX_ROLE.test(line)) continue;
873
+ return [{ line: i + 1, detail: "a listbox role over an options list with its own open state" }];
874
+ }
875
+ return [];
876
+ }
877
+ function handRolledToast(content) {
878
+ if (TOAST_MITIGATED.test(content)) return [];
879
+ if (!TOAST_QUEUE.test(content) || !TOAST_TIMER.test(content)) return [];
880
+ const lines = content.split("\n");
881
+ for (let i = 0; i < lines.length; i++) {
882
+ const line = lines[i];
883
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
884
+ if (!TOAST_QUEUE.test(line)) continue;
885
+ return [{ line: i + 1, detail: "a queue of toasts dismissed by a timer" }];
886
+ }
887
+ return [];
888
+ }
889
+ function handRolledPalette(content) {
890
+ if (PALETTE_MITIGATED.test(content)) return [];
891
+ if (!PALETTE_SHORTCUT.test(content) || !PALETTE_MODIFIER.test(content)) return [];
892
+ if (!PALETTE_OPEN.test(content) || !PALETTE_FILTER.test(content)) return [];
893
+ const lines = content.split("\n");
894
+ for (let i = 0; i < lines.length; i++) {
895
+ const line = lines[i];
896
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
897
+ if (!PALETTE_SHORTCUT.test(line)) continue;
898
+ return [{ line: i + 1, detail: "Ctrl/Cmd+K opening a filtered panel" }];
899
+ }
900
+ return [];
901
+ }
844
902
  function truncatedValueUnreachable(content) {
845
903
  const lines = content.split("\n");
846
904
  const out = [];
@@ -1376,7 +1434,7 @@ enigma-guardrails: BLOCKED - ${r.blocks.length} convention violation(s):`);
1376
1434
  console.log(`enigma-guardrails: ${r.count} ${all ? "tracked" : "staged"} file(s) checked, no blocking violations.`);
1377
1435
  return 0;
1378
1436
  }
1379
- var COMMENT_LINE, SKELETON_GUARD_SRC, SKELETON_SIGNAL_SRC, BUILTIN_RULES, PROJECT_CHECKS, CURRENT_PASSWORD, NEW_PASSWORD, NEW_PASSWORD_PROP, NOT_A_JSX_PROP, FILE_CHECKS, FIXERS, SPAWNERS, MUTATING_REQUEST, ENTITY_WRITE, OPTIMISTIC_SIGNAL, ALLOW_SERVER_FIRST, RESULT_BINDING, BLOCK_LOOKBACK, BLOCK_LOOKAHEAD, TEXTAREA, TEXTAREA_LOWER, TEXTAREA_UPPER, TEXTAREA_FIXED, TEXTAREA_AUTOSIZE, VIEW_GUARD, LOADING_NAME, LOADING_VALUE, VIEW_GUARD_LOADING, VIEW_CHROME_HEADING, VIEW_CHROME_TEXT, VIEW_BODY_LOOKAHEAD, ALLOW_VIEW_SKELETON, SKELETON_GUARD, SKELETON_SIGNAL, REMOTE_COLLECTION_BINDINGS, LIST_BOUNDED, ALLOW_UNBOUNDED_LIST, MENU_HANDLER, MENU_POINT, MENU_STATE, MENU_MITIGATED, SELECT_RANGE_KEY, SELECT_ADD_KEY, SELECT_STATE, SELECT_MITIGATED, COLOR_MATHS, COLOR_DRAG, COLOR_MEASURE, COLOR_STATE, COLOR_MITIGATED, CLIP_ONE_LINE, DYNAMIC_CHILD, VALUE_REACHABLE, WRAPPER_LOOKBACK, ALLOW_CLIPPED_VALUE, CLIPPED_SIMPLE_VALUE, NOT_TEXT_BINDING, ASYNC_ROUTE, SERVER_DATA_AWAIT, STREAM_BOUNDARY, ALLOW_BLOCKING_PAGE, LOADING_BOUNDARY_FILE, SPECIFIER, MODULE_EXT, JS_EXT, tsconfigCache, NAMED_IMPORT, GENERIC_ACCOUNTS, INTERNAL_MODULE, globCache, DIFF_HUNK, LEDGER_MAX_BYTES, LEDGER_KEEP, LEDGER_KEEP_REPLY, grEntry, isGrEntry;
1437
+ var COMMENT_LINE, SKELETON_GUARD_SRC, SKELETON_SIGNAL_SRC, BUILTIN_RULES, PROJECT_CHECKS, CURRENT_PASSWORD, NEW_PASSWORD, NEW_PASSWORD_PROP, NOT_A_JSX_PROP, FILE_CHECKS, FIXERS, SPAWNERS, MUTATING_REQUEST, ENTITY_WRITE, OPTIMISTIC_SIGNAL, ALLOW_SERVER_FIRST, RESULT_BINDING, BLOCK_LOOKBACK, BLOCK_LOOKAHEAD, TEXTAREA, TEXTAREA_LOWER, TEXTAREA_UPPER, TEXTAREA_FIXED, TEXTAREA_AUTOSIZE, VIEW_GUARD, LOADING_NAME, LOADING_VALUE, VIEW_GUARD_LOADING, VIEW_CHROME_HEADING, VIEW_CHROME_TEXT, VIEW_BODY_LOOKAHEAD, ALLOW_VIEW_SKELETON, SKELETON_GUARD, SKELETON_SIGNAL, REMOTE_COLLECTION_BINDINGS, LIST_BOUNDED, ALLOW_UNBOUNDED_LIST, MENU_HANDLER, MENU_POINT, MENU_STATE, MENU_MITIGATED, SELECT_RANGE_KEY, SELECT_ADD_KEY, SELECT_STATE, SELECT_MITIGATED, COLOR_MATHS, COLOR_DRAG, COLOR_MEASURE, COLOR_STATE, COLOR_MITIGATED, TWOFA_SUBJECT, TWOFA_ENROLL, TWOFA_PASSWORD, TWOFA_MITIGATED, TWOFA_WINDOW, LISTBOX_ROLE, LISTBOX_OPEN, LISTBOX_OPTIONS, LISTBOX_MITIGATED, TOAST_QUEUE, TOAST_TIMER, TOAST_MITIGATED, PALETTE_SHORTCUT, PALETTE_MODIFIER, PALETTE_OPEN, PALETTE_FILTER, PALETTE_MITIGATED, CLIP_ONE_LINE, DYNAMIC_CHILD, VALUE_REACHABLE, WRAPPER_LOOKBACK, ALLOW_CLIPPED_VALUE, CLIPPED_SIMPLE_VALUE, NOT_TEXT_BINDING, ASYNC_ROUTE, SERVER_DATA_AWAIT, STREAM_BOUNDARY, ALLOW_BLOCKING_PAGE, LOADING_BOUNDARY_FILE, SPECIFIER, MODULE_EXT, JS_EXT, tsconfigCache, NAMED_IMPORT, GENERIC_ACCOUNTS, INTERNAL_MODULE, globCache, DIFF_HUNK, LEDGER_MAX_BYTES, LEDGER_KEEP, LEDGER_KEEP_REPLY, grEntry, isGrEntry;
1380
1438
  var init_guardrails = __esm({
1381
1439
  "src/guardrails.ts"() {
1382
1440
  "use strict";
@@ -2437,6 +2495,114 @@ var init_guardrails = __esm({
2437
2495
  severity: "block",
2438
2496
  skill: "frontend-policy"
2439
2497
  },
2498
+ {
2499
+ id: "sec-2fa-reauth-prompt",
2500
+ label: "A credential just proven is not asked for again",
2501
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js", "*.py", "*.rb", "*.php"],
2502
+ excludeFiles: [
2503
+ "*.test.*",
2504
+ "*.spec.*",
2505
+ "**/tests/**",
2506
+ "**/__tests__/**",
2507
+ "**/fixtures/**",
2508
+ "*.min.js",
2509
+ "**/dist/**",
2510
+ "**/build/**",
2511
+ "**/node_modules/**",
2512
+ "**/vendor/**",
2513
+ "dist/**",
2514
+ "build/**",
2515
+ "node_modules/**",
2516
+ "vendor/**"
2517
+ ],
2518
+ scope: "file",
2519
+ stage: "diff",
2520
+ fileCheck: "sec-2fa-reauth-prompt",
2521
+ message: "Turning 2FA on asks for the password the visitor typed a moment ago. Nothing is verified by asking: the session was established by that exact credential during sign-up or sign-in, so the answer is already known - and what the prompt teaches is that a password box can appear at any moment for no stated reason, which is the habit every phishing page relies on. The step that proves the factor works is the CODE from the authenticator, not the password. Re-authentication is a real control and this is not it: it belongs to a sensitive change on a session that is no longer FRESH, so record `authenticatedAt` when the session is created and treat it as fresh for 5-15 minutes; ask again only past that window, and prefer the second factor or a passkey over the password when you do. The actions worth guarding that way are changing the password or the email, disabling 2FA or removing a factor, regenerating recovery codes, issuing an API token, changing payout details and deleting the account. Mark the line `enigma:allow-2fa-password-prompt` where a policy genuinely requires the prompt (security-policy).",
2522
+ severity: "block",
2523
+ skill: "security-policy"
2524
+ },
2525
+ {
2526
+ id: "fe-select-hand-rolled",
2527
+ label: "Custom selects come from the primitive",
2528
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
2529
+ excludeFiles: [
2530
+ "*.test.*",
2531
+ "*.spec.*",
2532
+ "**/tests/**",
2533
+ "**/__tests__/**",
2534
+ "**/fixtures/**",
2535
+ "*.min.js",
2536
+ "**/dist/**",
2537
+ "**/build/**",
2538
+ "**/node_modules/**",
2539
+ "**/vendor/**",
2540
+ "dist/**",
2541
+ "build/**",
2542
+ "node_modules/**",
2543
+ "vendor/**"
2544
+ ],
2545
+ scope: "file",
2546
+ stage: "diff",
2547
+ fileCheck: "fe-select-hand-rolled",
2548
+ message: "A select written by hand. Replacing the native one is often right - it cannot hold an icon, a second line or a tag, and its popup is drawn by the OS - but what replaces it has to do everything the platform was doing for free, and the panel is the part that gets written. Missing every time: a typeahead (one letter walks the rows that start with it, a longer buffer refines), a disabled row that is listed and announced but that the arrow keys walk PAST rather than onto, a highlight that the pointer and the keyboard share (two highlights on screen is what makes Enter open the row the mouse is not on), a panel that measures before it opens instead of hanging off the bottom of the window, a hidden input so the form it sits in can actually submit the value, and a long list rendered a window at a time instead of 250 rows for the seven anybody sees. `enigma add select` (@enigmax/primitives) is all of it with a test each, plus a filter that turns itself on from the option count, multi-select with tags, and a baseline theme because an unstyled popup is transparent text lying on the page. Mark the line `enigma:allow-hand-rolled-select` when the listbox genuinely has to be your own (frontend-policy).",
2549
+ severity: "block",
2550
+ skill: "frontend-policy"
2551
+ },
2552
+ {
2553
+ id: "fe-toast-hand-rolled",
2554
+ label: "Toasts come from the primitive",
2555
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
2556
+ excludeFiles: [
2557
+ "*.test.*",
2558
+ "*.spec.*",
2559
+ "**/tests/**",
2560
+ "**/__tests__/**",
2561
+ "**/fixtures/**",
2562
+ "*.min.js",
2563
+ "**/dist/**",
2564
+ "**/build/**",
2565
+ "**/node_modules/**",
2566
+ "**/vendor/**",
2567
+ "dist/**",
2568
+ "build/**",
2569
+ "node_modules/**",
2570
+ "vendor/**"
2571
+ ],
2572
+ scope: "file",
2573
+ stage: "diff",
2574
+ fileCheck: "fe-toast-hand-rolled",
2575
+ message: "A toast stack written by hand: an array of messages and a `setTimeout` that removes them. The queue is the easy half; what is missing is the timing. A timer that does not PAUSE while the pointer is over the toast dismisses the one thing somebody stopped to read, and one that does not pause on a hidden tab expires every toast the moment the tab is left. A timer restarted by a re-render never fires at all, and one keyed on the array index dismisses the wrong toast as soon as two overlap. Then the stack pushes the page around as it grows, and none of it is announced, because a toast needs a live region rather than a div that appeared. `enigma add toast` (@enigmax/primitives) is the real thing - hover and focus pause it, the tab pauses it, promise toasts, actions, and one styled surface you can restyle. Mark the line `enigma:allow-hand-rolled-toast` when the stack genuinely has to be your own (frontend-policy).",
2576
+ severity: "block",
2577
+ skill: "frontend-policy"
2578
+ },
2579
+ {
2580
+ id: "fe-palette-hand-rolled",
2581
+ label: "The Ctrl+K palette comes from the primitive",
2582
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
2583
+ excludeFiles: [
2584
+ "*.test.*",
2585
+ "*.spec.*",
2586
+ "**/tests/**",
2587
+ "**/__tests__/**",
2588
+ "**/fixtures/**",
2589
+ "*.min.js",
2590
+ "**/dist/**",
2591
+ "**/build/**",
2592
+ "**/node_modules/**",
2593
+ "**/vendor/**",
2594
+ "dist/**",
2595
+ "build/**",
2596
+ "node_modules/**",
2597
+ "vendor/**"
2598
+ ],
2599
+ scope: "file",
2600
+ stage: "diff",
2601
+ fileCheck: "fe-palette-hand-rolled",
2602
+ message: "A command palette written by hand. The shortcut and the panel are an afternoon; the list underneath is where every palette breaks. A shorter result list leaves the highlight past the end, so Enter opens nothing - the single most common palette bug. The arrows stop at the last row instead of wrapping, which reads as a frozen panel. Focus moves to the row, so the caret leaves the field and a screen reader hears nothing (a row is announced through `aria-activedescendant`, with focus never leaving the input). The pointer gets its own highlight beside the keyboard's, so Enter opens the row the mouse is not on. It reopens holding the last query, which hides what an empty query is for - the recents. And dismissing hands focus to the top of the document instead of back to the trigger. `enigma add palette` (@enigmax/primitives) has each of those with a test, plus grouping whose keyboard sequence stays flat across groups, recents guarded against a storage that throws in a private window, and a scroll lock that compensates for the scrollbar. Mark the line `enigma:allow-hand-rolled-palette` when the palette genuinely has to be your own (frontend-policy).",
2603
+ severity: "block",
2604
+ skill: "frontend-policy"
2605
+ },
2440
2606
  {
2441
2607
  id: "fe-icon-action-button",
2442
2608
  label: "Repeated actions are icon buttons, not text labels",
@@ -3224,7 +3390,11 @@ var init_guardrails = __esm({
3224
3390
  "fe-new-password-affordance-on-signin": (content) => newPasswordAffordanceOnSignIn(content),
3225
3391
  "fe-context-menu-hand-rolled": (content) => handRolledContextMenu(content),
3226
3392
  "fe-selection-hand-rolled": (content) => handRolledSelection(content),
3227
- "fe-color-picker-hand-rolled": (content) => handRolledColorPicker(content)
3393
+ "fe-color-picker-hand-rolled": (content) => handRolledColorPicker(content),
3394
+ "sec-2fa-reauth-prompt": (content) => twoFactorPasswordPrompt(content),
3395
+ "fe-select-hand-rolled": (content) => handRolledSelect(content),
3396
+ "fe-toast-hand-rolled": (content) => handRolledToast(content),
3397
+ "fe-palette-hand-rolled": (content) => handRolledPalette(content)
3228
3398
  };
3229
3399
  FIXERS = {
3230
3400
  "fe-name-input-capitalize": (line, file) => {
@@ -3297,6 +3467,23 @@ var init_guardrails = __esm({
3297
3467
  COLOR_MEASURE = /getBoundingClientRect/;
3298
3468
  COLOR_STATE = /\bset(?:Color|Colour|Hue|Saturation|Lightness|Brightness|Alpha|Opacity)\b|\b(?:hue|saturation)\s*[:=]/i;
3299
3469
  COLOR_MITIGATED = /@enigmax\/primitives|type=["']color["']|react-colorful|react-color|@uiw\/react-color|vue-color|ngx-color|@radix-ui|enigma:allow-hand-rolled-color-picker/;
3470
+ TWOFA_SUBJECT = /2fa|two[-_ ]?factor|totp|mfa|authenticator|otpauth/i;
3471
+ TWOFA_ENROLL = /enroll?(?!ed)|set[-_ ]?up|activate(?!d)|enable(?!d)|provision(?!ed)/i;
3472
+ TWOFA_PASSWORD = /type=["']password["']|\bpassword\s*[:=]|\bpassword\b\s*[,)]|currentPassword|current_password/i;
3473
+ TWOFA_MITIGATED = /authenticatedAt|authenticated_at|reauthenticat|sudo[-_ ]?mode|sudoMode|freshSession|fresh_session|requireFresh|lastAuthAt|last_auth_at|recentlyAuthenticated|stepUp|step_up|enigma:allow-2fa-password-prompt/i;
3474
+ TWOFA_WINDOW = 25;
3475
+ LISTBOX_ROLE = /role=["'{\s]*["']?(?:listbox|option)\b|aria-activedescendant/;
3476
+ LISTBOX_OPEN = /\bset(?:Open|IsOpen|DropdownOpen|MenuOpen|Expanded)\b|\bisOpen\b|\baria-expanded\b/;
3477
+ LISTBOX_OPTIONS = /\b(?:options|choices)\b/;
3478
+ LISTBOX_MITIGATED = /@enigmax\/primitives|createSelect|useSelectContext|<Select|SelectRoot|radix-ui|@headlessui|downshift|react-select|@mui\/material|antd|@mantine|@chakra-ui|primereact|enigma:allow-hand-rolled-select/;
3479
+ TOAST_QUEUE = /\bset(?:Toasts?|Snackbars?)\b|\btoasts\.(?:map|filter|slice)\b|\btoastQueue\b/;
3480
+ TOAST_TIMER = /setTimeout\(/;
3481
+ TOAST_MITIGATED = /@enigmax\/primitives|createNotifications|useNotifications|<Toaster|ToastT\b|ToasterProps\b|["']sonner["']|react-hot-toast|react-toastify|notistack|@radix-ui|@mui\/material|@chakra-ui|@mantine|antd|enigma:allow-hand-rolled-toast/;
3482
+ PALETTE_SHORTCUT = /key\s*===?\s*["'`][kK]["'`]/;
3483
+ PALETTE_MODIFIER = /metaKey|ctrlKey/;
3484
+ PALETTE_OPEN = /\bset(?:Open|IsOpen|PaletteOpen|CommandOpen|Palette)\b/;
3485
+ PALETTE_FILTER = /\bquery\b|\bfilter\(|\bsearch\b/i;
3486
+ PALETTE_MITIGATED = /@enigmax\/primitives|isPaletteShortcut|usePaletteContext|<SearchPalette|PaletteRoot|["']cmdk["']|\bkbar\b|enigma:allow-hand-rolled-palette/;
3300
3487
  CLIP_ONE_LINE = /\btruncate\b|\btext-ellipsis\b|text-overflow\s*:\s*ellipsis/;
3301
3488
  DYNAMIC_CHILD = /(?<![=!<>-])>\s*\{(?![#/:@])/;
3302
3489
  VALUE_REACHABLE = /\btitle\s*=|aria-label\s*=|<\s*Tooltip|TooltipTrigger|data-tooltip|hoverCard|HoverCard/i;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
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": {