
# 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.

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

  document.querySelector("#id").textContent = uuidv7();
</script>
```

```ts
// 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

```ts
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.

```html
<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

```ts
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.

```ts
// 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`.

```ts
// 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.
