npm package: @madprojx/emdash-security
Description: Security plugin for EmDash CMS — field validation, CSP management, and field-level write guards
Author: MadProjX
Latest version: 0.1.0

# @madprojx/emdash-security

Security plugin for [EmDash CMS](https://emdashcms.com). The first security-focused plugin in the EmDash ecosystem — built by a CISSP.

## What It Does

Three components, one plugin:

| Component | What it enforces |
|-----------|-----------------|
| **Field Validator** | Schema-level input validation at write time (`content:beforeSave`) |
| **Field Guard** | Field-level immutability and invariant rules (locked, create-only, allowed-values) |
| **CSP Manager** | Content Security Policy injection into public pages via `<meta http-equiv>` |

## Install

```bash
npm install @madprojx/emdash-security
```

Register in your EmDash config:

```typescript
import securityPlugin from "@madprojx/emdash-security";

export default defineConfig({
  plugins: [securityPlugin],
});
```

> **CSP Manager requires trusted plugin status.** In the EmDash admin, navigate to Plugins → @madprojx/emdash-security → mark as Trusted. This is required for the `page:fragments` hook to inject CSP meta tags.

## Field Validator

Configure validation rules per field in the EmDash admin (Plugins → Security → Field Rules), or seed them directly via the plugin KV:

```
KV key: settings:fieldRules
```

**Config format** (`collection.fieldName` or `fieldName` for all collections):

```json
{
  "posts.slug": {
    "required": true,
    "format": "slug"
  },
  "posts.title": {
    "required": true,
    "maxLength": 200
  },
  "products.url": {
    "format": "url",
    "urlAllowlist": ["example.com", "cdn.example.com"]
  },
  "contacts.email": {
    "format": "email",
    "maxLength": 254
  },
  "comments.body": {
    "maxLength": 5000,
    "blockedChars": "<>\"'"
  }
}
```

**Available rule properties:**

| Property | Type | Description |
|----------|------|-------------|
| `required` | boolean | Field must be present and non-empty |
| `maxLength` | number | Maximum string length |
| `minLength` | number | Minimum string length |
| `pattern` | string (regex) | Value must match this pattern |
| `blockedChars` | string (char class) | Characters not allowed (regex char class body) |
| `urlAllowlist` | string[] | For URL fields: only these hostnames permitted |
| `format` | `"email"` \| `"url"` \| `"slug"` \| `"uuid"` | Validates common formats |

Violations abort the save and return a `ValidationError` with the full list of violations.

## Field Guard

Enforces field-level invariants that don't require user context.

```
KV key: settings:fieldGuards
```

```json
{
  "products.price": {
    "type": "immutable"
  },
  "users.plan": {
    "type": "allowed-values",
    "allowedValues": ["free", "pro", "enterprise"]
  },
  "orders.internal_ref": {
    "type": "locked",
    "onViolation": "strip"
  },
  "posts.created_at": {
    "type": "create-only"
  }
}
```

**Guard types:**

| Type | Behavior |
|------|----------|
| `locked` | Field is always stripped from incoming writes (system-managed values) |
| `create-only` | Field can only be set when `isNew === true` |
| `allowed-values` | Value must be one of the declared set |
| `immutable` | Value cannot change after creation (requires `read:content` capability — see note) |

**`onViolation`:** `"throw"` (default) aborts the save; `"strip"` silently removes the field.

> **Note on `immutable`:** Checking the stored value against the incoming value requires fetching the existing record, which needs the `read:content` capability. This is on the roadmap. Currently, `immutable` will strip the field on updates (`onViolation: "strip"` behavior). PR welcome.

> **Note on role-based field guards:** EmDash's `content:beforeSave` event does not include user identity. Per-field write permissions based on user role (e.g., "only admin can set price") are not possible via the hook API in EmDash v0.1.0. Track [emdash-cms/emdash](https://github.com/emdash-cms/emdash/issues) for updates.

## CSP Manager

Configure CSP policies per URL path prefix.

```
KV key: settings:cspConfig
```

```json
[
  {
    "pathPrefix": "/",
    "label": "Global",
    "directives": {
      "default-src": ["'self'"],
      "script-src": ["'self'", "'unsafe-inline'"],
      "style-src": ["'self'", "'unsafe-inline'"],
      "img-src": ["'self'", "data:", "https:"],
      "font-src": ["'self'"],
      "connect-src": ["'self'"],
      "object-src": ["'none'"],
      "base-uri": ["'self'"],
      "form-action": ["'self'"],
      "upgrade-insecure-requests": true
    }
  }
]
```

More specific path prefixes override the global config. `/blog` overrides `/` for all paths starting with `/blog`.

**Built-in presets** (import and use as a starting point):

```typescript
import { CSP_PRESETS } from "@madprojx/emdash-security";

// CSP_PRESETS.strict    — default-src: 'none', locked down
// CSP_PRESETS.standard  — default-src: 'self', unsafe-inline allowed
// CSP_PRESETS.permissive — default-src: 'self' https:, for CDN-heavy sites
```

### HTTP Response Header CSP (Advanced)

`<meta http-equiv="Content-Security-Policy">` doesn't support `frame-ancestors`, `sandbox`, or `report-uri`. For full HTTP header CSP enforcement, add this snippet to your Cloudflare Worker:

```typescript
// In your EmDash worker entry point, wrap the fetch handler:
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const response = await emDashHandler(request, env, ctx);
    const headers = new Headers(response.headers);

    // Adjust the policy to match your settings:cspConfig global config
    headers.set(
      "Content-Security-Policy",
      "default-src 'self'; script-src 'self' 'unsafe-inline'; " +
      "style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; " +
      "object-src 'none'; base-uri 'self'; form-action 'self'; " +
      "upgrade-insecure-requests"
    );

    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers,
    });
  },
};
```

## Verification

After installing on your EmDash site:

**Field Validator:** Set a `pattern` rule on a field, attempt a write that violates it — the save should abort with a `ValidationError`.

**Field Guard:** Set a field as `locked`, attempt to include it in a write — it should be stripped silently.

**CSP Manager:** Set a policy, view source on any public page — confirm `<meta http-equiv="Content-Security-Policy" content="...">` appears in `<head>`.

## Security Contact

Found a vulnerability? Email security@madprojx.com.

## License

MIT © [MadProjX](https://madprojx.com)