npm package: @sparkvault/sdk
Description: SparkVault JavaScript SDK - Identity Verification and Encrypted Vaults
Author: SparkVault
Latest version: 1.21.11

# SparkVault JavaScript SDK

The official SparkVault JavaScript SDK for browser applications. Add passwordless authentication to your app in minutes with zero-config integration or full programmatic control.

[![npm version](https://img.shields.io/npm/v/@sparkvault/sdk.svg)](https://www.npmjs.com/package/@sparkvault/sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Table of Contents

- [Installation](#installation)
- [Zero-Config Integration](#zero-config-integration)
- [Manual Initialization](#manual-initialization)
- [Identity Verification](#identity-verification)
- [Backend Token Verification](#backend-token-verification)
- [React Integration](#react-integration)
- [Error Handling](#error-handling)
- [TypeScript Support](#typescript-support)
- [Browser Support](#browser-support)
- [Build & Release](#build--release)
- [License](#license)

## Installation

### CDN (Recommended)

```html
<script src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"></script>
```

### npm / yarn

```bash
npm install @sparkvault/sdk
# or
yarn add @sparkvault/sdk
```

**Bundle Formats:**
- `sparkvault.js` — UMD bundle for browsers (recommended)
- `sparkvault.esm.js` — ES modules for modern bundlers
- `sparkvault.cjs.js` — CommonJS for Node.js

## Zero-Config Integration

Add SparkVault authentication to your site with a single script tag — no JavaScript required!

```html
<!-- Add this single script tag -->
<script
  async
  src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"
  data-account-id="acc_YOUR_ACCOUNT_ID"
  data-attach-selector=".login-btn"
  data-success-url="/auth/verify-token"
></script>

<!-- Any element matching the selector becomes a login button -->
<button class="login-btn">Sign In</button>
<a href="#" class="login-btn">Create Account</a>
```

That's it! The SDK automatically:
- Initializes with your account ID
- Attaches click handlers to all matching elements
- Opens the verification modal when clicked
- POSTs the result to your success URL
- Watches for dynamically added elements (MutationObserver)

### Data Attributes

| Attribute | Required | Description |
|-----------|----------|-------------|
| `data-account-id` | Yes | Your SparkVault account ID (required for auto-init) |
| `data-attach-selector` | No | CSS selector for elements to attach click handlers |
| `data-success-url` | No | URL to POST `{ token, identity, identityType }` on success |
| `data-success-function` | No | Global function name to call on success (e.g., `"MyApp.auth.onSuccess"`) |
| `data-error-url` | No | URL to redirect on error (appends `?error=message`) |
| `data-error-function` | No | Global function name to call on error |
| `data-preload-config` | No | Set to `"false"` to defer config loading. Default: true |
| `data-timeout` | No | Request timeout in milliseconds. Default: 30000 |
| `data-debug` | No | Set to `"true"` for verbose console logging |

### Complete Example

```html
<script
  async
  src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"
  data-account-id="acc_YOUR_ACCOUNT_ID"
  data-attach-selector=".js-sparkvault-auth"
  data-success-url="https://example.com/auth/verify-token"
  data-success-function="onSparkVaultSuccess"
  data-error-function="onSparkVaultError"
  data-debug="true"
></script>

<script>
  // Optional: Define callback functions
  function onSparkVaultSuccess(result) {
    console.log('Verified:', result.identity);
    console.log('Token:', result.token);
    // The SDK will also POST to data-success-url
  }

  function onSparkVaultError(error) {
    console.error('Error:', error.message);
    alert('Authentication failed. Please try again.');
  }
</script>

<!-- Multiple elements can use the same selector -->
<button class="js-sparkvault-auth">Sign In</button>
<a href="#" class="js-sparkvault-auth">Login with SparkVault</a>
```

### Success URL Response

When you specify `data-success-url`, the SDK POSTs the verification result:

```json
{
  "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
  "identity": "user@example.com",
  "identityType": "email"
}
```

Your backend can respond with redirect instructions:

```javascript
// Option 1: Redirect to a URL
{ "redirectUrl": "/dashboard" }
// or
{ "redirect_url": "/dashboard" }

// Option 2: Reload the current page
{ "reload": true }

// Option 3: Return data (no automatic action)
{ "success": true, "user": { "id": 123, "email": "user@example.com" } }
```

### Manual Trigger from JavaScript

You can also trigger authentication programmatically using the `SparkVault` global:

```javascript
// Trigger authentication from anywhere in your code
SparkVault.identity.pop();

// This uses your configured success/error handlers
```

```html
<button onclick="SparkVault.identity.pop()">
  Sign In with SparkVault
</button>
```

## Manual Initialization

For full control over the SDK, initialize it manually in JavaScript:

```javascript
import SparkVault from '@sparkvault/sdk';

const sparkvault = SparkVault.init({
  accountId: 'acc_YOUR_ACCOUNT_ID'  // Required
});

// The SDK is now ready to use
const result = await sparkvault.identity.pop();
```

### Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `accountId` | `string` | **required** | Your SparkVault account ID (starts with `acc_`) |
| `timeout` | `number` | `30000` | HTTP request timeout in milliseconds |
| `preloadConfig` | `boolean` | `true` | Preload Identity config for instant modal |

### CDN Usage

```html
<script src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"></script>
<script>
  const sparkvault = SparkVault.init({
    accountId: 'acc_YOUR_ACCOUNT_ID'
  });

  document.getElementById('login-btn').addEventListener('click', async () => {
    const result = await sparkvault.identity.pop();
    console.log('Verified:', result.identity);
  });
</script>
```

## Identity Verification

The Identity module provides passwordless authentication through a beautiful, customizable modal. Users can verify via passkeys, email codes, SMS codes, SparkLink, or social providers.

### Basic Usage

```javascript
// Open the verification modal
const result = await sparkvault.identity.pop();

// User verified!
console.log(result.identity);      // "user@example.com" or "+14155551234"
console.log(result.identityType);  // "email" or "phone"
console.log(result.token);         // Signed JWT token

// Send the token to your backend for verification
await fetch('/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token: result.token })
});
```

### pop() Options

| Option | Type | Description |
|--------|------|-------------|
| `email` | `string` | Pre-fill email address (mutually exclusive with phone) |
| `phone` | `string` | Pre-fill phone in E.164 format, e.g. `"+14155551234"` |
| `backdropBlur` | `boolean` | Override backdrop blur setting from app config |
| `onCancel` | `function` | Callback when user closes modal without verifying |
| `onSuccess` | `function` | Callback on success (before promise resolves) |
| `onError` | `function` | Callback on error (before promise rejects) |

### pop() Result

| Field | Type | Description |
|-------|------|-------------|
| `token` | `string` | Signed JWT token (Ed25519). Verify this on your backend. |
| `identity` | `string` | The verified email address or phone number |
| `identityType` | `"email" \| "phone"` | Type of identity that was verified |

### Example with Options

```javascript
try {
  const result = await sparkvault.identity.pop({
    // Pre-fill the email field
    email: 'user@example.com',

    // Handle cancellation
    onCancel: () => {
      console.log('User cancelled');
    }
  });

  // Success! Send token to your backend
  await loginWithToken(result.token);

} catch (error) {
  if (error.code === 'user_cancelled') {
    // User closed the modal
  } else {
    // Handle other errors
    console.error('Verification failed:', error.message);
  }
}
```

### Authentication Methods

Authentication methods are configured in your Identity App settings. The SDK automatically shows only the methods you have enabled.

| Method | Description | Identity Type |
|--------|-------------|---------------|
| `passkey` | WebAuthn/FIDO2 biometric or security key | Email |
| `totp_email` | 6-digit code sent via email | Email |
| `totp_sms` | 6-digit code sent via SMS | Phone |
| `totp_voice` | 6-digit code sent via voice call | Phone |
| `sparklink` | One-click sign-in link via email | Email |
| `social_google` | Sign in with Google | Email |
| `social_apple` | Sign in with Apple | Email |
| `social_microsoft` | Sign in with Microsoft | Email |
| `social_github` | Sign in with GitHub | Email |
| `social_facebook` | Sign in with Facebook | Email |
| `social_linkedin` | Sign in with LinkedIn | Email |

## Backend Token Verification

> **Security Critical:** Always verify tokens on your backend. Never trust the frontend result alone. Tokens are signed with Ed25519 — use the JWKS endpoint to verify the signature.

### Node.js

```javascript
import * as jose from 'jose';

const ACCOUNT_ID = 'acc_YOUR_ACCOUNT_ID';
const IDENTITY_URL = 'https://api.sparkvault.com/v1/apps/identity';

app.post('/api/auth/login', async (req, res) => {
  const { token } = req.body;

  try {
    // Fetch public keys and verify signature
    const jwksUrl = `${IDENTITY_URL}/${ACCOUNT_ID}/.well-known/jwks.json`;
    const JWKS = jose.createRemoteJWKSet(new URL(jwksUrl));

    const { payload } = await jose.jwtVerify(token, JWKS, {
      issuer: `${IDENTITY_URL}/${ACCOUNT_ID}`,
      audience: ACCOUNT_ID
    });

    // Token is valid! Create user session
    const user = await findOrCreateUser(payload.identity, payload.identity_type);
    req.session.userId = user.id;

    res.json({ success: true, user });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});
```

### Python

```python
import jwt
from jwt import PyJWKClient

ACCOUNT_ID = 'acc_YOUR_ACCOUNT_ID'
IDENTITY_URL = 'https://api.sparkvault.com/v1/apps/identity'

def verify_identity_token(token: str):
    """Verify a SparkVault Identity token and return the claims."""
    jwks_url = f"{IDENTITY_URL}/{ACCOUNT_ID}/.well-known/jwks.json"
    jwks_client = PyJWKClient(jwks_url)

    # Get the signing key
    signing_key = jwks_client.get_signing_key_from_jwt(token)

    # Verify and decode
    claims = jwt.decode(
        token,
        signing_key.key,
        algorithms=["EdDSA"],
        issuer=f"{IDENTITY_URL}/{ACCOUNT_ID}",
        audience=ACCOUNT_ID
    )

    return claims

# Usage in Flask
@app.route('/api/auth/login', methods=['POST'])
def login():
    token = request.json.get('token')

    try:
        claims = verify_identity_token(token)

        # Create session
        user = find_or_create_user(claims['identity'], claims['identity_type'])
        session['user_id'] = user.id

        return jsonify({'success': True, 'user': user.to_dict()})
    except jwt.InvalidTokenError as e:
        return jsonify({'error': 'Invalid token'}), 401
```

### Go

```go
package main

import (
    "github.com/lestrrat-go/jwx/v2/jwk"
    "github.com/lestrrat-go/jwx/v2/jwt"
)

const (
    AccountID   = "acc_YOUR_ACCOUNT_ID"
    IdentityURL = "https://api.sparkvault.com/v1/apps/identity"
)

func verifyToken(tokenString string) (jwt.Token, error) {
    jwksURL := fmt.Sprintf("%s/%s/.well-known/jwks.json", IdentityURL, AccountID)

    // Fetch JWKS
    keySet, err := jwk.Fetch(context.Background(), jwksURL)
    if err != nil {
        return nil, err
    }

    // Parse and verify token
    token, err := jwt.Parse(
        []byte(tokenString),
        jwt.WithKeySet(keySet),
        jwt.WithIssuer(fmt.Sprintf("%s/%s", IdentityURL, AccountID)),
        jwt.WithAudience(AccountID),
    )
    if err != nil {
        return nil, err
    }

    return token, nil
}

// Usage in HTTP handler
func loginHandler(w http.ResponseWriter, r *http.Request) {
    var body struct {
        Token string `json:"token"`
    }
    json.NewDecoder(r.Body).Decode(&body)

    token, err := verifyToken(body.Token)
    if err != nil {
        http.Error(w, "Invalid token", http.StatusUnauthorized)
        return
    }

    identity, _ := token.Get("identity")
    // Create session with identity...
}
```

### Ruby

```ruby
require 'jwt'
require 'net/http'
require 'json'

ACCOUNT_ID = 'acc_YOUR_ACCOUNT_ID'
IDENTITY_URL = 'https://api.sparkvault.com/v1/apps/identity'

def verify_identity_token(token)
  jwks_url = "#{IDENTITY_URL}/#{ACCOUNT_ID}/.well-known/jwks.json"

  # Fetch JWKS
  uri = URI(jwks_url)
  response = Net::HTTP.get(uri)
  jwks = JSON.parse(response)

  # Decode and verify
  JWT.decode(
    token,
    nil,
    true,
    {
      algorithms: ['EdDSA'],
      iss: "#{IDENTITY_URL}/#{ACCOUNT_ID}",
      aud: ACCOUNT_ID,
      verify_iss: true,
      verify_aud: true,
      jwks: jwks
    }
  ).first
end

# Usage in Rails controller
class AuthController < ApplicationController
  def login
    claims = verify_identity_token(params[:token])

    user = User.find_or_create_by(email: claims['identity'])
    session[:user_id] = user.id

    render json: { success: true, user: user }
  rescue JWT::DecodeError => e
    render json: { error: 'Invalid token' }, status: :unauthorized
  end
end
```

### Token Claims

When decoded, the JWT token contains these claims:

```json
{
  "iss": "https://api.sparkvault.com/v1/apps/identity/acc_xxx",
  "sub": "identity:a1b2c3d4e5f67890",
  "aud": "acc_xxx",
  "identity": "user@example.com",
  "identity_type": "email",
  "method": "totp_email",
  "verified_at": 1703977195,
  "iat": 1703977200,
  "exp": 1703980800,
  "jti": "tok_unique_id"
}
```

| Claim | Type | Description |
|-------|------|-------------|
| `iss` | `string` | Issuer URL — always verify this matches expected value |
| `sub` | `string` | Subject — hashed identity for consistent user identification |
| `aud` | `string` | Audience — your account ID |
| `identity` | `string` | The verified email address or phone number |
| `identity_type` | `string` | `"email"` or `"phone"` |
| `method` | `string` | Authentication method used (e.g., `"totp_email"`, `"passkey"`) |
| `verified_at` | `number` | Unix timestamp when identity was verified |
| `iat` | `number` | Issued at timestamp |
| `exp` | `number` | Expiration timestamp |
| `jti` | `string` | Unique token ID for replay protection |

## React Integration

```jsx
import { useState, useCallback } from 'react';
import SparkVault from '@sparkvault/sdk';

// Initialize once, outside component
const sparkvault = SparkVault.init({
  accountId: 'acc_YOUR_ACCOUNT_ID'
});

function useSparkVaultAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const login = useCallback(async (options = {}) => {
    setLoading(true);
    setError(null);

    try {
      // Open verification modal
      const result = await sparkvault.identity.pop(options);

      // Send token to backend
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: result.token })
      });

      if (!response.ok) throw new Error('Login failed');

      const data = await response.json();
      setUser(data.user);
      return data.user;

    } catch (err) {
      if (err.code !== 'user_cancelled') {
        setError(err.message);
      }
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  const logout = useCallback(async () => {
    await fetch('/api/auth/logout', { method: 'POST' });
    setUser(null);
  }, []);

  return { user, loading, error, login, logout };
}

// Usage in component
function LoginPage() {
  const { user, loading, error, login, logout } = useSparkVaultAuth();

  if (user) {
    return (
      <div>
        <p>Welcome, {user.email}!</p>
        <button onClick={logout}>Sign out</button>
      </div>
    );
  }

  return (
    <div>
      <button onClick={() => login()} disabled={loading}>
        {loading ? 'Verifying...' : 'Sign in'}
      </button>
      {error && <p className="error">{error}</p>}
    </div>
  );
}
```

## Error Handling

The SDK throws typed errors that you can catch and handle appropriately.

```javascript
import SparkVault, {
  UserCancelledError,
  ValidationError,
  NetworkError,
  TimeoutError
} from '@sparkvault/sdk';

try {
  const result = await sparkvault.identity.pop();
} catch (error) {
  switch (error.code) {
    case 'user_cancelled':
      // User closed the modal — not necessarily an error
      console.log('User cancelled verification');
      break;

    case 'validation_error':
      // Invalid input (e.g., malformed email)
      console.error('Validation error:', error.message);
      break;

    case 'network_error':
      // Network connectivity issue
      console.error('Network error — please check your connection');
      break;

    case 'timeout_error':
      // Request timed out
      console.error('Request timed out — please try again');
      break;

    case 'popup_blocked':
      // Browser blocked the popup
      console.error('Please allow popups for this site');
      break;

    default:
      console.error('Unexpected error:', error.message);
  }
}
```

### Error Types

| Error Class | Code | Description |
|-------------|------|-------------|
| `UserCancelledError` | `user_cancelled` | User closed the modal without completing verification |
| `ValidationError` | `validation_error` | Invalid input or configuration |
| `NetworkError` | `network_error` | Network connectivity failure |
| `TimeoutError` | `timeout_error` | Request exceeded timeout limit |
| `PopupBlockedError` | `popup_blocked` | Browser blocked the popup window |
| `AuthenticationError` | `authentication_error` | Authentication failed (e.g., expired token) |

## TypeScript Support

The SDK is written in TypeScript and includes full type definitions.

```typescript
import SparkVault, {
  // Configuration
  SparkVaultConfig,

  // Identity types
  VerifyOptions,
  VerifyResult,
  TokenClaims,

  // Error types
  SparkVaultError,
  UserCancelledError,
  ValidationError,
  NetworkError,
  TimeoutError,
  PopupBlockedError,
} from '@sparkvault/sdk';

// Fully typed configuration
const config: SparkVaultConfig = {
  accountId: 'acc_YOUR_ACCOUNT_ID',
  timeout: 30000,
};

const sparkvault = SparkVault.init(config);

// Fully typed options and result
const options: VerifyOptions = {
  email: 'user@example.com',
  onCancel: () => console.log('Cancelled'),
};

const result: VerifyResult = await sparkvault.identity.pop(options);
// result.token: string
// result.identity: string
// result.identityType: 'email' | 'phone'
```

## Content Security Policy (CSP)

If your site uses a Content Security Policy, you must allow the SparkVault domains:

```
connect-src https://api.sparkvault.com wss://ws.sparkvault.com;
script-src https://cdn.sparkvault.com;
```

| Directive | Domain | Reason |
|-----------|--------|--------|
| `connect-src` | `https://api.sparkvault.com` | API requests (identity verification, config) |
| `connect-src` | `wss://ws.sparkvault.com` | WebSocket for SparkLink push notifications |
| `script-src` | `https://cdn.sparkvault.com` | SDK script (if using CDN installation) |

## Best Practices

1. **Initialize Once** — Create the SparkVault instance once when your app loads, not on every login attempt. The SDK preloads configuration for instant modal opening.

2. **Always Verify Server-Side** — Never trust client-side token validation in production. Always verify the JWT signature on your backend using the JWKS endpoint before creating a session.

3. **Handle Cancellation Gracefully** — Users may close the modal without completing verification. This throws a `UserCancelledError` — handle it gracefully rather than showing an error message.

4. **Cache JWKS Keys** — The JWKS endpoint returns public keys with caching headers. Use a library like `jose` that handles caching automatically, or cache keys for 5-10 minutes.

## Browser Support

The SDK supports all modern browsers:

- Chrome 80+
- Firefox 75+
- Safari 13.1+
- Edge 80+

**Note:** Passkey authentication requires WebAuthn support. On unsupported browsers, passkey will not appear as an option — other methods will still work.

## Build & Release

### Setup

```bash
git clone https://github.com/Spark-Vault/sdk-js.git
cd sdk-js
npm install
```

### Scripts

```bash
npm run build        # Build all bundles
npm run build:watch  # Build with watch mode
npm run typecheck    # Run TypeScript type checking
npm run lint         # Run ESLint
npm run test         # Run tests
npm run test:watch   # Run tests in watch mode
```

### Project Structure

```
sdk-js/
├── src/
│   ├── index.ts           # Main entry point
│   ├── config.ts          # Configuration management
│   ├── http.ts            # HTTP client
│   ├── errors.ts          # Error types
│   └── identity/          # Identity module
├── dist/                  # Built bundles
├── tests/                 # Test files
├── .github/workflows/     # CI/CD workflows
├── rollup.config.js       # Rollup bundler config
├── tsconfig.json          # TypeScript config
└── package.json
```

## License

MIT License - see [LICENSE](LICENSE) for details.

## Links

- [SparkVault Documentation](https://sparkvault.com/api/docs)
- [SDK API Reference](https://sparkvault.com/api/docs/sdk)
- [GitHub Repository](https://github.com/Spark-Vault/sdk-js)
- [npm Package](https://www.npmjs.com/package/@sparkvault/sdk)
- [Report Issues](https://github.com/Spark-Vault/sdk-js/issues)