npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

zerokey

v1.1.3

Published

Zero-knowledge cross-domain secret sharing library using ECDH encryption

Downloads

7

Readme

Zerokey

A zero-knowledge cross-domain secret sharing library that enables secure transfer of secrets between different domains without the server ever seeing the secret. Uses ECDH asymmetric encryption to ensure complete privacy.

Features

  • 🔐 Zero-Knowledge: Server never sees the actual secret
  • 🔗 Cross-Domain: Securely transfer secrets between different domains
  • 🛡️ ECDH Encryption: Uses P-256 curve with AES-GCM for hybrid encryption
  • 📦 No Dependencies: Uses only the Web Crypto API
  • ⏱️ Auto-Expiry: Pending keys expire after 5 minutes
  • 🎯 CSRF Protection: Built-in state parameter validation
  • 🧪 Well-Tested: Comprehensive Playwright test suite

Installation

npm install zerokey

Quick Start

On your app domain (app.example.com)

import { initSecretClient, getSecret } from 'zerokey/client';

// Start the flow (usually on app load)
await initSecretClient('https://auth.example.com/secret');

// Later, when you need the secret
const encryptionKey = getSecret();
if (!encryptionKey) {
  // User hasn't authenticated yet
}

On your auth domain (auth.example.com)

import { initSecretServer, setSecret } from 'zerokey/server';

// Set up the handler
initSecretServer();

// After user logs in and you derive the key
const encryptionKey = deriveKeyFromPassword(password, salt);
setSecret(encryptionKey);
// This will automatically redirect back

How It Works

  1. App domain generates an ephemeral ECDH keypair
  2. App domain redirects to auth domain with the public key
  3. Auth domain encrypts the secret with the public key
  4. Auth domain redirects back with encrypted secret in URL fragment
  5. App domain decrypts using the private key

The key insight is that URL fragments (#) are never sent to servers, ensuring the encrypted secret remains client-side only.

API Reference

Client API (zerokey/client)

initSecretClient(authUrl: string): Promise<void>

Initiates the secret transfer flow. If returning from auth domain, decrypts and stores the secret. Otherwise, generates a new keypair and redirects to the auth domain.

await initSecretClient('https://auth.example.com/secret');

getSecret(): string | null

Retrieves the decrypted secret from localStorage.

const secret = getSecret();
if (secret) {
  // Use the secret
}

clearSecret(): void

Clears the stored secret and any pending keys.

clearSecret();

Server API (zerokey/server)

initSecretServer(options?: SecretServerOptions): void

Initializes the server handler on the auth domain. Parses query parameters and prepares for secret transfer.

// Basic usage
initSecretServer();

// With domain validation for enhanced security
initSecretServer({
  validateCallbackUrl: (url) => url.startsWith('https://myapp.com')
});

Options:

  • validateCallbackUrl?: (url: string) => boolean - Optional callback to validate redirect URLs. This provides protection against unauthorized domains requesting secrets.

setSecret(secret: string): void

Sets the secret to be encrypted and transferred back to the app domain.

// After user authenticates
const encryptionKey = deriveKey(password, salt);
setSecret(encryptionKey);

Security Considerations

  1. URL Fragments: The library uses URL fragments (#) which are never sent to servers
  2. One-Time Keys: Each transfer uses a fresh ephemeral keypair
  3. Auto-Expiry: Pending keys expire after 5 minutes
  4. CSRF Protection: State parameter prevents replay attacks
  5. HTTPS Required: Always use HTTPS in production
  6. Domain Validation: Use validateCallbackUrl to restrict which domains can request secrets

Preventing Unauthorized Access

By default, any domain can request a secret from your auth server. To prevent malicious sites from obtaining secrets, use the validateCallbackUrl option:

// Only allow your specific app domain
initSecretServer({
  validateCallbackUrl: (url) => url.startsWith('https://myapp.com')
});

// Allow multiple trusted domains
initSecretServer({
  validateCallbackUrl: (url) => {
    const trustedDomains = [
      'https://app.example.com',
      'https://staging.example.com',
      'http://localhost:3000' // for development only - always use HTTPS in production
    ];
    return trustedDomains.some(domain => url.startsWith(domain));
  }
});

This prevents scenarios where dodgysite.com could redirect users to your auth server and attempt to obtain their secrets.

Testing

The library includes comprehensive Playwright tests that verify the complete cross-domain flow.

# Install dependencies
npm install

# Run tests
npm test

# Run tests in headed mode (see the browser)
npm run test:headed

# Debug tests
npm run test:debug

Test Coverage

  • ✅ Happy path flow
  • ✅ URL fragment handling
  • ✅ LocalStorage persistence
  • ✅ Key expiration (5 min timeout)
  • ✅ CSRF protection
  • ✅ Error handling
  • ✅ Browser navigation
  • ✅ Multiple concurrent flows

Browser Support

Requires browsers with Web Crypto API support:

  • Chrome 37+
  • Firefox 34+
  • Safari 11+
  • Edge 79+

Example Implementation

Complete Auth Page

<!DOCTYPE html>
<html>
<head>
  <title>Login - Auth Domain</title>
</head>
<body>
  <h1>Login</h1>
  <form id="loginForm">
    <input type="email" id="email" required>
    <input type="password" id="password" required>
    <button type="submit">Login</button>
  </form>

  <script type="module">
    import { initSecretServer, setSecret } from 'zerokey/server';
    
    // Initialize on page load
    initSecretServer();
    
    document.getElementById('loginForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      
      const email = document.getElementById('email').value;
      const password = document.getElementById('password').value;
      
      // Authenticate user (your logic here)
      const { salt, iterations } = await authenticateUser(email, password);
      
      // Derive encryption key from password
      const encryptionKey = await deriveKey(password, salt, iterations);
      
      // Send the key back encrypted
      setSecret(encryptionKey);
    });
  </script>
</body>
</html>

Complete App Page

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="app">
    <h1>Welcome to My App</h1>
    <div id="status">Loading...</div>
  </div>

  <script type="module">
    import { initSecretClient, getSecret } from 'zerokey/client';
    
    // Check for existing secret or start flow
    async function initialize() {
      const secret = getSecret();
      
      if (secret) {
        // User is authenticated
        document.getElementById('status').textContent = 'Authenticated';
        initializeApp(secret);
      } else {
        // Need to authenticate
        document.getElementById('status').textContent = 'Redirecting to login...';
        await initSecretClient('https://auth.example.com/login');
      }
    }
    
    // Listen for secret ready event
    window.addEventListener('zerokey:ready', () => {
      const secret = getSecret();
      document.getElementById('status').textContent = 'Authenticated';
      initializeApp(secret);
    });
    
    initialize();
  </script>
</body>
</html>

Advanced Usage

Custom Expiration Time

While the default 5-minute expiration is recommended, you can implement custom logic:

// In your app, before calling initSecretClient
const CUSTOM_EXPIRY = 10 * 60 * 1000; // 10 minutes

// Override the storage method
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
  if (key === 'zerokey_pending') {
    const data = JSON.parse(value);
    data.customExpiry = Date.now() + CUSTOM_EXPIRY;
    value = JSON.stringify(data);
  }
  originalSetItem.call(this, key, value);
};

Multiple Secrets

You can transfer multiple secrets by encoding them:

// On auth domain
const secrets = {
  encryptionKey: derivedKey,
  apiToken: userApiToken,
  refreshToken: refreshToken
};
setSecret(JSON.stringify(secrets));

// On app domain
const secretsJson = getSecret();
const secrets = JSON.parse(secretsJson);

Troubleshooting

Secret not received

  1. Check browser console for errors
  2. Verify both domains use HTTPS in production
  3. Ensure query parameters are properly encoded
  4. Check if pending key expired (5 min timeout)

CORS issues

This library doesn't make any cross-origin requests. All communication happens via redirects and URL parameters.

localStorage not available

The library requires localStorage. For Safari private browsing, consider using a fallback to sessionStorage.

License

MIT

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (npm test)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request