# Test your safety rules in CI

Canonical: https://docs.gemmein.com/reaffirm
Markdown: https://docs.gemmein.com/reaffirm.md

Check on every deploy that your safety rules still hold. Gemmein enforces your safety rules on the server, so your frontend is never the source of truth and a script of live calls can check them. That script, `reaffirm.mjs`, is included in `@gemmein/sdk`. Copy it next to your app, fill in the CONFIG block, and run it on every deploy.

```
PUBLIC_KEY=pk_test_... SECRET_KEY=sk_dev_... node reaffirm.mjs
```

Exit codes:

- **0**: every check passed.
- **1**: an access check failed.
- **2**: the run could not finish because of a config or connectivity problem. It is named as such and never reported as a failed access check.

A collection name with an invalid format throws at the `collection()` line. A well-formed name that does not exist returns `unknown_collection` and exits 2 with "fix the CONFIG block".

## The CONFIG block

```
const PRIVATE_COLLECTION   = "notes";  // a collection with the `private` rule (required)
const PUBLIC_COLLECTION    = "";       // a `community`/`public_read` collection, e.g. "board" ("" to skip)
const DIRECT_COLLECTION    = "";       // a `direct` collection: checks recipient scoping and handed files
const COMMUNITY_COLLECTION = "";       // a `community` collection: checks plain text and drafts
const GATED_COLLECTION     = "";       // a collection unlocked by a paid plan: checks paid access
const PROBE_FIELD = "probe";           // a field your private collection's shape allows
const TEXT_FIELD  = "text";            // the text field your direct/community shapes use
const TEST_USERS = ["reaffirm-a@test.dev", "reaffirm-b@test.dev", "reaffirm-c@test.dev"];
```

`PRIVATE_COLLECTION` is required. `PUBLIC_COLLECTION`, `DIRECT_COLLECTION`, `COMMUNITY_COLLECTION` and `GATED_COLLECTION` each add more checks. Leave any of them `""` and its checks are skipped *with the reason printed*, so the CI log always says what was checked and what was not. `PROBE_FIELD` and `TEXT_FIELD` are field names from your own collections; Development collections take their shape from writes, so the checks write data shaped like your app's. `TEST_USERS` are three Development test emails.

## Tier A: anonymous access, safe against Live

Reads and refusals only, with no session. An anonymous caller must be refused reading and writing the private collection (`denied`), and the public collection's exposure is reported with a record count.

## Tier B: isolation between people, Development only

Sessions are created without a sign-in code: `gemmeinServer(sk_dev).testSession(email)` returns a token you pass to `gemmein(pk, { tokenStore })`. `sk_live` throws `test_session_forbidden_live`, and the server refuses Live environments too. Development and Live enforce the same rules, so what passes in Development holds in Live.

What the checks confirm:

- B can't read A's private record, and B's list excludes it.
- `currentUser().userId` and `record.data.*` carry the shapes your UI reads.
- Live updates with `since`: a plain list returns the `watermark` to start from, a malformed `since` is refused (`invalid_since`), and a delta read returns the next watermark.
- A made-up file reference is refused on write (`unknown_file`).
- Sealed files: the uploader can `link()` their own file, and another person is refused (`not_found`).
- On a `direct` collection: the recipient sees the record, a third person is refused, and a file uploaded `{ for }` one person opens for that person only.
- On a `community` collection: HTML is refused (`html_not_allowed`) and an unpublished draft is invisible to the public.
- On a collection that needs a paid plan: a person with no plan gets `entitlement_required` carrying `err.requires`.

## What it costs

Test records are deleted afterwards. Test uploads remain in Development storage (the SDK has no call that deletes a file): two tiny PNGs per configured run. On a local runtime older than engine 0.4.8, the `since`, made-up-file-reference and handed-file checks fail with a message naming the local runtime's version as the likely cause. The hosted API is always current.

Add a check whenever you add a feature.
