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

@affectively/auth

v5.0.0

Published

Shared authentication and cryptographic utilities for AFFECTIVELY

Downloads

43

Readme

@affectively/auth (Aegis Auth)

Decentralized Identity, Capabilities & Fine-Grained Access Control

A zero-dependency authentication and authorization library built on Web Crypto API. Implements UCAN (User Controlled Authorization Networks) with XPath-like node selection for surgical access control over your data tree, alongside custodial transaction-signing contracts for world-facing services.

Architectural Note: Why is auth separate from encryption?

In the Affectively ecosystem, we separate Integrity & Identity (this package) from Confidentiality (@affectively/zk-encryption).

  • Use @affectively/auth when you need to answer "Who are you and what can you do?" (UCAN tokens, DIDs, ECDSA Signatures, XPath Data Authorization).
  • Use @affectively/zk-encryption when you need to answer "Who can read this data?" (Zero-Knowledge E2EE, ECIES, AES). Keeping these separate ensures edge workers that only need to route UCAN traffic aren't forced to load heavy payload-encryption libraries.

Parent

Child


What Lives Here

  • Data Authorization Engine: Fine-grained access control with priority-based rules (access.ts)
  • XPath Node Selection: Query complex JSON data trees using XPath (xpath.ts)
  • Firebase-Style Rules Engine: Evaluate declarative string-based rules against data trees (rules.ts)
  • Deep UCAN Capabilities: Full UCAN issuance, attenuation, and delegation chains (token.ts, delegation.ts)
  • Revocation-Aware Checks: Token/device revocation primitives (ucanAuth.ts)
  • Custodial Signer Contract: Canonical contract types for action-scoped custodial signing (custodialSigner.ts)

Custodial Signer Contract

src/custodialSigner.ts is the canonical Aegis contract for action-scoped custodial signing across server and worker runtimes. It defines:

  • Action allowlist names
  • Typed payload contracts per action
  • Execute/health/signer metadata response shapes
  • Shared error-code surface for fail-closed callers

World Reuse Pattern: Use this package as the single contract source, then pair it with:

  • Cloud Run signer service implementation in apps/custodial-signer
  • Shared typed client in shared-utils/src/crypto/custodial-signer
  • Runtime fail-closed callers (workers/server) that only send allowlisted actions

Data Authorization Engine Features

  • Zero Dependencies - Uses only Web Crypto API
  • DID Support - Generate and manage did:key identifiers
  • UCAN Tokens - Create, parse, verify, and delegate capabilities
  • XPath Selection - Target specific nodes in your data tree
  • Access Control - Fine-grained per-user, per-node, per-operation rules
  • Firebase-Style Rules - Declarative security rules
  • Sandboxing - Designate collaborative areas

Table of Contents


Installation

npm install @affectively/auth
# or
bun add @affectively/auth

Quick Start

import { generateIdentity, AccessControl } from '@affectively/auth';

// Create identity
const author = await generateIdentity({ displayName: 'Jane Author' });

// Set up access control
const ac = new AccessControl();
ac.grant(author.did, '//users/jane/**', ['read', 'write', 'delete']);
ac.createSandbox('//drafts/**');

// Check access
ac.check(author.did, '/users/jane/profile', 'write', data);
// => { allowed: true }

Real-World CMS Example

A complete content management system with pages, posts, media, embeds, and users:

const cmsData = {
  // ===================
  // PAGES
  // ===================
  pages: {
    'home': {
      id: 'home',
      title: 'Welcome to Our Site',
      slug: '/',
      status: 'published',
      template: 'homepage',
      meta: {
        description: 'The best site on the internet',
        ogImage: '/media/og-home.jpg',
        robots: 'index,follow',
      },
      sections: [
        {
          type: 'hero',
          heading: 'Hello World',
          subheading: 'We build amazing things',
          backgroundImage: '/media/hero-bg.jpg',
          cta: { text: 'Learn More', url: '/about' },
        },
        {
          type: 'features',
          items: [
            { icon: 'rocket', title: 'Fast', description: 'Lightning quick' },
            { icon: 'shield', title: 'Secure', description: 'Bank-level security' },
          ],
        },
      ],
      author: 'did:key:alice',
      createdAt: '2024-01-15T10:00:00Z',
      updatedAt: '2024-01-20T14:30:00Z',
    },

    'about': {
      id: 'about',
      title: 'About Us',
      slug: '/about',
      status: 'published',
      template: 'default',
      body: '<p>We are a team of passionate developers...</p>',
      sidebar: {
        widgets: ['team-members', 'contact-form'],
      },
      author: 'did:key:alice',
    },
  },

  // ===================
  // BLOG POSTS
  // ===================
  posts: {
    'hello-world': {
      id: 'hello-world',
      title: 'Hello World: Our First Post',
      slug: '/blog/hello-world',
      status: 'published',
      excerpt: 'Welcome to our new blog...',
      body: `
        <p>We're excited to launch our new blog!</p>
        <p>Stay tuned for more updates.</p>
      `,
      // Featured image
      featuredImage: {
        url: '/media/posts/hello-world-hero.jpg',
        alt: 'Hello World banner',
        width: 1200,
        height: 630,
        caption: 'Photo by Jane Doe',
      },
      // Image gallery (multiple images)
      gallery: [
        { url: '/media/posts/gallery-1.jpg', alt: 'Team meeting' },
        { url: '/media/posts/gallery-2.jpg', alt: 'Office space' },
        { url: '/media/posts/gallery-3.jpg', alt: 'Product demo' },
      ],
      categories: ['announcements', 'company'],
      tags: ['launch', 'blog', 'news'],
      author: 'did:key:alice',
      coAuthors: ['did:key:bob'],
      publishedAt: '2024-01-15T10:00:00Z',
      // SEO metadata
      meta: {
        title: 'Hello World - Our Blog',
        description: 'Read our first blog post...',
        canonical: 'https://example.com/blog/hello-world',
      },
      // Comments section
      comments: {
        enabled: true,
        moderation: 'auto',
        items: [
          {
            id: 'comment-1',
            author: 'did:key:visitor1',
            authorName: 'John Visitor',
            body: 'Great post!',
            status: 'approved',
            createdAt: '2024-01-16T08:00:00Z',
          },
        ],
      },
    },

    'product-launch': {
      id: 'product-launch',
      title: 'Announcing Our New Product',
      slug: '/blog/product-launch',
      status: 'published',
      body: '<p>Today we announce...</p>',
      // Embedded content (inflated oEmbeds)
      embeds: [
        {
          type: 'youtube',
          url: 'https://youtube.com/watch?v=abc123',
          oembed: {
            title: 'Product Demo Video',
            thumbnail_url: 'https://img.youtube.com/vi/abc123/maxresdefault.jpg',
            html: '<iframe src="https://youtube.com/embed/abc123" allowfullscreen></iframe>',
            width: 560,
            height: 315,
            provider_name: 'YouTube',
          },
        },
        {
          type: 'twitter',
          url: 'https://twitter.com/user/status/123456',
          oembed: {
            html: '<blockquote class="twitter-tweet">...</blockquote>',
            author_name: '@user',
            provider_name: 'Twitter',
          },
        },
        {
          type: 'spotify',
          url: 'https://open.spotify.com/track/xyz',
          oembed: {
            title: 'Launch Day Playlist',
            html: '<iframe src="https://open.spotify.com/embed/track/xyz"></iframe>',
            provider_name: 'Spotify',
          },
        },
      ],
      author: 'did:key:bob',
    },
  },

  // ===================
  // MEDIA LIBRARY
  // ===================
  media: {
    'hero-bg.jpg': {
      id: 'hero-bg.jpg',
      filename: 'hero-bg.jpg',
      url: '/uploads/hero-bg.jpg',
      mimeType: 'image/jpeg',
      size: 245000,
      width: 1920,
      height: 1080,
      alt: 'Hero background',
      folder: 'backgrounds',
      uploadedBy: 'did:key:alice',
      uploadedAt: '2024-01-10T09:00:00Z',
      // Responsive image variants
      variants: {
        thumbnail: { url: '/uploads/hero-bg-thumb.jpg', width: 150, height: 84 },
        medium: { url: '/uploads/hero-bg-medium.jpg', width: 800, height: 450 },
        large: { url: '/uploads/hero-bg-large.jpg', width: 1200, height: 675 },
      },
    },

    'document.pdf': {
      id: 'document.pdf',
      filename: 'annual-report-2024.pdf',
      url: '/uploads/annual-report-2024.pdf',
      mimeType: 'application/pdf',
      size: 1500000,
      folder: 'documents',
      uploadedBy: 'did:key:alice',
      visibility: 'private',
      allowedUsers: ['did:key:alice', 'did:key:bob'],
    },
  },

  // ===================
  // USERS
  // ===================
  users: {
    'did:key:alice': {
      did: 'did:key:alice',
      displayName: 'Alice Smith',
      email: '[email protected]',
      avatar: '/media/avatars/alice.jpg',
      role: 'admin',
      bio: 'Founder and CEO',
      social: { twitter: '@alicesmith', linkedin: 'alicesmith' },
      preferences: {
        theme: 'dark',
        notifications: { email: true, push: false },
      },
    },
    'did:key:bob': {
      did: 'did:key:bob',
      displayName: 'Bob Jones',
      role: 'editor',
    },
    'did:key:charlie': {
      did: 'did:key:charlie',
      displayName: 'Charlie Brown',
      role: 'author',
    },
  },

  // ===================
  // NAVIGATION
  // ===================
  navigation: {
    main: {
      id: 'main',
      items: [
        { label: 'Home', url: '/' },
        { label: 'About', url: '/about' },
        { label: 'Products', url: '/products', children: [
          { label: 'Product A', url: '/products/a' },
          { label: 'Product B', url: '/products/b' },
        ]},
        { label: 'Blog', url: '/blog' },
      ],
    },
  },

  // ===================
  // SETTINGS
  // ===================
  settings: {
    site: { name: 'My Site', tagline: 'Building the future' },
    seo: { defaultTitle: 'My Site', titleTemplate: '%s | My Site' },
    integrations: {
      mailchimp: { apiKey: '***', listId: 'abc123' },
      stripe: { publicKey: 'pk_***' },
    },
  },

  // ===================
  // FORMS
  // ===================
  forms: {
    contact: {
      id: 'contact',
      name: 'Contact Form',
      fields: [
        { name: 'name', type: 'text', required: true },
        { name: 'email', type: 'email', required: true },
        { name: 'message', type: 'textarea', required: true },
      ],
      submissions: [
        { id: 'sub-1', data: { name: 'Jane', email: '[email protected]', message: 'Hi!' }, status: 'unread' },
      ],
    },
  },
};

Access Control for the CMS

import { AccessControl } from '@affectively/auth';

const ac = new AccessControl();

// =====================
// PUBLIC ACCESS
// =====================

// Published pages and posts
ac.grantPublic('//pages/*[status="published"]', 'read');
ac.grantPublic('//posts/*[status="published"]', 'read');

// Public media (not private)
ac.grantPublic('//media/*[visibility!="private"]', 'read');

// Navigation and site settings
ac.grantPublic('//navigation/**', 'read');
ac.grantPublic('//settings/site', 'read');
ac.grantPublic('//settings/seo', 'read');

// =====================
// AUTHENTICATED USERS
// =====================

// Users can edit their own profile
ac.grant('did:key:alice', '//users/did:key:alice/**', ['read', 'write']);
ac.grant('did:key:bob', '//users/did:key:bob/**', ['read', 'write']);

// Anyone can submit forms
ac.grant('*', '//forms/*/submissions', 'write', { constraints: { requireAuth: true } });

// =====================
// AUTHORS
// =====================

// Authors can create/edit their own posts
ac.grant('did:key:charlie', '//posts/*[author="did:key:charlie"]/**', ['read', 'write']);

// Authors can read all posts (for reference)
ac.grant('did:key:charlie', '//posts/**', 'read');

// Authors can upload media
ac.grant('did:key:charlie', '//media/**', ['read', 'write']);

// Authors manage comments on their posts
ac.grant('did:key:charlie', '//posts/*[author="did:key:charlie"]/comments/**', ['read', 'write', 'delete']);

// =====================
// EDITORS
// =====================

// Editors can edit post CONTENT (title, body, excerpt, gallery, embeds)
ac.grant('did:key:bob', '//posts/**/body', ['read', 'write']);
ac.grant('did:key:bob', '//posts/**/title', ['read', 'write']);
ac.grant('did:key:bob', '//posts/**/excerpt', ['read', 'write']);
ac.grant('did:key:bob', '//posts/**/gallery/**', ['read', 'write']);
ac.grant('did:key:bob', '//posts/**/embeds/**', ['read', 'write']);
ac.grant('did:key:bob', '//posts/**/featuredImage/**', ['read', 'write']);

// Editors can manage all comments
ac.grant('did:key:bob', '//posts/**/comments/**', ['read', 'write', 'delete']);

// Editors can edit pages
ac.grant('did:key:bob', '//pages/**', ['read', 'write']);

// Editors can manage media
ac.grant('did:key:bob', '//media/**', ['read', 'write', 'delete']);

// Editors CANNOT change ownership or status (higher priority deny)
ac.deny('did:key:bob', '//posts/*/author', 'write', { priority: 50 });
ac.deny('did:key:bob', '//posts/*/status', 'write', { priority: 50 });

// =====================
// ADMINS
// =====================

// Full access
ac.grant('did:key:alice', '//**', '*');

// Even admins can't delete the homepage
ac.deny('*', '//pages/home', 'delete', { priority: 100 });

// =====================
// SENSITIVE DATA
// =====================

// API keys are admin-only
ac.deny('*', '//settings/integrations/**', '*');
ac.grant('did:key:alice', '//settings/integrations/**', '*', { priority: 50 });

// Form submissions are editor+ only
ac.deny('*', '//forms/*/submissions/**', 'read');
ac.grant('did:key:bob', '//forms/*/submissions/**', ['read', 'write']);

XPath Queries for the CMS

import { select, getLeaves, getBranches } from '@affectively/auth';

// Get all published posts
select('//posts/*[status="published"]', cmsData);

// Get all images in galleries
select('//gallery/*', cmsData);

// Get all oEmbed data
select('//embeds/*/oembed', cmsData);

// Get all YouTube embeds specifically
select('//embeds/*[type="youtube"]', cmsData);

// Get all URLs in the entire CMS
select('//url', cmsData);

// Get all user-editable text
select('//title', cmsData);
select('//body', cmsData);
select('//excerpt', cmsData);

// Get all images (featured + gallery + variants)
select('//featuredImage', cmsData);
select('//gallery/*', cmsData);
select('//variants/*', cmsData);

// Get media by uploader
select('//media/*[uploadedBy="did:key:alice"]', cmsData);

// Get pending comments
select('//comments/items/*[status="pending"]', cmsData);

// Get all form submissions
select('//forms/*/submissions/*', cmsData);

// Get all leaf values (for search indexing)
getLeaves(cmsData);

// Get all objects/sections (for editing)
getBranches(cmsData);

XPath Node Selection

Select specific nodes in your data tree using XPath-inspired expressions.

Axes

| Syntax | Description | Example | |--------|-------------|---------| | / | Direct children | /posts/hello-world | | // | Any depth (descendant) | //email | | * | Any single segment | /users/*/profile | | ** | Any path (recursive) | //settings/** |

Node Type Functions

| Function | Matches | Use Case | |----------|---------|----------| | leaf() | Strings, numbers, booleans | All content values | | branch() | Objects | All sections/containers | | array() | Arrays | All lists/galleries | | text() | Strings only | Text content | | node() | Everything | All nodes |

// All text in a post
select('/posts/hello-world//text()', data);

// All arrays (galleries, navigation items)
select('//array()', data);

// All sections (objects) on homepage
select('/pages/home/sections//branch()', data);

Predicates

| Syntax | Description | Example | |--------|-------------|---------| | [prop="value"] | Equals | /*[status="published"] | | [prop!="value"] | Not equals | /*[role!="guest"] | | [prop>value] | Greater than | /*[price>100] | | [prop] | Property exists | /*[featuredImage] | | [0] | First item | /items[0] | | [-1] | Last item | /items[-1] | | [contains(p,"x")] | Contains | /*[contains(tags,"featured")] | | [startsWith(p,"x")] | Starts with | /*[startsWith(slug,"/blog")] | | [matches(p,"re")] | Regex | /*[matches(email,"@company\\.com")] |

// Posts by author
select('//posts/*[author="did:key:alice"]', data);

// Posts with galleries
select('//posts/*[gallery]', data);

// Large files
select('//media/*[size>1000000]', data);

// Posts in category
select('//posts/*[contains(categories,"news")]', data);

Access Control

Operations

| Operation | Use For | |-----------|---------| | read | Viewing, fetching | | write | Creating, updating | | delete | Removing | | admin | Administrative actions | | * | All operations |

Methods

const ac = new AccessControl();

// Grant access
ac.grant(did, '//path/**', ['read', 'write'], { priority: 10, expiresIn: 86400000 });
ac.grantPublic('//path/**', 'read');

// Deny access
ac.deny(did, '//path/**', ['write', 'delete'], { priority: 100 });
ac.denyPublic('//admin/**', '*');

// Check access
const result = ac.check(did, '/users/alice', 'write', data);
// => { allowed: true, reason: 'Granted by rule: //users/alice/**' }

// Get accessible nodes
const nodes = ac.getAccessibleNodes(did, 'read', data);

// Patterns
ac.createSandbox('//drafts/**');           // Public read/write
ac.createPublicReadOnly('//docs/**');      // Public read, no write
ac.createUserOwned(did, '//users/me/**');  // Full control

// Expire rules
ac.grant(did, '//premium/**', 'read', { expiresIn: 7 * 24 * 60 * 60 * 1000 });

// Export/import
const json = ac.exportRules();
ac.importRules(json);

Priority

Higher priority rules are checked first. Default: grant=0, deny=100.

// Priority 100: Deny everyone
ac.deny('*', '//admin/**', '*', { priority: 100 });

// Priority 50: Allow admin user
ac.grant(adminDID, '//admin/**', '*', { priority: 50 });

Firebase-Style Rules

import { parseRules, evaluateRules } from '@affectively/auth';

const rules = parseRules({
  rules: {
    pages: {
      '$pageId': {
        '.read': 'resource.status === "published"',
        '.write': 'auth.role === "admin" || auth.role === "editor"',
      }
    },
    posts: {
      '$postId': {
        '.read': 'resource.status === "published" || auth.did === resource.author',
        '.write': 'auth.did === resource.author || auth.role === "editor"',
        comments: {
          '.read': true,
          '.write': 'auth !== null',
        }
      }
    },
    settings: {
      site: { '.read': true, '.write': 'auth.role === "admin"' },
      integrations: {
        '.read': 'auth.role === "admin"',
        '.write': 'auth.role === "admin"',
      },
    },
  }
});

const result = evaluateRules(rules, '/posts/hello-world', 'read', {
  auth: { did: 'did:key:visitor', role: 'user' },
  resource: { status: 'published', author: 'did:key:alice' }
});
// => { allowed: true }

Expression Syntax

// Boolean
'.read': true

// Comparisons
'.read': 'auth.did === $userId'
'.read': 'resource.count > 10'

// Boolean operators
'.read': 'auth !== null && auth.verified'
'.read': 'auth.role === "admin" || auth.role === "editor"'

// Methods
'.read': 'auth.capabilities.includes("read")'
'.read': 'resource.tags.includes("public")'
'.read': 'auth.email.endsWith("@company.com")'

Identity & UCAN Tokens

Generate Identity

import { generateIdentity, sign, verify } from '@affectively/auth';

const alice = await generateIdentity({
  algorithm: 'ES256',
  displayName: 'Alice',
  includeEncryptionKey: true,
});

console.log(alice.did); // did:key:z6Mkf...

// Sign data
const sig = await sign(alice, new TextEncoder().encode('Hello'));

// Verify
const valid = await verify(alice.signingKey.publicKey, sig, data);

UCAN Tokens

import { createUCAN, verifyUCAN, delegateCapabilities } from '@affectively/auth';

// Create token
const token = await createUCAN(
  alice,
  bobDID,
  [
    { can: 'file/read', with: 'storage://bucket/*' },
    { can: 'file/write', with: 'storage://bucket/uploads/*' },
  ],
  { expirationSeconds: 3600 }
);

// Verify
const result = await verifyUCAN(token, alice.signingKey.publicKey, {
  audience: bobDID,
  requiredCapabilities: [{ can: 'file/read', with: '*' }]
});

// Delegate (attenuate)
const childToken = await delegateCapabilities(
  token,
  bob,
  charlieDID,
  [{ can: 'file/read', with: 'storage://bucket/docs/*' }]
);

API Reference

Identity

  • generateIdentity(options?) - Create identity
  • sign(identity, data) - Sign data
  • verify(publicKey, signature, data) - Verify signature
  • deriveDID(publicKey) - Derive DID from key

UCAN

  • createUCAN(issuer, audience, capabilities, options?) - Create token
  • verifyUCAN(token, publicKey, options?) - Verify token
  • parseUCAN(token) - Parse without verification
  • delegateCapabilities(parent, issuer, audience, caps, options?) - Delegate

XPath

  • select(expression, data) - Select nodes
  • compile(expression) - Compile selector
  • getLeaves(data) - All terminal values
  • getBranches(data) - All objects
  • getValue(data, path) - Get value
  • pathExists(data, path) - Check existence

Access Control

  • ac.grant(subject, selector, operations, options?) - Grant
  • ac.deny(subject, selector, operations?, options?) - Deny
  • ac.check(subject, path, operation, data?) - Check
  • ac.getAccessibleNodes(subject, operation, data) - List accessible
  • ac.createSandbox(selector) - Public sandbox
  • ac.createPublicReadOnly(selector) - Read-only area
  • ac.createUserOwned(subject, selector) - User area

Firebase Rules

  • parseRules(json) - Parse rules
  • evaluateRules(rules, path, operation, context) - Evaluate
  • rules() - Builder

Storage

  • MemoryKeyStorage, MemoryIdentityStorage - Volatile
  • IndexedDBKeyStorage, IndexedDBIdentityStorage - Persistent
  • createStorage() - Auto-detect best option

License

MIT

Related