
# Utilities

> The small helpers around tokens: base64url codecs, type guards, claim checks, and the plumbing that `sign` and `verify` are built from.

::note
**ELI5.** A JWT is three chunks of base64url text joined by dots. Most of the time [`sign()`](/jwt/jws/sign) and [`verify()`](/jwt/jws/verify) handle everything, and you never touch the chunks. Sometimes you need a piece of what they do on its own: turn bytes into URL-safe text, ask whether a value is a key and what kind, check that a set of claims has not expired, or turn `"15m"` into seconds. `unjwt/utils` hands you those pieces, built with the same strict rules the token functions use. None of them checks a signature, so none of them can tell you whether a token is genuine.
::

::card-group
::card{title="Encoding" icon="i-lucide-binary" to="/utils/encoding"}
Strict base64url and base64, byte helpers, shared text codecs and secure random bytes.
::
::card{title="Type guards" icon="i-lucide-scan-search" to="/utils/guards"}
Narrow an unknown value to a JWK, a JWK Set, a `CryptoKey`, or an error class.
::
::card{title="Claims and durations" icon="i-lucide-clock" to="/utils/claims"}
`validateJwtClaims()`, the `Duration` format, algorithm inference and sanitizers.
::
::card{title="unjwt/internal" icon="i-lucide-cog" to="/utils/internal"}
JOSE plumbing for tests and tooling, with no semver guarantee.
::
::

## How they fit together

The program below signs a token, then takes it apart by hand. It reads the claims without verifying the signature, which is fine for a debugging tool and never fine for an authorization decision.

```ts
import { sign } from "unjwt/jws";
import { generateJWK } from "unjwt/jwk";
import {
  base64UrlParse,
  inferJWSAllowedAlgorithms,
  isJWTContent,
  isSymmetricJWK,
  safeJsonParse,
  validateJwtClaims,
} from "unjwt/utils";

const key = await generateJWK("HS256");
isSymmetricJWK(key); // true
inferJWSAllowedAlgorithms(key); // ["HS256"]

const token = await sign({ sub: "user_42" }, key, {
  expiresIn: "15m",
  currentDate: new Date("2026-10-15T12:00:00Z"),
});

const [headerSegment, payloadSegment] = token.split(".");
const header = safeJsonParse(base64UrlParse(headerSegment));
// { kid: "A0nyp_aeagG7…", alg: "HS256", typ: "JWT" }
isJWTContent(header); // true

const claims = safeJsonParse(base64UrlParse(payloadSegment));
// { sub: "user_42", iat: 1792065600, exp: 1792066500 }

validateJwtClaims(claims, { subject: "user_42", currentDate: new Date("2026-10-15T12:10:00Z") });
// passes

validateJwtClaims(claims, { currentDate: new Date("2026-10-15T12:20:00Z") });
// JWTError ERR_JWT_EXPIRED: JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: 2026-10-15T12:15:00.000Z)
```

`verify()` and [`decrypt()`](/jwt/jwe/decrypt) run the same `validateJwtClaims()` on a JSON payload once the signature or decryption has succeeded, so the claim options you pass there behave exactly as they do here. The JOSE types (`JWK`, `JWTClaims`, `Duration`, …) are exported from `unjwt`.

## Every export

Everything below is a named export of `unjwt/utils`. The root entry point `unjwt` re-exports the whole module flat, so `import { base64UrlParse } from "unjwt"` works too.

| Page                                  | Exports                                                                                                                                                                                          |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Encoding](/utils/encoding)           | `base64UrlStringify`, `base64UrlParse`, `base64Stringify`, `base64Parse`, `concatUint8Arrays`, `maybeArray`, `textEncoder`, `textDecoder`, `secureRandomBytes`                                   |
| [Type guards](/utils/guards)          | `isJWK`, `isJWKSet`, `isSymmetricJWK`, `isAsymmetricJWK`, `isPrivateJWK`, `isPublicJWK`, `isCryptoKey`, `isCryptoKeyPair`, `assertCryptoKey`, `isJWTContent`, `UnsecureError`, `isUnsecureError` |
| [Claims and durations](/utils/claims) | `validateJwtClaims`, `computeDurationInSeconds`, `inferJWSAllowedAlgorithms`, `inferJWEAllowedAlgorithms`, `sanitizeObject`, `sanitizeObjectCopy`, `safeJsonParse`                               |
| Types                                 | `BytesSource`, `DecodeOptions`, `DecodeReturnAs`, `UnsecureErrorCode`, `InferAlgorithmsOptions` and `StrictOmit`                                                                                 |

`isJWTError` lives next to `JWTError` and is exported from `unjwt`, `unjwt/jws`, `unjwt/jwe` and `unjwt/jwk`. The [type guards](/utils/guards#isjwterror) page covers it with the others.

## Errors

The codec, random and sanitize helpers come from [`unsecure`](https://unsecure.s94.dev) and throw its `UnsecureError` (`MALFORMED`, `INVALID_TYPE`, `OUT_OF_RANGE`). Everything unjwt wrote itself throws a `JWTError`. When a token function fails because a segment did not decode, it throws a `JWTError` and keeps the `UnsecureError` as `cause`. See [Errors](/errors) for the full list of codes.
