Skip to content

The public API

Everything a package exports is something it promises to keep working. So the surface is kept to what someone outside the framework has a reason to call — and until 0.5.1, it was not.

Fifty-six exports were removed. Not deprecated, removed: parseBody, scanRoutes, toResponse, parseCron, renderInbox, captureToken and about fifty others.

They were never features. They are the framework’s own moving parts, exported at some point so a test could reach them, and left exported afterwards. The tests do not even need it — every one of them imports from the module directly (from './body'), not from the package index. The exports were doing nothing except sitting in the public surface.

Why that is worth a breaking change rather than leaving them alone:

Everything exported is a promise 149 exports meant 149 things that could not change without a major version — for functions nobody was supposed to call
Autocomplete is documentation Typing import { and seeing redactToken, middlewarePrefix and assertSendable teaches nothing except that the list is untrustworthy
A large surface hides the small one The eight things you actually need were buried among sixty you did not

If you imported one of them, you were reaching into the framework’s internals — which is occasionally the right call, and is now something you do deliberately by importing from the file rather than accidentally because it appeared in autocomplete.

Everything below stayed, because each has a caller outside the framework. Most were undocumented before 0.5.1, which is what made the sweep necessary in the first place.

import { readRouteModule, setRouteManifest } from '@theoven/core'

You will never type these. oven build writes them into .oven/routes.ts and the bundled entry, so they are part of the contract between the CLI and core — and removing them would break every built application rather than only code that imported them.

generateManifest and generateOpenApi are the other half: the CLI calls them to produce that manifest and to answer oven openapi.

import { signAccessToken, verifyAccessToken } from '@theoven/auth'
// A token for a test, without going through /auth/login.
const token = signAccessToken({ sub: user.id }, secret, 900)
// Verifying one somewhere the auth brick is not registered — a worker, a second service.
const claims = verifyAccessToken(token, secret) // null when expired, tampered, or foreign

verifyAccessToken returns null rather than throwing, for the same reason verifyPassword returns false: a bad token is a normal event on a public endpoint, not an exception.

import { isUnusablePassword } from '@theoven/auth'
// Someone who signed up with Google has no password, so a settings page should offer
// "set a password" rather than "change password".
const canChange = !isUnusablePassword(user.raw.passwordHash)

This is why social sign-in needed no breaking migration: a provider-created user gets a sentinel no password can match, rather than a null column.

import { passwordAuthProvider, supportsAccounts } from '@theoven/auth'
import { drizzleStore } from '@theoven/auth-basic'
import { authModels, mongooseStore } from '@theoven/auth-mongo'

basicAuth and mongoAuth are thin wrappers over passwordAuthProvider plus a store. Call them directly when you want the combination they do not offer — a Drizzle store with a different provider name, or a Mongo store with your own connection handling:

passwordAuthProvider({
name: 'staff',
secret,
store: drizzleStore(db, { accounts: true }),
})

supportsAccounts(store) reports whether a store can hold linked accounts — the check the brick itself runs at boot before allowing social sign-in.

authModels(connection) gives you the compiled Mongoose models, for querying the auth collections alongside your own.

import { completeOAuth, startOAuth } from '@theoven/auth'

The two halves of the social sign-in flow, for an application that wants the redirect and callback somewhere other than /auth/oauth/:provider. Both take the same config the mounted endpoints use, so the state, PKCE and linking rules are identical.

import { providerFor, rememberProvider } from '@theoven/db'
import { assertSafeKey } from '@theoven/storage'

For people writing a database provider or a storage driver rather than using one. rememberProvider is how transaction() finds the right implementation for a client it was handed; assertSafeKey is the path-traversal guard every storage driver must apply, exported so a third-party driver applies the same one rather than its own idea of what is safe.

import { isUpgrade } from '@theoven/core'
// A middleware that should not run for WebSocket handshakes — a body-logging one, say.
app.use(async (ctx, next) => {
if (isUpgrade(ctx.req)) return next()
return logBody(ctx, next)
})

Nothing stops you. Import it from the file:

import { parseCron } from '@theoven/queue/src/cron'

It works, and it is honest — the import says plainly that you have reached past the contract, so the next person can see it and a version bump can break it without anyone claiming surprise. The raw escape hatches exist for the same reason and carry the same warning.

An export needs a caller outside the framework, and a sentence explaining who. If neither exists, it stays internal — and bun run check:docs now covers every README, so an export that is documented cannot quietly stop being true.