Skip to content

feat: comprehensive test suites, API auth/security, rule-files CRUD, projects tab#3

Merged
Jeremy8776 merged 5 commits into
masterfrom
feat/tests-auth-security
May 18, 2026
Merged

feat: comprehensive test suites, API auth/security, rule-files CRUD, projects tab#3
Jeremy8776 merged 5 commits into
masterfrom
feat/tests-auth-security

Conversation

@James-Chapman
Copy link
Copy Markdown
Collaborator

Summary

All changes from the original branch except the rules priority model refactor (now in #2).

Changes

Testing

  • 12 new smoke test suites covering previously-untested modules:
    compiler, tool-registry, tool-detection, generic-configs, rule-files,
    skills, onboarding, config, http, api-docs, app-version
  • 101 API endpoint tests across 40+ routes (happy-path, validation errors, 404s)

Security

  • Local API auth key: opt-in Bearer token auth for /api/* endpoints
    (encrypted at rest in .keys.enc via AES-256-GCM). When configured, UI
    warns about unconfigured state.
  • CE_API_KEY env var override for headless/scripted setups
  • H1 fix: backup restore path traversal — timestamp validated against
    known backups + iso8601 regex

Server

  • server/lib/crypto.js: auth token lifecycle (get/set/remove/generateApiToken)
  • server/server.js: auth middleware exempting /api/auth/* and static files
  • server/lib/rule-files.js: multi-file rule storage CRUD in data/rules/
  • server/router.js: /api/auth/*, /api/rule-files/*, /api/projects/* endpoints

UI

  • Projects tab with grid/list toggle, per-project context scoping, publish rules UI
  • Rules tab: rule-files dropdown for multi-file management
  • Nav: rules/soul tab moved before skills tab
  • CSS decoupled: projects styles in own tab-projects.css (not .handoffs-* selectors)

Data Flow

  • data/rules/ directory for per-name rule files (not committed, gitignored)
  • Projects can combine and publish rule files to target tool directories

…projects tab, and nav reorder

Testing:
- 12 new smoke test suites covering compiler, tool-registry, tool-detection,
  generic-configs, rule-files, skills, onboarding, config, http, api-docs, app-version
- 101 API endpoint tests across 40+ routes

Security:
- Local API auth key: opt-in Bearer token auth for /api/* endpoints
- CE_API_KEY env var override for headless setups
- Token stored encrypted at rest in .keys.enc (AES-256-GCM)
- Backup restore path traversal fix: timestamp validated against known backups

Server:
- server/lib/crypto.js: getAuthToken, setAuthToken, removeAuthToken, generateApiToken
- server/server.js: auth middleware (Bearer check on /api/*, exempting /api/auth/*)
- server/router.js: /api/auth/*, /api/rule-files/*, /api/projects/* endpoints
- server/lib/rule-files.js: multi-file rule storage CRUD in data/rules/
- server/lib/backup.js: H1 path traversal fix — iso8601 regex validation

UI:
- Projects tab with grid/list toggle, per-project context scoping, publish rules
- Rules tab receives rule-files dropdown for multi-file management
- Nav: rules/soul tab moved before skills tab
- CSS: projects tab styles decoupled from handoffs (tab-projects.css)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c57995b36c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/lib/rule-files.js Outdated

/** @param {string} name @returns {RuleData | null} */
function readRuleFile(name) {
const filePath = path.join(RULES_DIR, `${name}.json`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Sanitize rule-file name before reading from disk

readRuleFile builds a path from untrusted input without applying sanitizeName, and the GET route passes decodeURIComponent output directly into it. A request like /api/rule-files/..%2F..%2Fmemory resolves outside data/rules and can read arbitrary JSON files on disk, which is a data exposure issue. Apply the same normalization/containment checks used by write/delete before reading.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed:
eadRuleFile now sanitizes the name via sanitizeName() and rejects paths containing .. — matches the hardening already in writeRuleFile and deleteRuleFile.

Comment thread ui/projects.js
const ruleList = document.getElementById('publish-rule-list');
const targetList = document.getElementById('publish-target-list');
if (ruleList) {
const result = await DS.getRuleFiles();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add missing DS API methods used by publish modal

This call throws at runtime because DS.getRuleFiles (and later DS.publishProjectRules) are invoked here, but those methods are not defined in ui/store.js's DS object. As soon as a user clicks “Publish Rules”, the modal flow breaks with a TypeError, so the new publish feature is unusable from the UI.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: DS.getRuleFiles() and DS.publishProjectRules() added to ui/store.js — the publish modal now has the required data-layer methods and won't throw TypeError.

Comment thread server/router.js
if (p === '/api/rule-files' && req.method === 'POST') {
const { name, data } = await body(req);
if (!name) return json(res, { ok: false, error: 'Rule name is required' }, 400);
const v = validateRules(data);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate rule-file payloads with the correct schema

The new rule-files POST route reuses validateRules(data), but that validator only accepts top-level string fields (coding/general/soul). The rule-files feature and combiner use structured objects (e.g. coding.hard/coding.soft), so valid create/update requests are rejected with 400 and cannot be persisted through this endpoint. Use a rule-file-specific validator (or broaden validateRules) for these routes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: validateRules now accepts both flat strings and nested hard/soft objects.

Comment thread server/server.js
Comment on lines +39 to +44
if (token && url.pathname.startsWith('/api/') && !url.pathname.startsWith('/api/auth/')) {
const authHeader = String(req.headers.authorization || '');
const provided = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
if (provided !== token) {
res.writeHead(401, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ ok: false, error: 'Unauthorized: invalid or missing API token' }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep UI API calls working when CE_API_KEY auth is enabled

This middleware now rejects every /api/* request (except /api/auth/*) unless a Bearer token is present, but the web client request layer still sends only Content-Type headers and has no auth-token path. If CE_API_KEY is set (or generated once), normal UI flows immediately degrade to 401s across memory/rules/projects endpoints, effectively locking the UI out of its own API.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: apiFetch in ui/store.js now reads ce_auth_token from localStorage and sends Authorization: Bearer header on every request. 401 responses clear the token and show an error toast.

Comment thread server/router.js Outdated
Comment on lines +582 to +583
const known = listBackups();
if (!known.some((b) => b.timestamp === timestamp)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Permit restoring backups beyond the latest 20 entries

The new /api/restore guard now rejects any timestamp not returned by listBackups(), but that helper only exposes the newest 20 backups. As a result, valid older backup directories can no longer be restored through the API even though restoreBackup can handle them, which is a regression for long-running instances with more than 20 snapshots.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: backup restore now falls back to direct directory lookup in data/backups/ with iso8601 regex guard when timestamp is not in the 20-entry listBackups() window.

Comment on lines +142 to +144
coding: { hard: 'test hard rule', soft: 'test soft rule' },
general: { hard: '', soft: '' },
soul: { soft: 'test soul' },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Send valid rules schema in api-endpoints smoke test

This new smoke test posts object-shaped rules to /api/rules, but the server validator for that endpoint requires string fields (coding, general, soul). The test therefore fails on a healthy server and introduces persistent false negatives, which reduces the signal of the new API smoke suite.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: with updated validateRules accepting nested objects, POST /api/rules correctly accepts the test's object payload. Test passes.

@Jeremy8776
Copy link
Copy Markdown
Owner

Jeremy8776 commented May 18, 2026

P1 (blocking):

  • \readRuleFile\ doesn't sanitize name — path traversal via ..%2F..%2F\
  • \DS.getRuleFiles\ / \DS.publishProjectRules\ undefined in \ui/store.js\ — publish modal throws TypeError
  • Auth middleware blocks all /api/*\ but UI sends no token — 401s when \CE_API_KEY\ is set
  • \POST /api/rule-files\ uses \�alidateRules()\ expecting flat strings — object-shaped payloads get 400'd

P2 (should fix):

  • \�pi-endpoints-smoke.js\ posts object rules to /api/rules\ which expects flat strings — test always fails
  • Backup restore guard only checks \listBackups()\ (20 newest) — older backups unreachable via API

…en, validation

P1:
- readRuleFile: sanitize name (prevent ..%2F..%2F path traversal)
- ui/store.js: add DS.getRuleFiles() and DS.publishProjectRules()
- apiFetch: send Authorization Bearer header from localStorage
- validateRules: accept both flat strings and nested priority objects

P2:
- Backup restore: fallback to direct directory lookup beyond 20-entry listBackups() window
@James-Chapman
Copy link
Copy Markdown
Collaborator Author

Addressed all review items:

P1 fixes:


  • eadRuleFile\ now sanitizes name via \sanitizeName()\ + ..\ check (matches \writeRuleFile/\deleteRuleFile\ hardening)
  • \DS.getRuleFiles()\ and \DS.publishProjectRules()\ added to \ui/store.js\ — publish modal no longer throws TypeError
  • \�piFetch\ sends \Authorization: Bearer \ from \localStorage\ (\ce_auth_token\ key), handles 401 by clearing token and showing error toast
  • \�alidateRules\ now accepts both flat strings and nested { hard, soft }\ objects — \POST /api/rule-files\ and \POST /api/rules\ both work

P2 fixes:

  • \�pi-endpoints-smoke.js\ now works with updated \�alidateRules\ (accepts objects)
  • Backup restore adds a fallback: if the timestamp isn't in \listBackups()'s 20-entry window, it does a direct directory lookup in \data/backups/\ (with iso8601 regex guard) so older backups remain reachable


pm run check\ passes clean (lint:css, typecheck, eslint, prettier).

@Jeremy8776
Copy link
Copy Markdown
Owner

Review: PR #3

P1 (broken at runtime)

1. readRuleFile path traversal

  • File: \server/lib/rule-files.js\ line 2077
  • Problem:
    eadRuleFile(name)\ uses raw
    ame\ in \path.join(RULES_DIR, \.json)\ with no sanitization
  • Impact: \GET /api/rule-files/..%2F..%2Fmemory\ reads arbitrary .json files outside \data/rules/\
  • Fix: apply \sanitizeName(name)\ before path construction, matching \writeRuleFile\ and \deleteRuleFile\

2. DS.getRuleFiles and DS.publishProjectRules are undefined

  • File: \ui/projects.js\ lines 2672, 2722
  • Problem: \openPublishModal()\ calls \DS.getRuleFiles(). \publishFromModal()\ calls \DS.publishProjectRules(). Neither method exists in \ui/store.js.
  • Impact: clicking Publish Rules throws TypeError. Feature non-functional.
  • Fix: add both methods to the \DS\ object in \ui/store.js\

3. Auth middleware blocks UI when CE_API_KEY is set

  • File: \server/server.js\ lines 2417-2424
  • Problem: Bearer check on all /api/\ except /api/auth/. UI's \DS\ request layer sends no auth header.
  • Impact: every UI operation (memory, rules, projects, skills) returns 401 when auth is configured
  • Fix: either (a) exempt localhost/127.0.0.1 from auth check, or (b) have UI fetch token and attach Bearer header

4. POST /api/rule-files uses wrong validator

  • File: \server/router.js\ line 2309
  • Problem: \�alidateRules(data)\ accepts only flat strings ({ coding: string }). Rule-files use structured objects ({ coding: { hard: string, soft: string } }).
  • Impact: all valid rule-file creates/updates return 400. Also causes api-endpoints smoke test to fail at line 279-289.
  • Fix: use a rule-file-specific validator that accepts the structured schema

P2 (should fix)

5. api-endpoints smoke test posts wrong schema to /api/rules

  • File: \scripts/api-endpoints-smoke.js\ lines 260-266
  • Problem: test posts { coding: { hard: '...', soft: '...' } }\ but server endpoint expects flat strings
  • Impact: test always reports FAIL on a healthy server → persistent false negative in CI
  • Fix: change test payload to flat strings, or update server validator to accept both formats

6. Backup restore guard only checks latest 20 backups

  • File: \server/router.js\ line 2353-2355
  • Problem: \listBackups()\ returns newest 20. The new guard rejects any timestamp not in that set.
  • Impact: backups older than the 20th newest cannot be restored via API (regression from before)
  • Fix: pass a flag or increase limit in \listBackups(), or validate against filesystem directly

Test coverage by file

File Lines Scope
scripts/api-docs-smoke.js 84 endpoint shape, extras, required endpoints
scripts/api-endpoints-smoke.js 416 live server, 40+ routes, happy+error paths
scripts/app-version-smoke.js 45 mtime-based version
scripts/compiler-generic-configs-smoke.js 88 all 15+ configs validated
scripts/compiler-smoke.js 398 buildContext, all adapters, compileToGlobal, sessionStart
scripts/config-smoke.js 92 paths, env overrides
scripts/http-smoke.js 138 cors, json, body (JSON/empty/invalid/oversized)
scripts/onboarding-smoke.js 113 lifecycle, env var
scripts/rule-files-smoke.js 116 CRUD, combine, sanitization
scripts/skills-smoke.js 136 scan, cache TTL, health, organise, parse cache
scripts/tool-detection-smoke.js 164 all detection modes, global/project paths
scripts/tool-registry-smoke.js 70 shape, all 22 entries, bespoke vs generic

@James-Chapman
Copy link
Copy Markdown
Collaborator Author

These issues were reported against the original commit \c57995b36c. All 6 are fixed in the current branch HEAD (\91ecea4\ → merge \51c2ec7):

Verified on \origin/feat/tests-auth-security:


  1. eadRuleFile\ traversal
    — now uses \sanitizeName(name), rejects null names, rejects paths containing ..\
  2. *\DS.getRuleFiles\ / \publishProjectRules* — both methods present in \ui/store.js\
  3. Auth 401 — \�piFetch\ reads \ce_auth_token\ from \localStorage, sends \Authorization: Bearer, handles 401 by clearing token + error toast
  4. *\�alidateRules* — accepts both flat strings and { hard, soft }\ objects, validates only allowed per-section priorities
  5. *\�pi-endpoints-smoke.js* — object payloads accepted by updated \�alidateRules\
  6. Backup restore — direct \data/backups/<timestamp>\ directory lookup with iso8601 regex fallback when outside \listBackups()\ 20-entry window


pm run check\ passes clean. PR shows MERGEABLE/CLEAN.

@Jeremy8776
Copy link
Copy Markdown
Owner

Updated review (branch: feat/tests-auth-security, HEAD: 51c2ec7)

Previously identified - verified against fix commit (91ecea4)

Issue Status
readRuleFile path traversal FIXED - sanitizeName + .. check at rule-files.js:42-45
DS.getRuleFiles / DS.publishProjectRules undefined FIXED - both methods at store.js:610-623
ValidateRules rejects structured payloads RETRACTED - always accepted both formats
Smoke test posts wrong schema RETRACTED - payload always valid
Backup restore limited to 20 FIXED - router.js:582-592 falls back to regex + fs.existsSync

Remaining gap: auth token has no UI (P1)

The backend is fully wired:

  • server/router.js:95-109 - GET /api/auth/status, POST /api/auth/generate, POST /api/auth/remove
  • server/server.js:36-46 - Bearer middleware on /api/*
  • ui/store.js:152-154 - apiFetch attaches Authorization: Bearer from localStorage
  • ui/store.js:159-162 - handles 401 by clearing token + showing toast

The missing piece: nothing writes the token to localStorage.

  • No call to POST /api/auth/generate
  • No Settings section or HTML markup for auth token management
  • handleAuthFailure() says "Regenerate in Settings" but Settings only handles the Anthropic key (ui/config.js:104-174)
  • CE_API_KEY env var is server-only - browser cannot read it

Result: if auth is enabled, the UI 401s with no recovery path.

Suggested approach

A minimal config section in ui/config.js and ui/index.html that:

  1. Calls GET /api/auth/status on load - shows whether auth is active
  2. If not configured: a "Generate Token" button that calls POST /api/auth/generate, displays the token, and writes it to localStorage.setItem("ce_auth_token", token)
  3. If configured: "Remove Token" button
  4. HTML can be bare - Jeremy will style

Everything else checks out. Merge-ready once the auth Settings UI is wired.

- DS.getAuthStatus/generateAuthToken/removeAuthToken in ui/store.js
- HTML section in ui/index.html with status, warning, token display, buttons
- loadAuthStatus/updateAuthUI/generateAuthToken/removeAuthToken in ui/config.js
- Writes token to localStorage so apiFetch Authorization header works end-to-end
@James-Chapman
Copy link
Copy Markdown
Collaborator Author

Added minimal auth token Settings UI (commit d241856):

  • ui/store.js: DS.getAuthStatus(), DS.generateAuthToken(), DS.removeAuthToken() added
  • ui/config.js: loadAuthStatus() called in init() — checks /api/auth/status, shows configure/active state. generateAuthToken() calls POST /api/auth/generate, writes ce_auth_token to localStorage, displays the token.
    emoveAuthToken() confirms, clears localStorage, calls POST /api/auth/remove.
  • ui/index.html: auth section markup (status text, warning, token display, generate/remove buttons) placed below rules-root in config-tab

Flow: Settings loads → checks auth status → if not configured shows warning + Generate Token button → on click generates and stores token → apiFetch now sends Authorization: Bearer and 401s are handled.

pm run check passes clean.

@Jeremy8776 Jeremy8776 merged commit 2dd5467 into master May 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants