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.subtleneeds a secure context. Onhttp://it isundefined, which meanshash,hmac,hkdfand the OTP functions fail.localhostcounts 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.