@axiumine/koa-utils
v5.7.2
Published
Koa Utils
Readme
@axiumine/koa-utils
TypeScript utility library for Koa + GraphQL backends. Ships authentication middleware, GraphQL error helpers, data-source connectors (MongoDB, MariaDB, PostgreSQL, Redis), file upload pipeline (ClamAV + sharp re-encode), and transactional email via SocketLabs.
- Module format: ESM only (
.mjs+.d.mts) - Node:
^24.14.0 - TypeScript:
6.0 - License: GPL-3.0-or-later
- Maintainer: Giovanni Manzoni
Install
yarn add @axiumine/koa-utils
# or
npm install @axiumine/koa-utilsAll runtime dependencies are declared as peer dependencies — the package ships zero dependencies, so the consuming project owns every installed version.
Seven are required. The auth core — sign-up, the login variants, refresh, logout, the middleware, the models and the error helpers — reaches all of them:
yarn add @node-rs/bcrypt @sentry/node dotenv graphql mongoose redis uuidThe remaining twelve are declared optional in peerDependenciesMeta. There is no barrel export: each of these is reachable only through subpaths you opt into by importing them, so a project that never imports the subpath never loads the package, and npm/yarn will not warn about the missing peer. Install the row when you import the subpath:
| Optional peer | Required by |
|---|---|
| @socketlabs/email | email/SocketLabsLib, lib/access/createResetPwdFlow, lib/access/createVerifyEmailFlow, lib/access/verifyEmailMailer, koa/router/verifyEmail, and the signUp / login* / resetPwd / updatePassword / emailChangeHashVerify mutations |
| clamscan | files/scanVirus, files/uploadTempImage, files/uploadTempPdf |
| file-type | files/validateMimeType, files/validateMimeTypeImages, files/validateJpgPngMimeType, files/uploadTempImage, files/uploadTempPdf — loaded lazily, at first call |
| fs-extra | the files/move* and files/validate* helpers, files/uploadTempImage, files/uploadTempPdf |
| keygrip | koa/middleware/authenticatedAuthorizationHandler, .../verifySignedRefreshToken, koa/middleware/authenticatedLogoutHandler — types only; the Keygrip instance is yours to construct and pass in |
| koa | the koa/middleware/* handlers, koa/tdwKoaErrorHandler, koa/logRequestToDb — types only (Next) |
| mariadb | dataSources/MariaDB — Sequelize connects with dialect: 'mariadb' and loads the driver itself |
| pg | dataSources/PostgreSQL |
| reflect-metadata | dataSources/MariaDB — required by sequelize-typescript, never imported by this package |
| sequelize | dataSources/MariaDB — peer of sequelize-typescript |
| sequelize-typescript | dataSources/MariaDB |
| sharp | files/reEncodeToJpeg, files/reEncodeToPng, files/reEncodeToWebp, files/uploadTempImage |
Optionality is a promise about the load path, not a way to silence the warning: it holds only while the dependency stays behind a subpath the consumer opts into. Adding a root "." barrel, or importing one of these from a module that every subpath reaches, breaks it for every consumer at once.
Usage
No barrel — import each helper from its explicit subpath.
Data sources
import { MongoDBConnect, MongoDBDisconnect } from '@axiumine/koa-utils/dataSources/MongoDB'
import { redisClient, RedisConnect } from '@axiumine/koa-utils/dataSources/Redis'
import { pgPool, PostgreSQLClientConnect } from '@axiumine/koa-utils/dataSources/PostgreSQL'
import { sequelize, MariaDBConnect } from '@axiumine/koa-utils/dataSources/MariaDB'
await MongoDBConnect()
await RedisConnect()Koa middleware
import Koa from 'koa'
import Keygrip from 'keygrip'
import { tdwKoaErrorHandler } from '@axiumine/koa-utils/koa/tdwKoaErrorHandler'
import { authenticatedResourceHandler } from '@axiumine/koa-utils/koa/middleware/authenticatedResourceHandler'
import { authenticatedAuthorizationHandler } from '@axiumine/koa-utils/koa/middleware/authenticatedAuthorizationHandler'
const app = new Koa()
const keys = new Keygrip([process.env.COOKIE_KEY!])
app.use(tdwKoaErrorHandler)
app.use(authenticatedResourceHandler()) // resource endpoints
app.use(authenticatedAuthorizationHandler(keys)) // refresh endpointGraphQL mutations
Plain mutation definitions ready to drop into a GraphQLObjectType:
import { GraphQLObjectType } from 'graphql'
import { signUp } from '@axiumine/koa-utils/graphQL/schema/mutations/signUp'
import { loginRememberme } from '@axiumine/koa-utils/graphQL/schema/mutations/loginRememberme'
import { logout } from '@axiumine/koa-utils/graphQL/schema/mutations/logout'
import { refresh } from '@axiumine/koa-utils/graphQL/schema/mutations/refresh'
import { resetPwd } from '@axiumine/koa-utils/graphQL/schema/mutations/resetPwd'
import { updatePassword } from '@axiumine/koa-utils/graphQL/schema/mutations/updatePassword'
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: { signUp, loginRememberme, logout, refresh, resetPwd, updatePassword }
})Access flows on your own model
The mutations above are bound to this package's UserBase model (collection user, fields under login.* / account.*). If your accounts live elsewhere, build the same flows against your own model and field layout instead:
import { createResetPwdFlow } from '@axiumine/koa-utils/lib/access/createResetPwdFlow'
import { createVerifyEmailFlow } from '@axiumine/koa-utils/lib/access/createVerifyEmailFlow'
const { resetPwd, updatePassword } = createResetPwdFlow({
model: Account,
paths: {
email: 'mail',
password: 'pwd',
name: 'profile.fullName',
resetDateReq: 'resetPwd.resetDateReq',
resetHash: 'resetPwd.resetHash',
deleted: 'state.gone', // a set flag makes the reset answer exactly as it does for an unknown address
disabled: 'state.locked',
resetClear: ['resetPwd'] // paths to $unset — a container, not its members, on a strict subdocument
}
})
const verify = createVerifyEmailFlow({
model: Account,
paths: { email: 'mail', valid: 'verified' },
onAbandon: 'soft-delete', // 'delete' (default) | 'soft-delete' | 'keep'
deletedValue: () => new Date() // what 'soft-delete' writes; default true
})
router.get('/check/verify-email/:email/:hash', verify.routerVerifyEmail())Every paths key is optional and falls back to the UserBase layout, so the package-level exports are exactly these factories with no overrides. See docs/code/lib-access.md.
onAbandon decides what the two disposal guards — the fifth wrong hash and a link older than 3 days — do to a pending registration. The default 'delete' removes the row; use 'soft-delete' or 'keep' when other collections depend on it, since mongo has no cascade, or pass deleteUserByEmail to replace the writer outright. The link is rejected either way.
Guard notifications are debounced per address per template (15 minutes) so an unauthenticated GET /check/verify-email/… cannot be looped into a mail bomb aimed at a registered address. Pass mailThrottle for a different window or a Redis-backed one, mailThrottle: ALWAYS_MAIL to opt out, or mailer to replace the sender entirely (any object with the six IVerifyEmailMailer methods).
GraphQL error helpers
import { throwForbiddenError } from '@axiumine/koa-utils/graphQL/throw/throwForbiddenError'
import { throwNotFoundError } from '@axiumine/koa-utils/graphQL/throw/throwNotFoundError'
import { throwTooManyRequestsError } from '@axiumine/koa-utils/graphQL/throw/throwTooManyRequestsError'
if (!user) throw throwNotFoundError()All throw* helpers wrap throwGraphQLError(status, title, description) and yield a GraphQLError carrying extensions.http.status so tdwKoaErrorHandler maps it to the right HTTP code.
File uploads
import { initClamScan } from '@axiumine/koa-utils/files/scanVirus'
import { uploadTemp } from '@axiumine/koa-utils/files/uploadTempImage'
import { uploadTempPdf } from '@axiumine/koa-utils/files/uploadTempPdf'
await initClamScan() // once at boot
const { tempFile, ext } = await uploadTemp(filePromise) // jpg/png → webpPipeline: stream to /tmp (5 MB cap) → extension + magic-number MIME check → ClamAV → sharp re-encode (strips EXIF). PDF path scans only (no re-encode).
import { SocketLabsLib } from '@axiumine/koa-utils/email/SocketLabsLib'
const mailer = new SocketLabsLib()
await mailer.sendEmailVerify('[email protected]', hash)Note: copy is hard-coded English and branded with
PLATFORM_NAME. Subclass / replace template methods for other locales.
Helpers
import { encryptPassword } from '@axiumine/koa-utils/lib/encryptPassword'
import { compareHashAsync } from '@axiumine/koa-utils/lib/hash'
import { checkEmailLen } from '@axiumine/koa-utils/lib/checkEmailLen'
import { checkPwdLen } from '@axiumine/koa-utils/lib/checkPwdLen'
import { generateAccessToken, generateRefreshToken, accessTokenExpiry, REFRESH_TOKEN_EXPIRY } from '@axiumine/koa-utils/lib/tokens'
import { setLoginCookies } from '@axiumine/koa-utils/lib/setLoginCookies'
import { DateLib } from '@axiumine/koa-utils/lib/DateLib'
import { StringLib } from '@axiumine/koa-utils/lib/StringLib'Auth flow
signUp— creates a user withaccount.email.valid=false, sends verify email.routerVerifyEmail— Koa router validating the email + hash, enabling the account.loginRememberme/login4Ever/loginAdmin— bcrypt compare, generate uuidaccess:<uuid>+refresh:<uuid>in Redis, set signedrefresh_tokencookie (90 d), return access token in body.authenticatedResourceHandler— middleware readingAuthorization: Bearer access:<uuid>.authenticatedAuthorizationHandler(keys)— middleware verifying signed refresh cookie via Keygrip; mount before therefreshendpoint.refresh— rotates both tokens, random access TTL in [30, 90] minutes, fixed 90-day refresh TTL.logout+authenticatedLogoutHandler— clears Redis keys and cookie.
Environment variables
| Var | Purpose |
| --- | --- |
| MONGODB_URI | Mongo connection string |
| MARIADB_DBNAME, _USER, _PWD, _IP, _PORT, _RETRY, _TIMEOUT, _LOGGING | MariaDB |
| POSTGRESQL_USER, _PWD, _HOST, _PORT, _DBNAME, _POOL_MAX, _IDLE_TIMEOUT | PostgreSQL |
| REDIS_URL or REDIS_IS_CLUSTER + REDIS_DB{1,2,3}_HOST/PORT, REDIS_USERNAME, REDIS_PASSWORD | Redis |
| REDIS_KEY | Prefix for all keys (e.g. myapp:) |
| INTROSPECTION_CODE | Bypass header x-introspectioncode for schema introspection |
| SOCKETLABS_SERVER_ID, SOCKETLABS_SERVER_APIKEY | SocketLabs auth |
| PLATFORM_NAME, APP_DOMAIN, EMAIL_FROM, DEV_TEAM_EMAIL | Email templating |
| STATIC_FOLDER | Destination for moveFileStaticDomain |
| NODE_ENV | development enables extra Sentry capture in the error handler |
Each module calls dotenv.config() at import time.
TLS / cookies
tokenOptions sets secure: false by design. Flip it at the reverse proxy (Nginx / Caddy / CloudFront), not in source.
Build (contributors only)
yarn install
yarn build # ESM only — production output
yarn build:all # ESM + CJS dual output
yarn lint # eslint --fix + prettier --write
yarn cleanSource files are .mts. Imports use .mjs extension explicitly (NodeNext). Path aliases (@lib/*, @throw/*, @models/*, @context/*, @stypes/*, @private/*, @email/*, @dataSources/*) are rewritten to relative paths in emitted code via typescript-transform-paths (run through tspc, the ts-patch wrapper).
Adding a new public symbol requires both:
- Creating
src/<area>/<Name>.mts - Adding the matching entry under
exportsinpackage.json
There is no barrel and no main / module field.
Registry
yarn.lock always resolves against https://registry.npmjs.org/, so a plain clone installs with no extra setup.
Maintainers who install through a private npm mirror should not commit its host: a git clean/smudge filter rewrites the resolved URLs in both directions, keeping the mirror in the working copy and the public registry in history. yarn install configures it (via prepare → hooks:install); point it at your own mirror with YARN_PROXY_REGISTRY. A pre-commit check rejects any lockfile that still names a non-public host.
Repository
https://github.com/Axiumine/koa-utils
License
GPL-3.0-or-later © Giovanni Manzoni
