Test your safety rules in CI
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().userIdandrecord.data.*carry the shapes your UI reads.- Live updates with
since: a plain list returns thewatermarkto start from, a malformedsinceis 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
directcollection: the recipient sees the record, a third person is refused, and a file uploaded{ for }one person opens for that person only. - On a
communitycollection: 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_requiredcarryingerr.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.
Thanks for telling us.
Thanks for telling us.
Last updated