Skip to main content
The @ooneex/user package defines the identity contract for the framework. It ships only types and enums — no runtime, no storage, no logic. IUser describes who a request belongs to: email, roles, profile fields, two-factor state, and references to sessions, accounts, and verifications. Auth resolves a user from a token and attaches it to the request context; permission checks and controllers read it from there. Because it is a pure contract, every layer agrees on the same shape without depending on a database or auth provider.

Why this package

  • One identity shape. Every part of the stack — auth, roles, permissions, controllers — speaks the same IUser interface, so a user resolved in one place is usable everywhere.
  • Provider-agnostic. The model is independent of how you authenticate. A Clerk, JWT, or credentials backend maps its own user into IUser; downstream code never sees the provider.
  • Zero runtime. Pure type/interface/enum definitions. Importing it adds nothing to your bundle and forces no storage decisions.
  • Audit-aware. Sessions, accounts, verifications, and profile updates each have a dedicated interface with timestamps and status, so security trails are modeled, not improvised.
  • Roles are first-class. roles is a required field of uppercase strings, the same convention roles and route guards expect.

How it works

A user does not arrive with the request — it is resolved. Auth middleware reads the bearer token, validates it, maps the result into an IUser, and sets context.user. From that point the same object travels through the pipeline.
StageWhat happens to the user
Request arrivescontext.user is null — no identity has been established yet.
Auth middlewareValidates the token, builds an IUser, and assigns context.user. A guest route may leave it null.
Permission checkssetUserPermissions(context) reads context.user and its roles to grant abilities.
ControllerReads context.user (typed `IUsernull`) to scope data to the caller.
In @ooneex/controller, the context types the user as nullable so guest-accessible routes are handled explicitly:
export type ContextType<T extends ContextConfigType = ContextConfigType> = {
  // ...
  user: IUser | null;
  permission?: IPermission;
};

The IUser interface

IUser extends a shared IBase (lifecycle and moderation fields) and adds identity, profile, verification, and security fields. email and roles are required; everything else is optional.
FieldTypeMeaning
emailstringPrimary email address. Required.
rolesUppercase<string>[]Roles granted to the user (e.g. ["ROLE_USER"]). Required; consumed by roles and route guards.
externalIdstringIdentifier from an external auth provider (e.g. a Clerk user id).
namestringFull display name.
firstNamestringGiven name.
lastNamestringFamily name.
usernamestringUnique handle.
avatarstringURL of the profile picture.
biostringShort biography / about text.
phonestringPhone number.
birthDateDateDate of birth.
timezonestringPreferred timezone.
isEmailVerifiedbooleanWhether the email has been verified.
isPhoneVerifiedbooleanWhether the phone has been verified.
lastActiveAtDateLast activity timestamp.
emailVerifiedAtDateWhen the email was verified.
phoneVerifiedAtDateWhen the phone was verified.
lastLoginAtDateLast successful login.
passwordChangedAtDateWhen the password was last changed.
twoFactorEnabledbooleanWhether 2FA is enabled.
twoFactorSecretstringSecret backing the 2FA flow. Store encrypted, never expose.
recoveryTokensstring[]One-time recovery codes.
sessionsISession[]Active and historical sessions for the user.
accountsIAccount[]Linked authentication accounts (OAuth, credentials, WebAuthn).
verificationsIVerification[]Pending or completed verification records.

Inherited IBase fields

FieldTypeMeaning
idstringUnique identifier. Required.
isLocked / lockedAtboolean / DateWhether the account is locked, and when.
isBanned / bannedAt / banReasonboolean / Date / stringBan flag, timestamp, and reason.
isBlocked / blockedAt / blockReasonboolean / Date / stringBlock flag, timestamp, and reason.
isPublicbooleanWhether the record is publicly visible.
createdAt / updatedAt / deletedAtDateLifecycle timestamps (soft-delete via deletedAt).
languageLocaleTypePreferred locale.
IUser references three companion interfaces, each also extending IBase.
  • ISession — an authentication session: token, optional refreshToken, device and location metadata (userAgent, ipAddress, deviceType, browser, operatingSystem, location), isActive, expiresAt, and revocation fields (revokedAt, revokedReason). See JWT for how tokens are minted and validated.
  • IAccount — a linked credential or provider: type (an EAccountType), a hashed password for credentials accounts, OAuth fields (provider, providerAccountId, accessToken, refreshToken, scope, idToken), and provider profile data.
  • IVerification — a verification challenge: token, type (an EVerificationType), optional code, isUsed, expiresAt, and attemptsCount / maxAttempts for rate limiting.
A separate IUserProfileUpdate interface audits profile edits, recording changedFields, previousValues / newValues, a status (EProfileUpdateStatus), and an optional linked verification.

Enums

The package exports three string enums, each with a matching string-literal union type (AccountType, VerificationType, ProfileUpdateStatusType) for use where a plain string is preferred.
EnumMembersUsed by
EAccountTypeOAUTH "oauth", EMAIL "email", CREDENTIALS "credentials", WEBAUTHN "webauthn"IAccount.type
EVerificationTypeEMAIL "email", PHONE "phone", PASSWORD_RESET "password_reset", TWO_FACTOR "two_factor", ACCOUNT_ACTIVATION "account_activation"IVerification.type
EProfileUpdateStatusPENDING "pending", COMPLETED "completed", FAILED "failed", REVERTED "reverted"IUserProfileUpdate.status

Resolving a user in auth middleware

An auth middleware validates the incoming token, maps the provider’s user into an IUser, and assigns it to context.user. Optional fields are only set when present, so the resolved user stays minimal:
import type { IUser } from "@ooneex/user";

const user: IUser = {
  id: providerUser.privateMetadata?.externalId as string,
  externalId: providerUser.id,
  email: primaryEmail.emailAddress,
  roles: (providerUser.privateMetadata?.roles as Uppercase<string>[]) ?? ["ROLE_USER"],
};

if (providerUser.firstName) user.firstName = providerUser.firstName;
if (providerUser.lastName) user.lastName = providerUser.lastName;
if (providerUser.imageUrl) user.avatar = providerUser.imageUrl;

context.user = user; // resolved once; reused for the rest of the request
See Auth for the full middleware contract and guest-route handling.

Using the resolved user

Downstream code reads context.user rather than re-validating the token. Because it is IUser | null, always handle the guest case. In a controller, scope data to the caller:
import type { ContextType } from "@ooneex/controller";

public async index(context: ContextType) {
  const user = context.user;

  if (!user) {
    return context.response.exception("Authentication required", { status: 401 });
  }

  return context.response.json({ email: user.email, roles: user.roles });
}
In a permission class, setUserPermissions reads the user and its roles to grant abilities, which can / cannot then check:
public setUserPermissions(context: ContextType): this {
  const user = context.user;

  if (user?.roles.includes("ROLE_ADMIN")) {
    this.allow().to("manage", "all");
  } else if (user) {
    this.allow().to("read", "Article");
  }

  return this;
}
See Permissions for the full IPermission contract and Roles for how roles strings are defined and compared.

Best practices

  • Never store plaintext passwords. IAccount.password is the hashed credential; hash on write and compare hashes on login. Treat twoFactorSecret and recoveryTokens as secrets too — encrypt at rest, never serialize to clients.
  • Resolve once, reuse everywhere. Validate the token and build IUser in auth middleware, set context.user, and read it downstream. Don’t re-decode tokens in controllers or permissions.
  • Keep the identity model minimal. Populate only the fields you have. Optional fields exist for richer profiles, not as a checklist to fill.
  • Always handle null. The context types the user as IUser | null; branch on it explicitly so guest routes are intentional, not accidental.
  • Expose a safe view. When returning a user over the wire, project to the public fields — never send credentials, secrets, sessions, or verification tokens.
  • Trust roles, not ad-hoc flags. Authorize against the roles array via roles and permissions rather than inventing per-controller checks.