
# Encoding

> Strict base64url and base64 codecs with the JOSE rules fixed in, plus the byte, text and randomness helpers that go with them.

```ts
import { base64UrlParse, base64UrlStringify } from "unjwt/utils";

base64UrlStringify('{"alg":"HS256","typ":"JWT"}'); // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
base64UrlParse("aGVsbG8"); // "hello"
```

Every segment of a JWT is base64url text. These helpers are the codecs from [`unsecure`](https://unsecure.s94.dev/safety/codecs) with the alphabet and padding rules pinned, so you cannot call them with a policy that disagrees with the one `verify()` and `decrypt()` apply.

## Signatures

```ts
function base64UrlStringify(data: string | BytesSource): string;
function base64UrlParse(
  input: string | Uint8Array,
  options?: { returnAs?: "string" | "uint8array" | "bytes"; loose?: boolean },
): string | Uint8Array<ArrayBuffer>;

function base64Stringify(data: string | BytesSource): string;
function base64Parse(
  input: string | Uint8Array,
  options?: { returnAs?: "string" | "uint8array" | "bytes"; loose?: boolean },
): string | Uint8Array<ArrayBuffer>;
```

`BytesSource` is an `ArrayBuffer` or any `ArrayBufferView`. A `string` passed to a `*Stringify` function is UTF-8 encoded first.

## Options

| Option     | Type                                  | Default           | What it does                                                                                        |
| ---------- | ------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------- |
| `returnAs` | `"string" \| "uint8array" \| "bytes"` | mirrors the input | `"string"` decodes the bytes as UTF-8. `"uint8array"` and its alias `"bytes"` return the raw bytes. |
| `loose`    | `boolean`                             | `false`           | Drop characters the alphabet does not carry instead of throwing. For application data only.         |

## What unjwt adds to the unsecure codecs

| Function             | Alphabet           | Padding                 |
| -------------------- | ------------------ | ----------------------- |
| `base64UrlStringify` | URL-safe `-_`      | never written           |
| `base64UrlParse`     | URL-safe `-_` only | any `=` is rejected     |
| `base64Stringify`    | standard `+/`      | always written          |
| `base64Parse`        | standard `+/` only | the `=` run is required |

The URL-safe pair follows [RFC 7515 §2](https://www.rfc-editor.org/rfc/rfc7515#section-2): no padding, no whitespace, no line breaks. The standard pair follows [RFC 4648 §4](https://www.rfc-editor.org/rfc/rfc4648#section-4) and is the one PEM bodies use.

## Return shape

| You passed                         | You get back                                |
| ---------------------------------- | ------------------------------------------- |
| a `string`                         | a UTF-8 `string`, which must be valid UTF-8 |
| a `Uint8Array` of the encoded text | a `Uint8Array<ArrayBuffer>`                 |
| anything, plus `returnAs`          | exactly what `returnAs` names               |

```ts
import {
  base64Parse,
  base64Stringify,
  base64UrlParse,
  base64UrlStringify,
  textEncoder,
} from "unjwt/utils";

base64UrlStringify("hello"); // "aGVsbG8"
base64UrlStringify(new Uint8Array([255, 255])); // "__8"
base64Stringify("hello"); // "aGVsbG8="
base64Stringify(new Uint8Array([255, 255])); // "//8="

base64UrlParse("aGVsbG8", { returnAs: "bytes" }); // Uint8Array(5) [104, 101, 108, 108, 111]
base64UrlParse(textEncoder.encode("aGVsbG8")); // Uint8Array(5) [104, 101, 108, 108, 111]
base64UrlParse(textEncoder.encode("aGVsbG8"), { returnAs: "string" }); // "hello"
base64Parse("aGVsbG8="); // "hello"
```

When the decoded bytes are not text, ask for bytes. Decoding `"__8"` to a string throws, because `0xFF 0xFF` is not UTF-8.

```ts
base64UrlParse("__8", { returnAs: "bytes" }); // Uint8Array(2) [255, 255]
base64UrlParse("__8");
// UnsecureError MALFORMED: Base64.parse: decoded bytes are not valid UTF-8.
```

## Strict by default

`parse` accepts only the canonical encoding of a byte string. Two different texts that decode to the same bytes are how signature checks get bypassed, so anything else is refused.

```ts
base64UrlParse("aGVsbG8=");
// UnsecureError MALFORMED: Base64.parse: unexpected "=" padding.
base64UrlParse("a+Vs");
// UnsecureError MALFORMED: Base64.parse: invalid base64 character "+" at index 1.
base64UrlParse("aGVs bG8");
// UnsecureError MALFORMED: Base64.parse: invalid base64 character " " at index 4.
base64UrlParse("aGVsbG9");
// UnsecureError MALFORMED: Base64.parse: the last base64 symbol sets bits past the final byte.
base64Parse("aGVsbG8");
// UnsecureError MALFORMED: Base64.parse: expected 1 "=" padding characters, found 0.
base64Parse("__8=");
// UnsecureError MALFORMED: Base64.parse: invalid base64 character "_" at index 0.
```

`{ loose: true }` drops whitespace, `=` and any character outside the alphabet. Use it for values a person pasted. `verify()` and `decrypt()` have no such option, and token material should never go through it.

```ts
base64UrlParse("aGVs bG8=", { loose: true }); // "hello"
```

## Bytes and text

`concatUint8Arrays(...arrays)` joins byte arrays into one new buffer. `maybeArray(value)` wraps a value in an array unless it already is one. `textEncoder` and `textDecoder` are shared module-level instances from `unsecure/utils`, so you do not allocate one per call.

```ts
import {
  base64UrlStringify,
  concatUint8Arrays,
  maybeArray,
  textDecoder,
  textEncoder,
} from "unjwt/utils";

const signingInput = concatUint8Arrays(
  textEncoder.encode(base64UrlStringify('{"alg":"HS256"}')),
  textEncoder.encode("."),
  textEncoder.encode(base64UrlStringify("hi")),
);
textDecoder.decode(signingInput); // "eyJhbGciOiJIUzI1NiJ9.aGk"

concatUint8Arrays(new Uint8Array([1, 2]), new Uint8Array([3])); // Uint8Array(3) [1, 2, 3]
maybeArray("api"); // ["api"]
maybeArray(["api", "web"]); // ["api", "web"]
```

## secureRandomBytes()

`secureRandomBytes(length)` returns `length` bytes from the platform CSPRNG. It is the [`unsecure/random`](https://unsecure.s94.dev/generate/random) function, re-exported unchanged. Use it for salts, IVs and raw secrets, never `Math.random()`.

```ts
import { base64UrlStringify, secureRandomBytes } from "unjwt/utils";

const salt = secureRandomBytes(16); // Uint8Array(16)
base64UrlStringify(salt); // "HZVurVz1r5er…"

secureRandomBytes(-1);
// UnsecureError OUT_OF_RANGE: secureRandomBytes: length must be an integer between 0 and 2147483647, got -1.
```

## Errors

These helpers come from `unsecure` and throw its `UnsecureError` directly, not a `JWTError`.

| Cause                                                                          | `code`         |
| ------------------------------------------------------------------------------ | -------------- |
| Text that is not the canonical encoding, or a decoded string that is not UTF-8 | `MALFORMED`    |
| A value that is neither text nor bytes, such as `null`                         | `INVALID_TYPE` |
| A `secureRandomBytes` length that is negative, fractional or too large         | `OUT_OF_RANGE` |

Check with [`isUnsecureError`](/utils/guards#isunsecureerror) imported from unjwt. unjwt bundles its own copy of `unsecure`, so an `UnsecureError` class imported from your own `unsecure` dependency is a different class and `instanceof` against it never matches.

## See also

- [unsecure codecs](https://unsecure.s94.dev/safety/codecs): the full strict and loose contract, plus hex and base32.
- [Type guards](/utils/guards): `isUnsecureError` and `isJWTError`.
- [Errors](/errors): how a decode failure inside `verify()` surfaces as a `JWTError` with the `UnsecureError` as `cause`.
