# modsure

Moderate posts, comments, and other user-written text against your site's rules using Jev.

**Someone clicks Post → modsure checks your rules → your site publishes the post or explains what needs to change.**

Zero runtime or development dependencies. Native `fetch`, ESM JavaScript, TypeScript declarations, Node.js 22+. Runs on your server with your own TypeSafe API key.

## 1. Install

Install modsure in your application:

```sh
npm install modsure
```

These examples use ES modules. Set `"type": "module"` in your application's `package.json`, or use `.mjs` file extensions and update the imports accordingly.

## 2. Add your API key

Get your own TypeSafe API key from [console.typesafe.ai](https://console.typesafe.ai). Create a `.env` file in your application's root:

```dotenv
TYPESAFE_API_KEY=your_api_key
```

Add `.env` to `.gitignore`. On your hosting platform, set `TYPESAFE_API_KEY` in its environment settings. Keep the key and moderation code on your server, not in browser code.

## 3. Write your rules in one config file

Create `modsure.config.js` in your application's root. This file keeps the API-key environment variable, settings, and rules together:

```js
export default {
  apiKey: process.env.TYPESAFE_API_KEY,
  threshold: 0.7,
  rules: [
    {
      id: 'no-spam',
      description: 'No unsolicited advertising or promotional links.',
      message: 'Please remove advertising or promotional links before posting.',
    },
    {
      id: 'be-respectful',
      description: 'No personal attacks, harassment, or threats against other people.',
      message: 'Please remove personal attacks, harassment, or threats.',
    },
    {
      id: 'respect-privacy',
      description: 'Do not share another person’s private contact details without permission.',
      message: 'Please remove other people’s private information.',
    },
  ],
};
```

- `id`: a unique name for the rule, returned when it is flagged.
- `description`: the rule Jev checks, written in plain language.
- `message`: what your site can show the author if the rule is flagged. Defaults to `description`.
- `threshold`: the violation probability that triggers rejection. Here, `0.7` means 70% or higher. A rule can set its own `threshold` to override this value.

The library's default threshold is `0.5` if you omit it. Try your rules against representative posts and adjust the threshold for your community.

## 4. Check text before publishing

Create `moderation.js` next to your config:

```js
import { createModerator } from 'modsure';
import config from './modsure.config.js';

// Create once and reuse for incoming posts.
export const moderator = createModerator(config);
```

In your server's post handler:

```js
import { ModerationError } from 'modsure';
import { moderator } from './moderation.js';

// `text` is the submitted post from your request handler.
try {
  const result = await moderator.moderate(text);

  if (result.allowed) {
    // Your application saves/publishes this exact text here.
  } else {
    // Leave the post unpublished and return these reasons to the author.
    const reasons = result.violations.map(({ id, message }) => ({
      rule: id,
      message,
    }));
    console.log(reasons);
  }
} catch (error) {
  if (!(error instanceof ModerationError)) throw error;
  // No decision was made. Keep the draft and let the author retry.
  // Your application should return an unavailable response, not publish.
  console.error(error.code);
}
```

Load `.env` when starting your server, replacing `server.js` with your application's entry point:

```sh
node --env-file=.env server.js
```

If your framework already loads `.env`, use its normal start command. Modsure does not automatically load `.env` or discover `modsure.config.js`: your application loads the environment and imports the config explicitly.

**Modsure returns decisions; your application handles publishing and displaying feedback.** Any flagged rule makes `allowed` false. Errors never count as an approved post. Invalid configuration or empty text raises `TypeError`.

Keep the rules under your site's control. Post text and rule descriptions are sent to TypeSafe for evaluation; modsure does not store or log them.

## Try a complete example

With the config and `moderation.js` above, create `check-post.js`:

```js
import { moderator } from './moderation.js';

const text = process.argv[2] ?? 'Thanks for writing this helpful article!';
const result = await moderator.moderate(text);
console.log(JSON.stringify(result, null, 2));
```

```sh
node --env-file=.env check-post.js 'Thanks for writing this helpful article!'
```

This makes a real API call using your key. For a full social-feed example, use the repository's `demo/` directory.

## Rules and decisions

Each rule needs a unique `id` and a plain-language `description` of the site's policy. Optional `message` is the explanation shown to the author; it defaults to the description. Write one clear requirement per rule.

The library sends one Noul question per rule in a single request to [TypeSafe's Jev API](https://docs.typesafe.ai/introduction/quickstart). A [Noul answer](https://docs.typesafe.ai/primitives/noul) is the probability that the content violates that rule. A rule is flagged when its probability is **greater than or equal to** its threshold. If any rule is flagged, `allowed` is false.

Jev returns judgments, not generated explanations. Rejection messages come from your configured rules, so authors see your wording. These are model judgments and can be wrong; test rules and thresholds against representative posts before relying on them.

```js
// Example result; probabilities depend on the model's response.
{
  allowed: false,
  model: 'jev-1.13.0',
  violations: [{
    id: 'no-spam',
    description: 'No unsolicited advertising or promotional links.',
    message: 'Please remove advertising or promotional links before posting.',
    threshold: 0.5,
    probability: 0.94,
    violated: true
  }],
  checks: [/* every evaluated rule, using the same fields */]
}
```

## Configuration

| Option | Default | Meaning |
| --- | --- | --- |
| `apiKey` | Required | Your TypeSafe API key |
| `rules` | Required | Nonempty array of site rules |
| `threshold` | `0.5` | Default violation threshold, between 0 and 1 |
| `rules[].threshold` | Global threshold | Override for a particular rule |
| `model` | `jev-latest` | Jev model identifier; pin a version if desired |
| `timeoutMs` | `10000` | Deadline for request and response body |
| `fetch` | Native `fetch` | Optional compatible transport, useful for tests |

Raising a threshold requires a higher violation probability to reject. A threshold of 0 flags every answer; 1 flags only an answer of exactly 1. The default is a starting point, not a calibrated policy for every community. Rules are copied when the moderator is created; create a new moderator when policy changes.

`moderator.moderate(text, { signal })` accepts an optional `AbortSignal`. Empty or whitespace-only text and invalid configuration raise `TypeError`.

Service failures throw `ModerationError`; they never return an accept/reject result:

| `code` | Meaning |
| --- | --- |
| `HTTP_ERROR` | Provider returned an error; `status` contains the HTTP status |
| `NETWORK_ERROR` | Network/transport failure |
| `TIMEOUT` | Request exceeded its deadline |
| `ABORTED` | Caller cancelled the request |
| `INVALID_RESPONSE` | Invalid JSON, malformed data, or a missing/invalid rule answer |

There are no automatic retries or hidden additional calls. Your site controls retries and how an unavailable moderation service affects posting. Limits and billing are controlled by your TypeSafe account.

## Development

A working social-feed demo and browser recording instructions are in [demo/README.md](demo/README.md). Run `npm run demo` after adding your API key to `.env` to try it locally.

```sh
npm test
npm pack --dry-run
TYPESAFE_API_KEY=your-key node examples/moderate.js 'Text to check'
```

Tests use mocked HTTP responses and require no API key. The example makes a real, billable API request. The package has no build step.

## License

MIT. See [LICENSE](LICENSE).
