@wyrhta/core
v0.7.1
Published
Shared foundation for Wyrhta Labs services: identity, auth, HTTP kit, household, DB conventions
Readme
@wyrhta/core
Shared foundation library for Wyrhta Labs household-manager services (Heorth, KithLedger). Published on npm, and consumed as a normal versioned dependency — never a workspace/local link, because each service is an independent repo:
npm install @wyrhta/core{
"dependencies": {
"@wyrhta/core": "^0.7.1"
}
}Every release is published from CI by
trusted publishing (ADR 0011), so
each tarball carries a provenance attestation naming the workflow, repository,
and commit that produced it — npm audit signatures verifies it.
Pinning the git tag directly still works and is the right way to test an unreleased change against a consumer:
{ "@wyrhta/core": "github:Wyrhta-Labs/wyrhta-core#v0.7.1" }Note that this form builds core from source during install, so it needs git
and a TypeScript toolchain on the machine running npm install.
Either way, a change here only reaches consumers when a new version is released
and the consumer's package.json is deliberately bumped.
Module map
Each subpath below is an independent exports entry — import only what you need.
| Subpath | What it provides |
|---|---|
| @wyrhta/core/config | Zod-based environment parsing: baseEnvSchema + parseEnv, plus emptyToUndefined (blank value = absent) and parseEnvOrExit (parse-or-exit startup guard for a full app schema). JWT_SECRET is optional in the base schema — verifier-only deployments hold no secret; apps that sign HS256 re-declare it as required in extra. Also loadDotEnv({ path, skip }), the opt-in local-dev .env reader: exported variables always win and a missing file is not an error. It is the one place core reads a file — a deliberate exception to "core never reads env or files", confined to the module that is the environment |
| @wyrhta/core/lib | API-key crypto helpers, structured logger, and ISO 8601 duration arithmetic (parseDuration / isPositiveDuration / addDuration) over the fixed-step subset both recurrence expanders use — an RRULE is rejected, and so is a duration that parses but does not advance |
| @wyrhta/core/http | Response envelope (ok/err), pagination helpers, and requestId / securityHeaders / rateLimit / errorHandler / trimTrailingSlash Hono middleware. trimTrailingSlash replaces Hono's own: it emits a path-only Location, because an absolute one carries the scheme, host and prefix the app sees rather than the ones the client used, which is wrong behind a proxy. rateLimit keys on the first X-Forwarded-For entry by default, so it is only sound behind a proxy that replaces (not appends to) that header; the optional resolveIp injects an app-supplied key instead. errorHandler is the one core export that reads NODE_ENV — it omits Zod field details in production; createErrorHandler({ validationDetails }) decides that explicitly instead |
| @wyrhta/core/identity | users + api_keys schema, argon2 password hashing, JWT issuing/verification — HS256 shared secret or RS256 / EdDSA keys with kid selection, optional iss / aud claims and clock-skew leeway — plus key loading and JWKS document construction, roles (admin / adult / child) |
| @wyrhta/core/auth | Auth-scheme dispatch and Hono guards (requireAuth, requireJwt, requireRole); the consuming app injects the API-key lookup and, optionally, the expected JWT issuer/audience, public verification keys, and clock-skew leeway |
| @wyrhta/core/household | DB-enforced singleton household |
| @wyrhta/core/testing | assertTestDatabase() — the destructive-suite guard: an integration suite that truncates tables refuses to run unless DATABASE_URL names a database ending in _test. Reachable ONLY on this subpath; it is in no other barrel, so nothing test-only can be imported from application code |
| @wyrhta/core/db | Drizzle ORM / postgres.js client factory, a migrations runner, and SQLSTATE classification (pgErrorCode / isPgError) that walks the cause chain — drizzle-orm >= 0.44 wraps driver errors, so reading error.code directly misses every real violation. isUniqueViolation in ./identity is isPgError(e, '23505') |
What core is NOT
- No business domains — no bills, chores, contacts, ledgers, or any other service-specific concept lives here.
- No UI.
- DB-agnostic where it touches application tables: core does not assume or
own app-level tables outside of its own (identity, household). Apps inject
their own lookups (e.g. the API-key lookup in
./auth) rather than core reaching into app schema. - No speculative features. New capabilities land demand-driven only — when a consumer concretely needs them, not in anticipation of future use.
Domain error codes
Core signals expected, caller-actionable failures by throwing a bare Error
whose message is an UPPER_SNAKE_CASE code matching
^[A-Z][A-Z0-9_]{1,63}$ — e.g. INVALID_TOKEN, TOKEN_EXPIRED,
INVALID_ISSUER, INVALID_AUDIENCE, UNKNOWN_KEY_ID, INVALID_ALGORITHM,
INVALID_KEY_MATERIAL, MISSING_JWT_VERIFICATION_KEY, CONFLICT. Anything
else that escapes core (driver errors, Zod internals, non-Error throws) is
an unexpected failure and carries no such contract.
The distinction is deliberate and consumer-facing: a caller may match on the shape to decide what is safe to surface to a client and what must be collapsed into a generic message. Keep new core error codes in this style.
This convention used to be implemented and documented in the
./mcpscaffold, which was removed in 0.3.0 (ADR 0008). It is documented here because core's own modules —./identity,./auth— still depend on it.
Release discipline
- Every change ships as a semver tag plus a
CHANGELOG.mdentry. - Pre-1.0: a minor bump may break compatibility; a patch bump is safe to take without review.
- Consumers upgrade by a deliberate pin-bump of the
#vX.Y.Ztag in theirpackage.json— there is no auto-update.
Requirements
- Node.js >= 24. The package is ESM-only;
require()from CJS tooling such as drizzle-kit works via Node'srequire(ESM)support. That support landed in 22.12, which was the floor until 0.7.0 — every consumer now builds and ships on Node 24, so 24 is what CI covers and whatenginesdeclares.
Development
npm run build # tsc --project tsconfig.json
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run test:watch # vitest (watch mode)
npm run db:generate # drizzle-kit generate
npm run db:migrate # drizzle-kit migrate
npm run db:push # drizzle-kit push
npm run db:studio # drizzle-kit studio