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 🙏

© 2026 – Pkg Stats / Ryan Hefner

ratatoskr-client

v0.2.0

Published

Browser client library for Ratatoskr - automerge-repo sync server with authentication

Downloads

182

Readme

ratatoskr-client

Browser client library for Ratatoskr - an automerge-repo sync server with authentication and per-document permissions.

Features

  • 🔐 Popup-based OIDC authentication - Seamless login flow
  • 📡 WebSocket sync - Real-time document synchronization via automerge-repo
  • 📄 Document management - Create, list, delete documents
  • 🔑 Access control - Manage document ACLs and permissions
  • 🎫 API tokens - Generate tokens for CLI/programmatic access
  • 🌐 Works everywhere - Browser, bundlers, or direct ESM import
  • 📴 Offline-first - Create and edit documents offline, sync when back online
  • 💾 Persistent storage - Documents saved to IndexedDB survive browser sessions

Quick Start

Installation (npm/bun/yarn)

npm install ratatoskr-client @automerge/automerge-repo

Direct ESM Import (No Build Step)

<script type="module">
  import { RatatoskrClient } from 'https://esm.sh/ratatoskr-client';
  import { Repo } from 'https://esm.sh/@automerge/automerge-repo';
  
  const client = new RatatoskrClient({
    serverUrl: window.location.origin
  });
  
  // Login via popup
  const user = await client.login();
  console.log('Logged in as:', user.name);
  
  // Get automerge repo for real-time sync
  const repo = client.getRepo();
</script>

Complete Example

<!DOCTYPE html>
<html>
<head>
  <title>Ratatoskr Example</title>
</head>
<body>
  <button id="login">Login</button>
  <button id="create">Create Document</button>
  <div id="output"></div>

  <script type="module">
    import { RatatoskrClient } from 'https://esm.sh/ratatoskr-client';
    
    const client = new RatatoskrClient({
      serverUrl: window.location.origin
    });
    
    // Login button
    document.getElementById('login').onclick = async () => {
      try {
        const user = await client.login();
        document.getElementById('output').textContent = `Hello, ${user.name}!`;
      } catch (err) {
        alert('Login failed: ' + err.message);
      }
    };
    
    // Create document button
    document.getElementById('create').onclick = async () => {
      const doc = await client.createDocument({
        id: `doc:${crypto.randomUUID()}`,
        type: 'notes'
      });
      console.log('Created:', doc);
    };
  </script>
</body>
</html>

API Reference

RatatoskrClient

The main client class for interacting with a Ratatoskr server.

Constructor

new RatatoskrClient(options: RatatoskrClientOptions)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | serverUrl | string | ✅ | - | Base URL of the Ratatoskr server | | tokenStorageKey | string | ❌ | "ratatoskr:token" | localStorage key for token persistence | | enableOfflineSupport | boolean | ❌ | true | Enable offline-first document creation with IndexedDB storage |

Authentication Methods

login(): Promise<User>

Opens a popup for OIDC authentication. Returns user info on success. Requires network connectivity.

const user = await client.login();
// { id: "alice", name: "Alice Smith", email: "[email protected]" }
logout(): void

Clears stored credentials and disconnects.

client.logout();
isAuthenticated(): boolean

Check if user has a stored token (doesn't validate the token).

if (client.isAuthenticated()) {
  // Token exists (may still be expired)
}
hasStoredCredentials(): boolean

Check if both token and user info are cached. Useful for showing "Welcome back" UI even when offline.

if (client.hasStoredCredentials()) {
  console.log(`Welcome back, ${client.getUser()?.name}!`);
}
getUser(): User | null

Get the current user object. When offline with stored credentials, returns the cached user.

fetchUserInfo(): Promise<User>

Fetch current user info from the server. Updates the cached user info. Useful to validate stored tokens.

try {
  const user = await client.fetchUserInfo();
  console.log('Token valid for:', user.name);
} catch {
  console.log('Token expired, need to login again');
}
validateToken(): Promise<boolean>

Validate the stored token by fetching user info. Returns true if valid, false if expired/invalid.

if (client.isAuthenticated()) {
  const isValid = await client.validateToken();
  if (!isValid) {
    // Token expired, prompt re-login
  }
}

Automerge Integration

getRepo(): Repo

Get or create the automerge-repo instance connected to the server.

const repo = client.getRepo();

// Find an existing document
const handle = repo.find('doc:abc123');
await handle.whenReady();
const doc = handle.docSync();

// Create a new document
const newHandle = repo.create();
newHandle.change(doc => {
  doc.title = 'My Document';
  doc.items = [];
});
disconnect(): void

Disconnect from the server and clean up the repo.

client.disconnect();

Document Management

createDocument(request): Promise<DocumentMetadata>

Create a new document on the server.

const doc = await client.createDocument({
  id: 'doc:my-unique-id',      // Required: doc:, app:, or eph: prefix
  type: 'notes',                // Optional: document type
  acl: [                        // Optional: initial ACL
    { principal: 'bob', permission: 'write' },
    { principal: 'public', permission: 'read' }
  ],
  expiresAt: '2025-12-31'       // Optional: auto-delete date
});

Document ID Prefixes: | Prefix | Example | Description | |--------|---------|-------------| | doc: | doc:abc123 | Regular document with full ACL support | | app: | app:myapp | Per-user-per-app private document | | eph: | eph:session | Ephemeral document (no persistence) |

listDocuments(): Promise<ListDocumentsResponse>

List documents you own or have access to.

const { owned, accessible } = await client.listDocuments();

console.log('My documents:', owned);
console.log('Shared with me:', accessible);
getDocument(id): Promise<DocumentMetadata>

Get metadata for a specific document.

const doc = await client.getDocument('doc:abc123');
console.log('Owner:', doc.owner);
console.log('Size:', doc.size);
deleteDocument(id): Promise<void>

Delete a document (owner only).

await client.deleteDocument('doc:abc123');

Access Control

getDocumentACL(id): Promise<ACLEntry[]>

Get the access control list for a document.

const acl = await client.getDocumentACL('doc:abc123');
// [{ principal: 'bob', permission: 'write' }]
setDocumentACL(id, acl): Promise<void>

Update a document's ACL (owner only).

await client.setDocumentACL('doc:abc123', [
  { principal: 'bob', permission: 'write' },
  { principal: 'charlie', permission: 'read' },
  { principal: 'public', permission: 'read' }  // Anyone can read
]);

ACL Entries: | Principal | Description | |-----------|-------------| | User ID | Grant access to specific user | | Document ID | Inherit ACL from another document | | "public" | Grant access to everyone (including anonymous) |

Permissions: | Permission | Allows | |------------|--------| | "read" | Read document content | | "write" | Read and write document content |

API Tokens

createApiToken(name, scopes?, expiresAt?): Promise<{token, id}>

Create an API token for CLI or programmatic access.

const { token, id } = await client.createApiToken(
  'my-cli-tool',
  ['read', 'write'],       // Optional scopes
  '2026-01-01T00:00:00Z'   // Optional expiration
);

console.log('Save this token:', token);  // Only shown once!
listApiTokens(): Promise<ApiToken[]>

List all your API tokens (tokens values are not returned).

const tokens = await client.listApiTokens();
for (const t of tokens) {
  console.log(`${t.name} (${t.id}) - last used: ${t.lastUsedAt}`);
}
deleteApiToken(id): Promise<void>

Revoke an API token.

await client.deleteApiToken('token-id-here');

Offline-First Document Creation

Documents can be created and edited offline. They'll be registered on the server when connectivity and authentication are restored.

createDocumentOffline<T>(initialValue, options?): Promise<string>

Create a document that works offline. Returns the document ID immediately.

// Create document offline - works even without network
const docId = await client.createDocumentOffline(
  { title: 'My Notes', items: [] },
  { type: 'notes' }
);

// Document is usable immediately via automerge
const handle = client.getRepo().find(docId);
handle.change(doc => {
  doc.items.push('First item');
});

// When online + authenticated, document auto-registers on server

Note: Documents created offline are private (no ACLs) until you're online. You can set ACLs after the document syncs using setDocumentACL().

getDocumentSyncStatus(documentId): Promise<DocumentStatusEntry | undefined>

Get the sync status of a document.

const status = await client.getDocumentSyncStatus(docId);
if (status) {
  console.log('Status:', status.status); // 'local' | 'syncing' | 'synced'
  console.log('Registered on server:', status.serverRegistered);
}
getConnectivityState(): ConnectivityState

Get the current connectivity state.

const state = client.getConnectivityState();
// 'online' | 'offline' | 'connecting'
processPendingOperations(): Promise<{processed, failed}>

Force processing of pending sync operations. Useful after login.

await client.login();
const result = await client.processPendingOperations();
console.log(`Synced ${result.processed} documents`);
getPendingOperationsCount(): Promise<number>

Get the number of pending sync operations.

getUnsyncedDocuments(): Promise<DocumentStatusEntry[]>

Get all documents that haven't been synced to the server yet.

onSyncEvent(listener): () => void

Subscribe to sync events. Returns an unsubscribe function.

const unsubscribe = client.onSyncEvent((event) => {
  switch (event.type) {
    case 'connectivity:changed':
      console.log('Connectivity:', event.connectivity);
      break;
    case 'document:status-changed':
      console.log(`Document ${event.documentId}: ${event.status}`);
      break;
    case 'sync:completed':
      console.log(`Synced ${event.processed} documents`);
      break;
    case 'auth:required':
      console.log('Please log in to sync');
      break;
    case 'auth:token-expired':
      console.log('Token expired, please log in again');
      break;
  }
});

// Later: unsubscribe()

Event Types: | Event | Description | |-------|-------------| | connectivity:changed | Network/server connection state changed | | document:status-changed | A document's sync status changed | | sync:started | Sync processing started | | sync:completed | Sync processing completed | | sync:error | Sync error occurred | | auth:required | Authentication needed to continue syncing | | auth:token-expired | Token is invalid/expired |

isOfflineEnabled(): boolean

Check if offline support is enabled.

destroy(): void

Cleanup all resources including IndexedDB connections.

Types

interface User {
  id: string;
  email?: string;
  name?: string;
}

interface DocumentMetadata {
  id: string;
  owner: string;
  type: string | null;
  size: number;
  expiresAt: string | null;
  createdAt: string;
  updatedAt: string;
}

interface ACLEntry {
  principal: string;           // User ID, document ID, or "public"
  permission: 'read' | 'write';
}

interface ApiToken {
  id: string;
  name: string;
  scopes: string[];
  lastUsedAt: string | null;
  expiresAt: string | null;
  createdAt: string;
}

// Offline support types
type ConnectivityState = 'online' | 'offline' | 'connecting';

type DocumentSyncStatus = 'local' | 'syncing' | 'synced';

interface DocumentStatusEntry {
  documentId: string;
  status: DocumentSyncStatus;
  serverRegistered: boolean;
  createdAt: string;
  lastSyncAttempt?: string;
  error?: string;
}

type SyncEventType =
  | 'sync:started'
  | 'sync:completed'
  | 'sync:error'
  | 'document:status-changed'
  | 'connectivity:changed'
  | 'auth:required'
  | 'auth:token-expired';

interface SyncEvent {
  type: SyncEventType;
  documentId?: string;
  status?: DocumentSyncStatus;
  connectivity?: ConnectivityState;
  error?: string;
  processed?: number;
  failed?: number;
}

RatatoskrNetworkAdapter

Low-level network adapter for custom automerge-repo setups.

import { Repo } from '@automerge/automerge-repo';
import { RatatoskrNetworkAdapter } from 'ratatoskr-client';

const adapter = new RatatoskrNetworkAdapter({
  serverUrl: 'https://your-server.com',
  token: 'your-auth-token'  // Optional
});

const repo = new Repo({
  network: [adapter],
  peerId: 'my-peer-id'
});

// Update token after login
adapter.setToken(newToken);

Usage Patterns

Single-File HTML Tool

<!DOCTYPE html>
<html>
<head>
  <title>My Tool</title>
  <script type="importmap">
    {
      "imports": {
        "ratatoskr-client": "https://esm.sh/ratatoskr-client?external=@automerge/automerge-repo",
        "@automerge/automerge-repo": "https://esm.sh/@automerge/automerge-repo"
      }
    }
  </script>
</head>
<body>
  <script type="module">
    import { RatatoskrClient } from 'ratatoskr-client';

    const client = new RatatoskrClient({
      serverUrl: window.location.origin
    });
    
    // Check for existing session
    if (client.isAuthenticated()) {
      try {
        await client.fetchUserInfo();
        startApp();
      } catch {
        client.logout();
        showLogin();
      }
    } else {
      showLogin();
    }
    
    function showLogin() {
      // Show login UI
    }
    
    async function startApp() {
      const repo = client.getRepo();
      // Use automerge-repo for real-time collaboration
    }
  </script>
</body>
</html>

React/Vue/Svelte App

// src/lib/ratatoskr.ts
import { RatatoskrClient } from 'ratatoskr-client';

export const client = new RatatoskrClient({
  serverUrl: import.meta.env.VITE_RATATOSKR_URL
});

// React hook example
export function useRatatoskr() {
  const [user, setUser] = useState(client.getUser());
  
  const login = async () => {
    const u = await client.login();
    setUser(u);
  };
  
  const logout = () => {
    client.logout();
    setUser(null);
  };
  
  return { client, user, login, logout };
}

CLI Tool with API Token

// Use the token directly with fetch or the network adapter
const response = await fetch(`${process.env.RATATOSKR_URL}/api/v1/documents`, {
  headers: {
    'Authorization': `Bearer ${process.env.RATATOSKR_TOKEN}`
  }
});

Error Handling

All async methods throw errors on failure:

try {
  await client.login();
} catch (err) {
  if (err.message.includes('popup')) {
    // Popup was blocked or closed
  } else if (err.message.includes('timeout')) {
    // Authentication took too long
  }
}

try {
  await client.createDocument({ id: 'doc:test' });
} catch (err) {
  // err.message contains server error details
}

Browser Compatibility

  • Modern browsers with ES2022 support
  • Requires crypto.randomUUID() (Chrome 92+, Firefox 95+, Safari 15.4+)
  • WebSocket support required

License

MIT