@veraorg/vera-shared
v1.0.0
Published
Shared types, zod schemas, error codes, entitlement keys and i18n messages for Vera.
Readme
@veraorg/vera-shared
Types, zod schemas, error codes, entitlement keys and i18n messages for Vera.
This package holds no runtime logic, no database access and no network calls. Everything it exports is either a schema, a type, a constant table, or a pure function over those shapes.
Read ../CLAUDE.md before changing anything here. It is the contract for every repo
in the folder, and it exists in exactly one place — a second copy kept in sync by hand
is a copy that drifts, which is what happened to the one that used to live here.
Install
Published on npm, public:
pnpm add @veraorg/vera-sharedEvery consumer installs from the registry, including locally. file:../Vera-Shared is
gone and does not come back: Railway and Vercel clone a single repo and cannot
resolve a sibling folder, so the link failed the moment anything deployed
(ENOENT: no such file or directory '/Vera-Shared').
The tradeoff that replaces it
Editing this package no longer changes anything for a consumer. A change here reaches
Vera-Bot or Vera-API only after it is released and installed:
pnpm changeset # in the same commit as the change
pnpm version:packages # bumps the version, writes CHANGELOG.md
pnpm release # build + typecheck + test, then publishthen bump the range in the consumer and pnpm install there.
This is slower than a live link and that is the point — what a consumer compiles against
is now exactly what deploys. A build step that quietly copied unpublished output into
sibling node_modules used to hide the difference, which turned every mismatch into a
works-locally-fails-on-Railway problem with nothing in any diff to explain it. It has
been removed.
Releasing
pnpm release publishes; it does not need --access public on the command line because
.changeset/config.json sets access: "public". That field is load-bearing — scoped
packages default to restricted, and a restricted publish still succeeds, so the only
symptom is a consumer's install returning 404. See .changeset/README.md.
Publishing is permanent: a version can be deprecated, but not truly unpublished after
72 hours. Check what npm pack --dry-run lists before the first one.
Then swap every consumer from file:../Vera-Shared to a real version range.
Layout
src/
common/ snowflakes, ObjectIds, timestamps, TTL helpers, pagination
errors/ error codes, classes, VeraError, VeraException
entitlements/ feature keys, Entitlement shape, free/premium tables
schemas/ one file per collection, plus COLLECTIONS
i18n/ locales, the single resolver, generated key union
messages/ typed message bundles (the /messages subpath)
tokens/ design tokens and Components V2 accent colours
messages/
en/ common.json, bot.json, dashboard.json ← source of truth
ar/ same shape
tests/ run against dist/, through the exports mapTwo entry points:
import { guildSchema, canUse, veraError } from '@veraorg/vera-shared'
import { messages, lookup } from '@veraorg/vera-shared/messages'They are separate so a consumer that only needs types never pulls in the ICU files.
Scripts
| Command | What it does |
|---|---|
| pnpm gen:messages | Regenerates the MessageKey union from messages/en |
| pnpm build | Bundles ESM + CJS + .d.ts (runs gen:messages first) |
| pnpm typecheck | tsc --noEmit, over src, tests and the config files |
| pnpm test | Runs the suite (builds first) |
| pnpm test:watch | Same, in watch mode |
| pnpm check | Build, typecheck, test — what CI runs |
Order matters. Tests import the package by its own name and resolve through the
exports map, so they exercise the built bundle exactly as a consumer repo will. That
means dist/ has to exist before typecheck and test — which is why check builds
first. On a clean clone, run pnpm build before a bare pnpm typecheck.
src/i18n/message-keys.generated.ts is generated and git-ignored; pnpm build
regenerates it.
Invariants enforced here
These are checked mechanically rather than left to code review:
- Every error code has an English string.
ERROR_MESSAGES_COMPLETEinsrc/i18n/index.tsis a compile-time assertion. Add a code without a message and the build names the missing key. - Safety is never gated.
assertSafetyNeverGated()throws ifmod.coreorautomod.basicis ever disabled on a tier. Call it once at boot. - AI can never ban or kick.
AI_ALLOWED_CASE_ACTIONScontains onlywarnandtimeout;AI_MAX_AUTONOMOUS_ACTIONistimeout. - A verdict carries no action.
verdictOutputSchemahas no action field on purpose — the model reports, code decides. Adding one moves an authorization decision into a prompt. - A stored verdict holds no message content. IDs and a SHA-256 hash only.
- Shadow mode suppresses enforcement, never welfare routing. A
self_harmverdict parses only withescalate—noneincluded, so shadow mode cannot silence it — and never with acaseId. A false positive costs a moderator thirty seconds; a false negative means the system saw someone in crisis and said nothing. - A welfare alert carries a link, not a copy.
welfareAlertSchemahas no content field, ships no default outreach text, and there is no setting anywhere that DMs a detected user. - Smart Moderation cannot be enabled without an alert channel. Enforced by refinement on the guild config, so "enabled with nowhere to send it" is not a reachable state.
- A mandatory category always escalates. A
csamverdict that did anything other than escalate fails to parse, shadow mode included. AI_INVALID_OUTPUTis a security signal. Listed inSECURITY_SIGNAL_CODESso elevated logging is something code branches on, not a comment someone has to notice.- A provider refusal is a verdict, not an outage.
aiRequestStatusSchemakeepsblocked(the provider declined),rejected(Vera never called it) andprovider_error(it tried and failed) as three separate states, andverdictSourcerecords whether a verdict was classified or inferred from a refusal. Acsamverdict still has to escalate either way — a test asserts it. - An open transfer and a resolved one cannot be confused.
subscriptions.transferaccepts only in-flight statuses; the append-onlytransferscollection accepts only terminal ones.
Each of these has a test that fails when the rule is removed — the suite was checked by breaking the rules, not just by watching it go green.
- Arabic cannot define a key English lacks.
gen:messagesfails on an extra key and warns on a missing one (which falls back to English by design).
Adding things
An error code — add it to VERA_ERROR_CODE_CLASS with its class, then add
common.errors.<lowercase_code> to messages/en/common.json. The build tells you if
you forget the second step.
Before adding one, check it earns its place: two codes may not share both the same
user-facing message and the same remedy. If they would, use the existing code. A
code exists to tell someone something different about what to do next — which
subsystem failed belongs in the log line keyed by traceId, not in a code the user
sees. A test enforces this by comparing every code's rendered English string.
An entitlement key — add it to FEATURE_KEYS, then to both FREE_ENTITLEMENTS
and PREMIUM_ENTITLEMENTS. The map type makes a missing tier a compile error.
A message — edit messages/en/…, run pnpm gen:messages. Full sentences with ICU
variables, never concatenation. Arabic needs all six plural forms.
A collection — add the name to COLLECTIONS and a schema file beside the others.
Anything with a retention policy needs an expiresAt field.
