Package Exports
- nostr-mill
- nostr-mill/themes
Readme
MILL — Multi-Interface Login Layer
Zero-dependency Nostr signer UI as a Web Component.
Drop it into any web app with a <script> tag. Works with every Nostr signing method.
Supported Methods
| Method | NIP | Description |
|---|---|---|
| Browser Extension | NIP-07 | Alby, nos2x, Flamingo, Nostore |
| Remote Signer | NIP-46 | Bunker URL or QR scan |
| Android Signer | NIP-55 | Amber (via Android intents) |
| Private Key | — | nsec/hex, AES-256 encrypted in sessionStorage |
| Read Only | — | Public key / npub view-only access |
| New Identity | — | Generate keypair in-browser |
Install
CDN (zero config)
<!-- Latest from your self-hosted CDN -->
<script src="https://cdn.yourdomain.com/mill/mill.umd.js"></script>
<!-- Or via jsDelivr (once published to npm as nostr-mill) -->
<script src="https://cdn.jsdelivr.net/npm/nostr-mill/dist/mill.umd.js"></script>npm
npm install nostr-mill
# nostr-tools is an optional peer dep for real key derivation:
npm install nostr-toolsUsage
Script tag / CDN
<script src="mill.umd.js"></script>
<button onclick="MILL.open({ onConnected: console.log })">
Connect Nostr Account
</button>Web Component
<nostr-signer id="signer" theme="dark"></nostr-signer>
<script>
const signer = document.getElementById('signer');
// Open programmatically
signer.open({
onConnected: (result) => {
console.log(result.method); // 'nip07' | 'nip46' | 'nip55' | 'privatekey' | 'readonly' | 'newkey'
console.log(result.pubkey); // hex pubkey
}
});
// Or listen via events
signer.addEventListener('mill:connected', (e) => {
const { method, pubkey } = e.detail;
});
signer.addEventListener('mill:disconnected', () => {
console.log('user disconnected');
});
</script>ESM / bundler
import MILL from 'nostr-mill';
MILL.open({
theme: 'dark',
onConnected: (result) => {
// result.method — which method the user chose
// result.pubkey — hex public key
// result.signer — window.nostr-compatible interface (where available)
},
onClose: () => console.log('modal closed'),
});Theming
MILL uses CSS custom properties scoped to the Shadow DOM :host. Override them externally:
nostr-signer {
--mill-accent: #00c896;
--mill-bg: #0a0a0a;
--mill-radius: 8px;
--mill-font: 'Your App Font', sans-serif;
}Built-in themes
// Named themes: 'dark' (default), 'light', 'minimal', 'grain'
MILL.open({ theme: 'light' });
// Or pass a partial token object — merged onto the dark baseline
MILL.open({
theme: {
'--mill-accent': '#ff6b35',
'--mill-bg': '#0f0f0f',
'--mill-radius': '4px',
'--mill-font': "'IBM Plex Sans', sans-serif",
}
});
// Or use brandTheme() helper — pass just a few inputs
import { brandTheme } from 'nostr-mill/themes';
MILL.open({ theme: brandTheme({ accent: '#7c3aed', radius: '6px' }) });Full CSS variable reference
| Variable | Default | Description |
|---|---|---|
--mill-bg |
#09080f |
Modal backdrop background |
--mill-surface |
#100e1b |
Modal surface |
--mill-card |
#181528 |
Method card background |
--mill-card-hover |
#1f1c35 |
Method card hover |
--mill-border |
#2a2544 |
Default border |
--mill-border-light |
#3e3860 |
Highlighted border |
--mill-accent |
oklch(0.67 0.28 282) |
Primary accent (purple) |
--mill-accent-dim |
…/ 0.13 |
Accent tint background |
--mill-teal |
oklch(0.67 0.18 195) |
Secondary accent |
--mill-text |
#ede8fc |
Primary text |
--mill-text-secondary |
#9d94c0 |
Secondary text |
--mill-muted |
#5e5880 |
Muted / placeholder text |
--mill-danger |
oklch(0.65 0.24 15) |
Error / danger states |
--mill-warning |
oklch(0.78 0.18 65) |
Caution states |
--mill-success |
oklch(0.7 0.2 155) |
Success / positive states |
--mill-radius |
14px |
Base border radius |
--mill-font |
'Space Grotesk', system-ui |
UI font stack |
--mill-font-mono |
'JetBrains Mono', monospace |
Monospace font stack |
Events
| Event | e.detail |
Description |
|---|---|---|
mill:connected |
{ method, pubkey, signer?, perms? } |
User successfully connected |
mill:disconnected |
{} |
User disconnected |
Return value (result object)
type MillResult = {
method: 'nip07' | 'nip46' | 'nip55' | 'privatekey' | 'readonly' | 'newkey';
pubkey: string; // hex-encoded public key, always present
perms?: SigningPerms; // per-kind signing preferences (privatekey / newkey only)
bunkerUrl?: string; // NIP-46 only
nsec?: string; // newkey flow only — the generated nsec (handle carefully)
};Security notes
- Private key flows: nsec is encrypted with AES-256-GCM (PBKDF2, 100k iterations) and stored only in
sessionStorage— wiped on tab close. - NIP-07: MILL never sees the private key. Only the public key and completed signed events pass through.
- NIP-46: Only signed event payloads travel over the relay — never the key.
- NIP-55: On-device intent — no network between apps.
Browser support
Modern browsers with Shadow DOM v1, CSS custom properties, and crypto.subtle (all evergreen browsers). No IE11.
License
MIT © Your Name