Import from a CDN

A browser page that downloads one module and nothing else.

#The rule

Every module is published as its own entry point. A CDN import names a subpath, so the browser fetches that module's bundle alone.

<script type="module">
  import { uuidv7 } from "https://esm.sh/unsecure/uuid";

  document.querySelector("#id").textContent = uuidv7();
</script>
// The barrel over a CDN: every module, Argon2 included, for one function
import { uuidv7 } from "https://esm.sh/unsecure";

// One module
import { uuidv7 } from "https://esm.sh/unsecure/uuid";

Bundlers are the other case. The package is sideEffects: false, so importing from unsecure and letting the bundler tree-shake is equivalent to naming the subpath. It is only a network import that pays for the whole barrel.

#Pin the version

import { uuidv7 } from "https://esm.sh/unsecure@0.3.0/uuid";

Unpinned, the CDN serves whatever is newest, which means your page's behaviour changes without a deploy.

#An import map

An import map keeps the version in one place and the imports readable.

<script type="importmap">
  {
    "imports": {
      "unsecure/": "https://esm.sh/unsecure@0.3.0/"
    }
  }
</script>

<script type="module">
  import { totp, generateOTPSecret } from "unsecure/otp";
  import { Base32 } from "unsecure/utils";
  import { uuidv7 } from "unsecure/uuid";
</script>

#The subpaths

import { argon2Hash, argon2Verify } from "https://esm.sh/unsecure/argon2";
import { secureCompare } from "https://esm.sh/unsecure/compare";
import { entropy } from "https://esm.sh/unsecure/entropy";
import { UnsecureError } from "https://esm.sh/unsecure/errors";
import { secureGenerate } from "https://esm.sh/unsecure/generate";
import { hash } from "https://esm.sh/unsecure/hash";
import { hkdf } from "https://esm.sh/unsecure/hkdf";
import { hmac, hmacVerify } from "https://esm.sh/unsecure/hmac";
import { totp, generateOTPSecret } from "https://esm.sh/unsecure/otp";
import { secureRandomBytes } from "https://esm.sh/unsecure/random";
import { safeJsonParse } from "https://esm.sh/unsecure/sanitize";
import { Base64, Base32, Hex } from "https://esm.sh/unsecure/utils";
import { uuidv7, createUUIDv7Generator } from "https://esm.sh/unsecure/uuid";

#Deno and Bun

The same URLs work in Deno without an install step, and Bun resolves them too.

// Deno
import { uuidv7 } from "https://esm.sh/unsecure@0.3.0/uuid";

// Or add it to the import map in deno.json
// deno add npm:unsecure

#What runs in a browser and what does not

Everything on this site runs in a browser. There are no Node built-ins anywhere in the package, and crypto.subtle and crypto.getRandomValues are the only globals it touches.

Two things to keep in mind:

  • crypto.subtle needs a secure context. On http:// it is undefined, which means hash, hmac, hkdf and the OTP functions fail. localhost counts as secure; a plain-HTTP intranet host does not.
  • Argon2 holds the thread. At the defaults a hash takes roughly 140 ms of synchronous work, which is 140 ms of frozen UI. Run it in a Worker.
// argon2-worker.ts
import { argon2Hash } from "https://esm.sh/unsecure@0.3.0/argon2";

self.onmessage = async (event) => {
  self.postMessage(await argon2Hash(event.data));
};

#What not to do in a browser

Client-side verification is a user-experience feature, not a security boundary. Hashing a password in the browser does not protect it from your own server, and checking an HMAC in the browser proves nothing to anyone but the browser.

Use these in a page for the things that genuinely belong there: generating a value, encoding one, validating a format, deriving a key from something the user typed, checking entropy before the form is submitted.