Migrating to 0.3

Everything that changed between 0.2.3 and 0.3.0, and the edit each change asks for.

0.3.0 is a breaking release. Most of the breakage is one change repeated across every module: the library now throws a single error class, and the codecs now refuse text they used to accept.

Work through the sections that name a function you call. Anything not listed here behaves as it did.

#1. Every error is an UnsecureError

Before, failures came back as Error, TypeError, RangeError, SyntaxError, or an opaque DOMException from Web Crypto. Now every one of them is an UnsecureError carrying a machine-readable code.

// 0.2
try {
  base64Parse(input);
} catch (error) {
  if (error instanceof SyntaxError) return respond(400);
}

// 0.3
import { UnsecureError } from "unsecure/errors";

try {
  base64Parse(input);
} catch (error) {
  if (error instanceof UnsecureError && error.code === "MALFORMED") return respond(400);
  throw error;
}

A Web Crypto refusal that used to escape as a DOMException is now code: "PLATFORM", with the runtime's own error kept in error.cause. safeJsonParse likewise wraps the engine's SyntaxError as MALFORMED with the original as cause.

Most messages are byte-identical. These gained a prefix naming the function, so any test matching on message text must be updated:

  • uuidv7Timestamp: Not a valid UUIDv7: <value>
  • safeJsonParse: <the engine's own text>
  • The OTP secret errors now read hotp: …, totpVerify: …, otpauthURI: … rather than otp: ….
  • The generator's four next messages are prefixed with SecureRandomGenerator.next: .
  • hkdf's two length messages became one: hkdf: length must be an integer between 1 and 8160, got 0.
  • Six Argon2 range messages now spell their bounds as numbers and name the value found.

Note

The code union is complete for this release but may grow in a later minor. Keep a default branch in any switch over error.code.

#2. The codec wrappers are gone

hexEncode, hexDecode, base64Encode, base64Decode, base64UrlEncode, base64UrlDecode, base32Encode and base32Decode have been removed.

0.20.3
hexEncode(data)hexStringify(data)
hexDecode(text, opts)hexParse(text, { loose: true, ...opts })
base64Encode(data)base64Stringify(data)
base64Decode(text)base64Parse(text, { loose: true })
base64UrlEncode(data)base64Stringify(data, { alphabet: "base64url" })
base64UrlDecode(text)base64Parse(text, { alphabet: "base64url", loose: true })
base32Encode(data)base32Stringify(data)
base32Decode(text)base32Parse(text, { loose: true })

The { loose: true } in the right column is not decoration. The old wrappers decoded leniently; parse is strict by default. Drop it only once you know the text is canonical.

#3. Strict decoding refuses more

Strict parse now accepts exactly the canonical encoding of some byte string, and nothing else.

  • Whitespace is a character. base64Parse("Zm 9v") throws where it used to decode.
  • Padding must be right or absent. "Zm9vYg=", "Zm9vYmFy=" and a = inside the body are all refused. Unpadded stays canonical, so anything stringify emits round-trips.
  • The length must encode whole bytes. "Zm9vY" is refused.
  • No bits past the final byte. "Zh==" and "MZXW7===" are refused.
  • Base32 strict is uppercase-only for the base32 and base32hex alphabets. Crockford is unchanged: case-insensitive, with O→0 and I/L→1 in both modes.
  • Bytes past 0x7F are invalid characters. A Uint8Array input is the encoded text read one byte per character, so a UTF-8 BOM is three invalid characters rather than something to skip.
  • String output must be valid UTF-8. Strict parse returning a string throws MALFORMED when the decoded bytes are not text. Pass { returnAs: "bytes" } for binary payloads.

Loose decoding changed too: hexParse no longer stops at the first character it cannot read, it drops it and keeps going. "zz666f6f" used to decode to nothing and now decodes to foo; "66 6f 6f" used to decode to one byte and now decodes to three; "0x66" loses only the x, leaving the single byte 0x06.

A custom base32 alphabet is now validated when it is resolved: 32 distinct ASCII characters, none of them = or whitespace. Anything else is OUT_OF_RANGE.

#4. secureCompare

  • secureCompare(expected, null) returns false instead of throwing.
  • A plain array as received (say [1, 2, 3] out of a JSON body) is a mismatch instead of being compared element by element.
  • An expected that is neither text, bytes nor undefined now throws INVALID_TYPE, where it used to return false or fail internally.
  • Both arguments accept any BytesSource, not just Uint8Array.

The rule is unchanged: the trusted value goes first.

#5. hmac and hmacVerify

  • An empty secret throws OUT_OF_RANGE from both functions, before Web Crypto. A secret that failed to load is a deployment bug, not a wrong signature.
  • hmacVerify no longer infers the signature format from the type of data. A string signature is decoded strictly with the codec named by returnAs — hex when returnAs names raw bytes or is omitted. A hex signature verified with { returnAs: "base64" } still fails; an uppercase hex signature now verifies.
  • hash, hmac and hkdf take string | BytesSource and throw INVALID_TYPE for anything else, where the platform used to report it.

#6. One-time passwords

Every numeric option is checked at the boundary now, so several silent behaviours became errors.

  • hotpVerify(secret, code, undefined) throws. It used to verify against counter 0, which meant a counter that never loaded accepted a stale code.
  • digits outside 6 to 8 is refused (7 is still allowed).
  • generateOTPSecret(0) throws.
  • period: null and time: null throw. Only an omitted option takes its default.
  • otpauthURI({ issuer: "" }) throws. Omit the key, or pass undefined, to mean "no issuer".
  • An empty secret throws OUT_OF_RANGE.
  • totpVerify throws when the derived step plus the window would leave the safe integer range.

Two behavioural fixes need no edit but are worth knowing: verification now walks the whole window on every call, so timing says nothing about which step matched, and delta is the nearest match rather than the first one scanned. otpauthURI percent-encodes its values, so an issuer of My App is My%20App and not My+App.

#7. secureGenerate

  • length must be an integer of at least 1. NaN used to return "", Infinity used to hang, 5.5 quietly produced 5 characters.
  • An invalid Date as timestamp throws instead of prefixing the literal text NaN.
  • A character appearing in two selected sets throws OUT_OF_RANGE, because the repeat would skew the distribution.
  • length counts code points, so a custom set holding astral characters yields fewer UTF-16 units than before for the same length and never emits half a surrogate pair.

#8. Sanitizers

  • sanitizeObjectCopy no longer turns a Date, Map, Set, typed array, RegExp, class instance or function into a plain object. Those values are now shared between the input and the copy. For a fully detached plain structure, use safeJsonParse(JSON.stringify(obj)).
  • sanitizeObjectCopy copies own enumerable data properties only. Accessors are skipped rather than materialized, and an accessor at an array index leaves a hole.
  • sanitizeObject throws FROZEN when a frozen or sealed object holds a dangerous key, where it used to report success.
  • All three functions walk an explicit stack, so a deeply nested payload no longer overflows the call stack.

#9. Random

  • randomJitter throws OUT_OF_RANGE for a fractional bound, and for null. Only undefined takes a default, so randomJitter(undefined, 50) now means the range [0, 50) instead of failing.
  • secureRandomBytes refuses a length above 2**31 - 1 rather than allocating for hours, and its message names the value and the range.

#10. New in 0.3

  • unsecure/argon2 — password hashing (RFC 9106) in plain JavaScript. argon2Hash, argon2Verify, argon2NeedsRehash and the raw argon2 KDF.
  • unsecure/errorsUnsecureError and UnsecureErrorCode.
  • Algorithm names are matched case-insensitively everywhere, so "sha-256" works.
  • importHmacKey and importHkdfKey, and CryptoKey accepted in place of a secret by hmac, hmacVerify, hkdf and the four OTP functions. See Hashing and MAC.